あなたが大量のトラフィックを処理できるようにするサービスを実行している場合は、バックエンドサーバー間でそのトラフィックの多くをバランスを取ることができます。市場には多くの生産レベルの負荷バランサー(NGINX、HAProxyなど)がありますが、それらがどのように動作するかを知ることは良い知識です。
A Simple HTTP Load Balancer in Go Using Standard Library この実装では、ラウンドロビンアルゴリズムを使用して、バックエンドサーバーのコレクション間で入力リクエストを均等に配布します。
基本構造
まず、当社のコアデータ構造を定義する必要があります。当社の負荷バランスは、複数のバックエンドサーバとその状態を追跡します。
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
}
今、ここで覚えておくべきいくつかの重要なポイント:
- バックエンドの構造は、そのURLとライブ状態を持つ単一のバックエンドサーバを表します。
- 私たちは、同時環境における各バックエンドのライブ状態を安全に更新し、チェックするためにムテックスを使用しています。
- LoadBalancerはバックエンドのリストを追跡し、ラウンドロビンアルゴリズムのカウンターを維持します。
- 我々は、原子の操作を用いて、同時に存在する環境における計量を安全に増加させます。
- NextBackend メソッドは、 round-robin アルゴリズムを実装し、不健康なバックエンドを省略します。
健康チェック
バックエンドの不利用性を検出することは、任意の負荷バランス装置の重要な機能の1つです。
// 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()
}
}
}
この健康チェックは簡単です:
- バックエンドとの TCP 接続を開始しようとします。
- この接続が成功した場合、バックエンドは生きている、そうでなければ死んでいる。
- チェックは、HealthCheckPeriodically 関数で指定された間隔で実行されます。
生産環境では、実際には HTTP リクエストを特定のエンドポイントに送信するより高度な健康チェックを望むかもしれませんが、これは私たちを始めます。
HTTP ハンドラー
次に、リクエストを受信し、バックエンドにルーティングする HTTP マネージャーを実装しましょう。
// 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)
}
そこで魔法が起こる:
- 私たちのラウンドロビンアルゴリズムは、次の利用可能なバックエンドを提供します。
- バックエンドが利用できない場合、503 Service Unavailable エラーを返します。
- そうでなければ、Goの内部逆プロキシを介して選択したバックエンドにリクエストをプロキシします。
どのように注目するかnet/http/httputil
パッケージは、TheReverseProxy
私たちのために HTTP リクエストをプロキシするすべての複雑さを処理するタイプです。
「Putting It All Together
最後に、実施をしましょう。main
Function to configure and start our load balancer: 負荷バランザーを構成して起動する機能:
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)
}
}
次に、主な機能で何が起きているかです。
- 私たちは、パースしたコマンドラインの旗に基づいてポートを構成します。
- バックエンドサーバーのリストを作成しました。
- 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
- 初期健康診断を行います。
- 定期的な健康診断のためのゴロチナを開始します。
- 最後に、HTTP サーバをハンドラーとして負荷バランザーで起動します。
Load Balancerのテスト
したがって、再び負荷バランザーをテストするには、いくつかのバックエンドサーバーが必要です。バックエンドサーバーの天真な実装は、このように見えるかもしれません。
// 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)
}
}
このバックエンドの複数のインスタンスを異なるポートで実行する:
go build -o backend backend.go
./backend -port 8081 &
./backend -port 8082 &
./backend -port 8083 &
次に、負荷バランスを作成して実行します。
go build -o load-balancer main.go
./load-balancer
今、それをテストするためにいくつかの要請をしてください:
curl http://localhost:8080/test
複数のリクエストを作成し、バックエンド サーバーでラウンド ロビン モードで配布されます。
潜在的な改善
これは最小限の実装ですが、それを改善するためにできることはたくさんあります:
-
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.
私たちは、Goの標準ライブラリ以外の何ものでも使用するシンプルで効果的なHTTPロードバランザーを構築しました。この例は、ネットワークプログラミングにおける重要な概念とGoの内蔵ネットワーク機能の強さを示しています。
これは生産準備ができていませんが、負荷バランスがどのように機能するかを理解するための堅固な基盤を提供します。完全なソリューションは約150行のコード - Goの表現力と標準ライブラリの強さを証明します。
生産用途では、より多くの機能を構築し、強力なエラー処理を追加したいのですが、コアコンセプトはすべて同じです。これらの基本原則を理解すると、道路で使用する負荷バランザーをよりよく構成し、デバッグできます。
ここでソースコードを見つけることができます。https://github.com/rezmoss/simple-load-balancer