Si está ejecutando servicios que deberían ser capaces de manejar una gran cantidad de tráfico, puede cargar mucho de ese tráfico entre sus servidores de backend. Hay muchos balanceadores de carga de nivel de producción en el mercado (NGINX, HAProxy, etc.) pero saber cómo funcionan detrás de la escena es un buen conocimiento.
A Simple HTTP Load Balancer in Go Using Standard Library, En esta implementación, usaremos un algoritmo de rotonda para distribuir uniformemente las solicitudes entrantes entre una colección de servidores de backend.
La estructura básica
Primero, necesitamos definir nuestras estructuras de datos básicas.Nuestro balanceador de carga rastreará múltiples servidores backend y su salud:
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
}
Ahora hay algunos puntos clave a tener en cuenta:
- El backend struct representa un servidor backend único con su URL y su estado vivo.
- Estamos utilizando un mutex para actualizar de forma segura y comprobar el estado vivo de cada backend en un entorno simultáneo.
- El LoadBalancer mantiene un seguimiento de una lista de backends y mantiene un contador para el algoritmo de round-robin.
- Utilizamos operaciones atómicas para incrementar de forma segura el contador en un entorno simultáneo.
- El método NextBackend implementa el algoritmo de round-robin, salpicando los backends poco saludables.
Control de salud
Detectar la indisponibilidad del backend es una de las funciones críticas de cualquier balanceador de carga. Implementa un mecanismo de comprobación de salud muy simple:
// 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()
}
}
}
Este control de salud es sencillo:
- Intentamos iniciar una conexión TCP con el backend.
- Si esta conexión tiene éxito, entonces el backend está vivo; de lo contrario está muerto.
- La verificación se ejecuta en un intervalo especificado en la función HealthCheckPeriodically.
En un entorno de producción, probablemente te gustaría un control de salud más avanzado que realice una solicitud HTTP a algún punto final especificado, pero esto nos lleva a empezar.
El manejo de HTTP
Vamos a seguir adelante e implementar el manipulador HTTP que recibirá la solicitud y la redirigirá a nuestros backend:
// 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)
}
Y aquí es donde ocurre la magia:
- Nuestro algoritmo de round-robin nos da el siguiente backend disponible.
- En el caso de que no hay backend disponibles, devolveremos un error 503 Service Unavailable.
- De lo contrario, proxy la solicitud al backend seleccionado a través del proxy interno de Go.
Observa cómo lanet/http/httputil
El paquete proporciona elReverseProxy
tipo, que maneja todas las complejidades de las solicitudes de proxy HTTP para nosotros.
Poniéndolo todo juntos
Por último, vamos a implementar elmain
Función para configurar y iniciar nuestro balanceador de carga:
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)
}
}
Ahora, aquí está lo que está sucediendo en la función principal:
- Configuramos el puerto basado en las banderas de la línea de comandos que analizamos.
- Hemos creado una lista de servidores de 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
- Realizamos una revisión sanitaria inicial.
- Iniciamos una goroutina para los controles de salud periódicos.
- Por último, iniciamos el servidor HTTP con nuestro balanceador de carga como manipulador.
Prueba del balanceador de carga
Así que, una vez más, para probar nuestro balanceador de carga, necesitamos algunos servidores de 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)
}
}
Execute varias instancias de este backend en diferentes puertos:
go build -o backend backend.go
./backend -port 8081 &
./backend -port 8082 &
./backend -port 8083 &
Después, construye y ejecute el balanceador de carga:
go build -o load-balancer main.go
./load-balancer
Ahora, haga algunas solicitudes para probarlo:
curl http://localhost:8080/test
Haga múltiples solicitudes y verá que se distribuyen a través de sus servidores de backend de forma redonda.
Potenciales mejoras
Esta es una implementación mínima, pero hay muchas cosas que puedes hacer para mejorarla:
-
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.
Hemos construido un balanceador de carga HTTP simple pero eficaz, utilizando nada más que la biblioteca estándar de Go. Este ejemplo ilustra conceptos importantes en la programación de red y el poder de las características de red integradas de Go.
Aunque esto no está listo para la producción, proporciona una base sólida para comprender cómo funcionan los equilibradores de carga.La solución completa es de alrededor de 150 líneas de código - una prueba de la expresividad de Go y la fuerza de su biblioteca estándar.
Para el uso de la producción, usted desearía desarrollar más características y agregar un manejo de errores robusto, pero los conceptos básicos son todos los mismos.Una comprensión de estos principios básicos le preparará para configurar y deshabilitar mejor cualquier balanceador de carga que utilice en el camino.
Puedes encontrar el código fuente aquíhttps://github.com/rezmoss/simple-load-balancer