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

在這篇文章中,我們將使用 循環法 (Round Robin algorithm) 在 Go 中構建一個簡單的應用程式負載平衡器。本帖的目標是理解負載平衡器的運作原理,逐步介紹。
負載平衡器是一個系統,用於在多台伺服器之間分配進來的網路流量。它確保不會讓單一伺服器承受過多的負載,防止瓶頸並改善整體用戶體驗。負載平衡的策略也確保如果一台伺服器故障,流量可以自動重新路由到另一台可用的伺服器上,從而減少故障的影響並提高可用性。
有不同的算法和策略來分配流量:
在這篇文章中,我們將專注於實現一個 循環法 的負載平衡器。
循環法算法以循環的方式將每個進來的請求發送到下一台可用的伺服器。如果伺服器 A 處理第一個請求,則伺服器 B 處理第二個,伺服器 C 處理第三個。一旦所有伺服器都收到請求,則再次從伺服器 A 開始。
現在,讓我們進入程式碼並構建我們的負載平衡器!
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 標誌,這表明伺服器是否可用來處理請求。
我們的負載平衡器的核心是循環法算法。以下是其工作原理:
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 來避免任何競態條件。我們的配置儲存在 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"
]
}
我們希望在將任何進來的流量路由到伺服器之前,確保伺服器是健康的。這是通過定期向每台伺服器發送健康檢查來實現的:
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,阻止未來流量路由到它。
當負載平衡器收到請求時,它會使用 反向代理 (reverse proxy) 將請求轉發到下一台可用的伺服器。在 Golang 中,httputil 包提供了一種內建的方法來處理反向代理,我們將在程式碼中使用 ReverseProxy 函數:
func (s *Server) ReverseProxy() *httputil.ReverseProxy {
return httputil.NewSingleHostReverseProxy(s.URL)
}
反向代理是一個位於客戶端和一個或多個後端伺服器之間的伺服器。它接收客戶端的請求,將其轉發到其中一台後端伺服器,然後將伺服器的響應返回給客戶端。客戶端與代理交互,並不知道具體的後端伺服器是哪一台。
在我們的情況下,負載平衡器作為反向代理,位於多台伺服器的前面,並將進來的 HTTP 請求分配到它們之間。
當客戶端向負載平衡器發送請求時,它使用 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 以供調試使用(儘管在生產環境中應避免這樣暴露內部伺服器的詳情)。
最後,我們在指定的港口啟動負載平衡器:
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