Nếu bạn đang chạy các dịch vụ nên có thể xử lý một lượng lớn lưu lượng truy cập, bạn có thể tải cân bằng rất nhiều lưu lượng truy cập đó giữa các máy chủ backend của bạn. Có rất nhiều chất cân bằng tải cấp sản xuất trên thị trường (NGINX, HAProxy, v.v.) nhưng biết làm thế nào họ làm việc đằng sau cảnh là kiến thức tốt.
A Simple HTTP Load Balancer in Go Using Standard Library, Trong thực hiện này, chúng tôi sẽ sử dụng thuật toán round-robin để phân phối đồng đều các yêu cầu đến giữa một tập hợp các máy chủ backend.
Cấu trúc cơ bản
Đầu tiên, chúng ta cần xác định cấu trúc dữ liệu cốt lõi của chúng tôi. bộ cân bằng tải của chúng tôi sẽ theo dõi nhiều máy chủ backend và tình trạng của chúng:
package main
import (
"flag"
"fmt"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"sync/atomic"
"time"
)
// Backend represents a backend server
type Backend struct {
URL *url.URL
Alive bool
mux sync.RWMutex
ReverseProxy *httputil.ReverseProxy
}
// SetAlive updates the alive status of backend
func (b *Backend) SetAlive(alive bool) {
b.mux.Lock()
b.Alive = alive
b.mux.Unlock()
}
// IsAlive returns true when backend is alive
func (b *Backend) IsAlive() (alive bool) {
b.mux.RLock()
alive = b.Alive
b.mux.RUnlock()
return
}
// LoadBalancer represents a load balancer
type LoadBalancer struct {
backends []*Backend
current uint64
}
// NextBackend returns the next available backend to handle the request
func (lb *LoadBalancer) NextBackend() *Backend {
// Simple round-robin
next := atomic.AddUint64(&lb.current, uint64(1)) % uint64(len(lb.backends))
// Find the next available backend
for i := 0; i < len(lb.backends); i++ {
idx := (int(next) + i) % len(lb.backends)
if lb.backends[idx].IsAlive() {
return lb.backends[idx]
}
}
return nil
}
Bây giờ một vài điểm chính ở đây để ghi nhớ:
- Backend struct đại diện cho một máy chủ backend duy nhất với URL và trạng thái sống của nó.
- Chúng tôi đang sử dụng một mutex để cập nhật an toàn và kiểm tra trạng thái sống của mỗi backend trong một môi trường đồng thời.
- LoadBalancer theo dõi một danh sách các backend và duy trì một bộ đếm cho thuật toán round-robin.
- Chúng tôi sử dụng các hoạt động nguyên tử để tăng số liệu an toàn trong một môi trường đồng thời.
- Phương pháp NextBackend thực hiện thuật toán round-robin, bỏ qua các backend không lành mạnh.
Kiểm tra sức khỏe
Phát hiện sự không sẵn có của backend là một trong những chức năng quan trọng của bất kỳ bộ cân bằng tải nào. Hãy thực hiện một cơ chế kiểm tra sức khỏe rất đơn giản:
// isBackendAlive checks whether a backend is alive by establishing a TCP connection
func isBackendAlive(u *url.URL) bool {
timeout := 2 * time.Second
conn, err := net.DialTimeout("tcp", u.Host, timeout)
if err != nil {
log.Printf("Site unreachable: %s", err)
return false
}
defer conn.Close()
return true
}
// HealthCheck pings the backends and updates their status
func (lb *LoadBalancer) HealthCheck() {
for _, b := range lb.backends {
status := isBackendAlive(b.URL)
b.SetAlive(status)
if status {
log.Printf("Backend %s is alive", b.URL)
} else {
log.Printf("Backend %s is dead", b.URL)
}
}
}
// HealthCheckPeriodically runs a routine health check every interval
func (lb *LoadBalancer) HealthCheckPeriodically(interval time.Duration) {
t := time.NewTicker(interval)
for {
select {
case <-t.C:
lb.HealthCheck()
}
}
}
Kiểm tra sức khỏe này rất đơn giản:
- Chúng tôi cố gắng bắt đầu một kết nối TCP với backend.
- Nếu kết nối này thành công, thì backend vẫn còn sống; nếu không thì nó đã chết.
- Kiểm tra được thực hiện ở một khoảng thời gian được chỉ định trong hàm HealthCheckPeriodically.
Trong một môi trường sản xuất, bạn có thể muốn một kiểm tra sức khỏe tiên tiến hơn thực sự thực hiện một yêu cầu HTTP đến một số điểm cuối được chỉ định, nhưng điều này bắt đầu.
Bộ xử lý HTTP
Hãy tiếp tục và triển khai trình xử lý HTTP sẽ nhận được yêu cầu và định tuyến chúng đến các backend của chúng tôi:
// ServeHTTP implements the http.Handler interface for the LoadBalancer
func (lb *LoadBalancer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
backend := lb.NextBackend()
if backend == nil {
http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
return
}
// Forward the request to the backend
backend.ReverseProxy.ServeHTTP(w, r)
}
Và đây là nơi mà phép thuật xảy ra:
- Thuật toán round-robin của chúng tôi cung cấp cho chúng tôi backend tiếp theo có sẵn.
- Nếu không có backend nào có sẵn, chúng tôi sẽ trả về lỗi 503 Service Unavailable.
- Nếu không, chúng tôi proxy yêu cầu đến backend được chọn thông qua proxy ngược nội bộ của Go.
Lưu ý cách cácnet/http/httputil
Gói cung cấp cácReverseProxy
loại, mà xử lý tất cả các phức tạp của proxy HTTP yêu cầu cho chúng tôi.
Đặt tất cả cùng nhau
Cuối cùng, chúng ta hãy thực hiệnmain
chức năng để cấu hình và khởi động bộ cân bằng tải của chúng tôi:
func main() {
// Parse command line flags
port := flag.Int("port", 8080, "Port to serve on")
flag.Parse()
// Configure backends
serverList := []string{
"http://localhost:8081",
"http://localhost:8082",
"http://localhost:8083",
}
// Create load balancer
lb := LoadBalancer{}
// Initialize backends
for _, serverURL := range serverList {
url, err := url.Parse(serverURL)
if err != nil {
log.Fatal(err)
}
proxy := httputil.NewSingleHostReverseProxy(url)
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Error: %v", err)
http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
}
lb.backends = append(lb.backends, &Backend{
URL: url,
Alive: true,
ReverseProxy: proxy,
})
log.Printf("Configured backend: %s", url)
}
// Initial health check
lb.HealthCheck()
// Start periodic health check
go lb.HealthCheckPeriodically(time.Minute)
// Start server
server := http.Server{
Addr: fmt.Sprintf(":%d", *port),
Handler: &lb,
}
log.Printf("Load Balancer started at :%d\n", *port)
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
Bây giờ, đây là những gì đang xảy ra trong chức năng chính:
- Chúng tôi cấu hình cổng dựa trên các cờ dòng lệnh mà chúng tôi phân tích.
- Chúng tôi đã thiết lập một danh sách các máy chủ backend.
- For each backend server, we:
- Parse the URL
- Create a reverse proxy for that backend
- Add error handling for when a backend fails
- Add the backend to our load balancer
- Chúng tôi thực hiện kiểm tra sức khỏe ban đầu.
- Chúng tôi bắt đầu một goroutine cho kiểm tra sức khỏe định kỳ.
- Cuối cùng, chúng tôi bắt đầu máy chủ HTTP với bộ cân bằng tải của chúng tôi với tư cách là bộ xử lý.
Thử nghiệm Load Balancer
Vì vậy, một lần nữa để kiểm tra trình cân bằng tải của chúng tôi, chúng tôi cần một số máy chủ backend.
// Save this to backend.go
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
)
func main() {
port := flag.Int("port", 8081, "Port to serve on")
flag.Parse()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
hostname, _ := os.Hostname()
fmt.Fprintf(w, "Backend server on port %d, host: %s, Request path: %s\n", *port, hostname, r.URL.Path)
})
log.Printf("Backend started at :%d\n", *port)
if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil); err != nil {
log.Fatal(err)
}
}
Chạy nhiều trường hợp của backend này trên các cổng khác nhau:
go build -o backend backend.go
./backend -port 8081 &
./backend -port 8082 &
./backend -port 8083 &
Sau đó xây dựng và chạy Balancer tải:
go build -o load-balancer main.go
./load-balancer
Bây giờ, thực hiện một số yêu cầu để kiểm tra nó:
curl http://localhost:8080/test
Thực hiện nhiều yêu cầu và bạn sẽ thấy chúng được phân phối trên các máy chủ backend của bạn theo cách round-robin.
Những cải tiến tiềm năng
Đây là một thực hiện tối thiểu, nhưng có rất nhiều điều bạn có thể làm để cải thiện nó:
-
Different balancing algorithms: Implement weighted round-robin, least connections, or IP hash-based selection.
-
Better health checking: Make full HTTP requests to a health endpoint instead of just checking TCP connectivity.
-
Metrics collection: Track request counts, response times, and error rates for each backend.
-
Sticky sessions: Ensure requests from the same client always go to the same backend.
-
TLS support: Add HTTPS for secure connections.
-
Dynamic configuration: Allow updating the backend list without restarting.
Chúng tôi đã xây dựng một trình cân bằng tải HTTP đơn giản nhưng hiệu quả, sử dụng không có gì ngoài thư viện tiêu chuẩn của Go. ví dụ này minh họa các khái niệm quan trọng trong lập trình mạng và sức mạnh của các tính năng kết nối mạng tích hợp của Go.
Mặc dù đây không phải là sản xuất sẵn sàng, nhưng nó cung cấp một nền tảng vững chắc để hiểu làm thế nào tải cân bằng hoạt động. giải pháp hoàn chỉnh là khoảng 150 dòng mã - một bằng chứng cho sự biểu cảm của Go và sức mạnh của thư viện tiêu chuẩn của nó.
Đối với việc sử dụng sản xuất, bạn muốn xây dựng thêm các tính năng và thêm xử lý lỗi mạnh mẽ, nhưng các khái niệm cốt lõi đều giống nhau. Một sự hiểu biết về các nguyên tắc cơ bản này sẽ chuẩn bị cho bạn để cấu hình và xử lý tốt hơn bất kỳ bộ cân bằng tải nào bạn sẽ sử dụng trên đường.
Bạn có thể tìm thấy mã nguồn tại đâyhttps://github.com/rezmoss/simple-load-balancer