14,587 lecturas
14,587 lecturas

Este guión de 150 líneas es realmente un balanceador de carga completo

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

Demasiado Largo; Para Leer

Este artículo te mostrará cómo crear un balanceador de carga HTTP simple en Go, utilizando sólo la biblioteca estándar.Este programa realiza la distribución de rotonda a través de servidores backend, la verificación de la salud para detectar fallos y la solicitud de proxy, todo en alrededor de 150 líneas de código.
featured image - Este guión de 150 líneas es realmente un balanceador de carga completo
Rez Moss HackerNoon profile picture

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:

  1. El backend struct representa un servidor backend único con su URL y su estado vivo.
  2. Estamos utilizando un mutex para actualizar de forma segura y comprobar el estado vivo de cada backend en un entorno simultáneo.
  3. El LoadBalancer mantiene un seguimiento de una lista de backends y mantiene un contador para el algoritmo de round-robin.
  4. Utilizamos operaciones atómicas para incrementar de forma segura el contador en un entorno simultáneo.
  5. 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:

  1. Intentamos iniciar una conexión TCP con el backend.
  2. Si esta conexión tiene éxito, entonces el backend está vivo; de lo contrario está muerto.
  3. 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:

  1. Nuestro algoritmo de round-robin nos da el siguiente backend disponible.
  2. En el caso de que no hay backend disponibles, devolveremos un error 503 Service Unavailable.
  3. De lo contrario, proxy la solicitud al backend seleccionado a través del proxy interno de Go.


Observa cómo lanet/http/httputilEl paquete proporciona elReverseProxytipo, que maneja todas las complejidades de las solicitudes de proxy HTTP para nosotros.

Poniéndolo todo juntos

Por último, vamos a implementar elmainFunció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:

  1. Configuramos el puerto basado en las banderas de la línea de comandos que analizamos.
  2. Hemos creado una lista de servidores de backend.
  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. Realizamos una revisión sanitaria inicial.
  5. Iniciamos una goroutina para los controles de salud periódicos.
  6. 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:


  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.


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

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks