Si vous exécutez des services qui devraient être en mesure de gérer une grande quantité de trafic, vous pouvez charger beaucoup de ce trafic entre vos serveurs de backend.Il existe de nombreux équilibreurs de charge de niveau de production sur le marché (NGINX, HAProxy, etc.) mais savoir comment ils fonctionnent derrière la scène est une bonne connaissance.
A Simple HTTP Load Balancer in Go Using Standard Library, Dans cette mise en œuvre, nous utiliserons un algorithme de robin rond pour distribuer uniformément les demandes entrantes entre une collection de serveurs backend.
La structure de base
Tout d'abord, nous devons définir nos structures de données de base.Notre équilibreur de charge suivra plusieurs serveurs de back-end et leur santé:
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
}
Voici quelques points clés à garder à l’esprit :
- Le backend struct représente un seul serveur backend avec son URL et son état vivant.
- Nous utilisons un mutex pour mettre à jour en toute sécurité et vérifier l'état vivant de chaque backend dans un environnement concomitant.
- Le LoadBalancer suit une liste de back-ends et maintient un compteur pour l'algorithme de robin rond.
- Nous utilisons des opérations atomiques pour accroître en toute sécurité le compteur dans un environnement concurrentiel.
- La méthode NextBackend met en œuvre l'algorithme round-robin, sautant les backends malsains.
Vérification de santé
Détecter l'indisponibilité du backend est l'une des fonctions critiques de tout équilibreur de charge.
// 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()
}
}
}
Le contrôle de la santé est simple :
- Nous essayons d'initier une connexion TCP avec le backend.
- Si cette connexion réussit, le backend est vivant, sinon il est mort.
- La vérification est exécutée à un intervalle spécifié dans la fonction HealthCheckPeriodically.
Dans un environnement de production, vous voudriez probablement un contrôle de santé plus avancé qui fait en fait une demande HTTP à un endpoint spécifié, mais cela nous fait commencer.
Le gestionnaire HTTP
Avançons et mettons en œuvre le gestionnaire HTTP qui recevra la demande et la dirigera vers nos backends :
// 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)
}
Et c’est là que la magie se produit :
- Notre algorithme rond-robin nous donne le prochain backend disponible.
- Dans le cas où aucun backend n'est disponible, nous renvoyons une erreur 503 Service Non disponible.
- Dans le cas contraire, nous proxyons la demande au backend sélectionné via le proxy interne inverse de Go.
Notez comment lenet/http/httputil
Le paquet fournit leReverseProxy
type, qui traite toutes les complexités de la proxy des demandes HTTP pour nous.
Tout mettre ensemble
Enfin, mettons en œuvre lemain
fonction pour configurer et démarrer notre équilibreur de charge:
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)
}
}
Voici ce qui se passe dans la fonction principale :
- Nous configurons le port en fonction des drapeaux de ligne de commande que nous analysons.
- Nous avons créé une liste de serveurs de back-end.
- 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
- Nous effectuons un contrôle de santé initial.
- Nous commençons une goroutine pour les contrôles de santé périodiques.
- Enfin, nous démarrons le serveur HTTP avec notre équilibreur de charge en tant que gestionnaire.
Tester l’équilibre de charge
Donc, encore une fois, pour tester notre équilibreur de charge, nous avons besoin de serveurs de back-end. Une mise en œuvre naïve d'un serveur de back-end pourrait ressembler à ceci:
// 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)
}
}
Exécuter plusieurs instances de ce backend sur différents ports:
go build -o backend backend.go
./backend -port 8081 &
./backend -port 8082 &
./backend -port 8083 &
Construisez et exécutez l’équilibreur de charge :
go build -o load-balancer main.go
./load-balancer
Maintenant, faites quelques demandes pour le tester:
curl http://localhost:8080/test
Faites plusieurs demandes et vous verrez qu'elles sont distribuées sur vos serveurs de back-end en mode round-robin.
Des améliorations potentielles
C'est une mise en œuvre minimale, mais il y a beaucoup de choses que vous pouvez faire pour l'améliorer:
-
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.
Nous avons construit un équilibreur de charge HTTP simple mais efficace, en utilisant rien d'autre que la bibliothèque standard de Go. Cet exemple illustre des concepts importants dans la programmation réseau et la puissance des fonctionnalités de réseau intégrées de Go.
Bien que ce ne soit pas prêt à la production, il fournit une base solide pour comprendre comment fonctionnent les équilibreurs de charge.La solution complète est d'environ 150 lignes de code - un témoignage de l'expressivité de Go et de la force de sa bibliothèque standard.
Pour l'utilisation de la production, vous voudriez développer plus de fonctionnalités et ajouter un traitement des erreurs robuste, mais les concepts de base sont tous les mêmes. Une compréhension de ces principes de base vous préparera à mieux configurer et déboguer tout équilibreur de charge que vous utiliserez sur la route.
Vous pouvez trouver le code source icihttps://github.com/rezmoss/simple-load-balancer