負載平衡器在現代軟體開發中至關重要。如果你曾經想過,請求是如何在多台伺服器之間分配的,或者為什麼某些網站即使在高流量時仍然感覺更快,答案往往在於高效的負載平衡。

Without Load Balancer

在這篇文章中,我們將使用 循環法 (Round Robin algorithm) 在 Go 中構建一個簡單的應用程式負載平衡器。本帖的目標是理解負載平衡器的運作原理,逐步介紹。

什麼是負載平衡器?

負載平衡器是一個系統,用於在多台伺服器之間分配進來的網路流量。它確保不會讓單一伺服器承受過多的負載,防止瓶頸並改善整體用戶體驗。負載平衡的策略也確保如果一台伺服器故障,流量可以自動重新路由到另一台可用的伺服器上,從而減少故障的影響並提高可用性。

為什麼我們使用負載平衡器?

  • 高可用性:通過分配流量,負載平衡器確保即使一台伺服器故障,流量也可以路由到其他健康的伺服器,從而增加應用程式的韌性。
  • 可擴展性:負載平衡器允許你通過增加更多伺服器來水平擴展系統,以應對增長的流量。
  • 效率:它通過確保所有伺服器均等地分享工作負載來最大化資源利用率。

負載平衡算法

有不同的算法和策略來分配流量:

  • 循環法:一種最簡單的方法,將請求依序分配給可用伺服器。一旦到達最後一台伺服器,則重新從頭開始。
  • 加權循環法:類似於循環法,但每台伺服器被分配一些固定的數值權重。這個權重用於決定流量路由的伺服器。
  • 最少連接數:將流量路由到活動連接數最少的伺服器。
  • IP 哈希:根據客戶端的 IP 地址選擇伺服器。

在這篇文章中,我們將專注於實現一個 循環法 的負載平衡器。

什麼是循環法算法?

循環法算法以循環的方式將每個進來的請求發送到下一台可用的伺服器。如果伺服器 A 處理第一個請求,則伺服器 B 處理第二個,伺服器 C 處理第三個。一旦所有伺服器都收到請求,則再次從伺服器 A 開始。

現在,讓我們進入程式碼並構建我們的負載平衡器!

步驟 1:定義負載平衡器和伺服器

type LoadBalancer struct {
    Current int
    Mutex   sync.Mutex
}

我們首先定義一個簡單的 LoadBalancer 結構,其中包含一個 Current 欄位,用於跟蹤下一個應該處理請求的伺服器。Mutex 確保我們的程式碼在並發使用時是安全的。

每台我們進行負載平衡的伺服器由 Server 結構定義:

type Server struct {
    URL       *url.URL
    IsHealthy bool
    Mutex     sync.Mutex
}

在這裡,每台伺服器都有一個 URL 和一個 IsHealthy 標誌,這表明伺服器是否可用來處理請求。

步驟 2:循環法算法

我們的負載平衡器的核心是循環法算法。以下是其工作原理:

func (lb *LoadBalancer) getNextServer(servers []*Server) *Server {
    lb.Mutex.Lock()
    defer lb.Mutex.Unlock()

    for i := 0; i < len(servers); i++ {
        idx := lb.Current % len(servers)
        nextServer := servers[idx]
        lb.Current++

        nextServer.Mutex.Lock()
        isHealthy := nextServer.IsHealthy
        nextServer.Mutex.Unlock()

        if isHealthy {
            return nextServer
        }
    }

    return nil
}
  • 此方法以循環的方式遍歷伺服器列表。如果選定的伺服器是健康的,則返回該伺服器以處理進來的請求。
  • 我們使用 Mutex 確保只有一個 goroutine 可以訪問和修改負載平衡器的 Current 欄位。這確保了在同時處理多個請求時,循環法算法的正確運作。
  • 每個 Server 也有自己的 Mutex。當我們檢查 IsHealthy 欄位時,我們鎖定伺服器的 Mutex 以防止多個 goroutine 的並發訪問。
  • 如果沒有 Mutex 鎖定,則可能會有其他 goroutine 更改該值,這可能導致讀取不正確或不一致的數據。
  • 我們在更新 Current 欄位或讀取 IsHealthy 欄位值之後立即解鎖 Mutex,以使關鍵區域盡可能小。這樣,我們使用 Mutex 來避免任何競態條件。

步驟 3:配置負載平衡器

我們的配置儲存在 config.json 檔案中,其中包含伺服器 URL 和健康檢查間隔(下面的部分將詳細介紹)。

type Config struct {
    Port                string   `json:"port"`
    HealthCheckInterval string   `json:"healthCheckInterval"`
    Servers             []string `json:"servers"`
}

配置文件可能看起來像這樣:

{
  "port": ":8080",
  "healthCheckInterval": "2s",
  "servers": [
    "http://localhost:5001",
    "http://localhost:5002",
    "http://localhost:5003",
    "http://localhost:5004",
    "http://localhost:5005"
  ]
}

步驟 4:健康檢查

我們希望在將任何進來的流量路由到伺服器之前,確保伺服器是健康的。這是通過定期向每台伺服器發送健康檢查來實現的:

func healthCheck(s *Server, healthCheckInterval time.Duration) {
    for range time.Tick(healthCheckInterval) {
        res, err := http.Head(s.URL.String())
        s.Mutex.Lock()
        if err != nil || res.StatusCode != http.StatusOK {
            fmt.Printf("%s is down\n", s.URL)
            s.IsHealthy = false
        } else {
            s.IsHealthy = true
        }
        s.Mutex.Unlock()
    }
}

每隔幾秒(根據配置指定),負載平衡器會向每台伺服器發送一個 HEAD 請求,以檢查其是否健康。如果一台伺服器故障,則將 IsHealthy 標誌設置為 false,阻止未來流量路由到它。

步驟 5:反向代理

當負載平衡器收到請求時,它會使用 反向代理 (reverse proxy) 將請求轉發到下一台可用的伺服器。在 Golang 中,httputil 包提供了一種內建的方法來處理反向代理,我們將在程式碼中使用 ReverseProxy 函數:

func (s *Server) ReverseProxy() *httputil.ReverseProxy {
    return httputil.NewSingleHostReverseProxy(s.URL)
}
什麼是反向代理?

反向代理是一個位於客戶端和一個或多個後端伺服器之間的伺服器。它接收客戶端的請求,將其轉發到其中一台後端伺服器,然後將伺服器的響應返回給客戶端。客戶端與代理交互,並不知道具體的後端伺服器是哪一台。

在我們的情況下,負載平衡器作為反向代理,位於多台伺服器的前面,並將進來的 HTTP 請求分配到它們之間。

步驟 6:處理請求

當客戶端向負載平衡器發送請求時,它使用 getNextServer 函數中實現的循環法算法選擇下一台可用的健康伺服器,並將客戶請求代理到該伺服器。如果沒有可用的健康伺服器,我們就會向客戶發送服務不可用的錯誤。

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        server := lb.getNextServer(servers)
        if server == nil {
            http.Error(w, "No healthy server available", http.StatusServiceUnavailable)
            return
        }
        w.Header().Add("X-Forwarded-Server", server.URL.String())
        server.ReverseProxy().ServeHTTP(w, r)
    })

ReverseProxy 方法將請求代理到實際的伺服器,我們還添加了一個自訂標頭 X-Forwarded-Server 以供調試使用(儘管在生產環境中應避免這樣暴露內部伺服器的詳情)。

步驟 7:啟動負載平衡器

最後,我們在指定的港口啟動負載平衡器:

log.Println("Starting load balancer on port", config.Port)
err = http.ListenAndServe(config.Port, nil)
if err != nil {
        log.Fatalf("Error starting load balancer: %s\n", err.Error())
}

工作示範

https://www.loom.com/share/d21e79861ce94e6b80abb0314854a43b?sid=c065bc6a-51cd-43ad-a2f7-54307c4c5570

結論

在這篇文章中,我們使用循環法算法從零開始構建了一個基本的負載平衡器。這是一種簡單而有效的方式來在多台伺服器之間分配流量,確保系統能夠高效地處理更高的負載。

還有許多方面可以深入探索,例如添加複雜的健康檢查、實現不同的負載平衡算法或改善容錯性。但這個基本示例可以作為進一步構建的堅實基礎。

你可以在 這裡 找到源碼。


原文出處:https://dev.to/vivekalhat/building-a-simple-load-balancer-in-go-70d

按讚的人:

共有 0 則留言