14,587 lectures
14,587 lectures

Ce script 150-Line Go est en fait un équilibreur de charge complet

par Rez Moss7m2025/04/23
Read on Terminal Reader
Read this story w/o Javascript

Trop long; Pour lire

Cet article vous montrera comment créer un simple équilibreur de charge HTTP dans Go, en utilisant uniquement la bibliothèque standard. Il effectue une distribution rond-robin sur les serveurs backend, des contrôles de santé pour détecter les pannes et des demandes de proxy – tout cela dans environ 150 lignes de code.
featured image - Ce script 150-Line Go est en fait un équilibreur de charge complet
Rez Moss HackerNoon profile picture

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 :

  1. Le backend struct représente un seul serveur backend avec son URL et son état vivant.
  2. Nous utilisons un mutex pour mettre à jour en toute sécurité et vérifier l'état vivant de chaque backend dans un environnement concomitant.
  3. Le LoadBalancer suit une liste de back-ends et maintient un compteur pour l'algorithme de robin rond.
  4. Nous utilisons des opérations atomiques pour accroître en toute sécurité le compteur dans un environnement concurrentiel.
  5. 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 :

  1. Nous essayons d'initier une connexion TCP avec le backend.
  2. Si cette connexion réussit, le backend est vivant, sinon il est mort.
  3. 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 :

  1. Notre algorithme rond-robin nous donne le prochain backend disponible.
  2. Dans le cas où aucun backend n'est disponible, nous renvoyons une erreur 503 Service Non disponible.
  3. Dans le cas contraire, nous proxyons la demande au backend sélectionné via le proxy interne inverse de Go.


Notez comment lenet/http/httputilLe paquet fournit leReverseProxytype, qui traite toutes les complexités de la proxy des demandes HTTP pour nous.

Tout mettre ensemble

Enfin, mettons en œuvre lemainfonction 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 :

  1. Nous configurons le port en fonction des drapeaux de ligne de commande que nous analysons.
  2. Nous avons créé une liste de serveurs de back-end.
  3. 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
  4. Nous effectuons un contrôle de santé initial.
  5. Nous commençons une goroutine pour les contrôles de santé périodiques.
  6. 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:


  1. Different balancing algorithms: Implement weighted round-robin, least connections, or IP hash-based selection.

  2. Better health checking: Make full HTTP requests to a health endpoint instead of just checking TCP connectivity.

  3. Metrics collection: Track request counts, response times, and error rates for each backend.

  4. Sticky sessions: Ensure requests from the same client always go to the same backend.

  5. TLS support: Add HTTPS for secure connections.

  6. 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

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks