Se você estiver executando serviços que devem ser capazes de lidar com uma grande quantidade de tráfego, você pode carregar muito desse tráfego entre seus servidores de backend.Existem muitos balançadores de carga de nível de produção no mercado (NGINX, HAProxy, etc.) mas saber como eles funcionam por trás da cena é um bom conhecimento.
A Simple HTTP Load Balancer in Go Using Standard Library, Nesta implementação, usaremos um algoritmo rotativo para distribuir uniformemente as solicitações de entrada entre uma coleção de servidores de backend.
A estrutura básica
Primeiro, precisamos definir nossas estruturas de dados principais. Nosso balançador de carga rastreará vários servidores backend e sua saúde:
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
}
Agora, alguns pontos-chave a ter em mente:
- O backend struct representa um único servidor backend com seu URL e status vivo.
- Estamos usando um mutex para atualizar com segurança e verificar o status ao vivo de cada backend em um ambiente simultâneo.
- O LoadBalancer mantém um registro de uma lista de back-ends e mantém um contador para o algoritmo de round-robin.
- Usamos operações atômicas para incrementar com segurança o contador em um ambiente concorrente.
- O método NextBackend implementa o algoritmo de round-robin, ignorando os backends não saudáveis.
Verificação de saúde
Detectar a indisponibilidade do backend é uma das funções críticas de qualquer balançador de carga. Implementa um mecanismo de verificação de saúde muito simples:
// 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 controle de saúde é simples:
- Nós tentamos iniciar uma conexão TCP com o backend.
- Se essa conexão for bem-sucedida, então o backend está vivo; caso contrário, está morto.
- A verificação é executada em um intervalo especificado na função HealthCheckPeriodically.
Em um ambiente de produção, você provavelmente gostaria de uma verificação de saúde mais avançada que realmente faça uma solicitação HTTP para algum endpoint especificado, mas isso nos dá início.
O HTTP Handler
Vamos seguir em frente e implementar o manipulador HTTP que receberá a solicitação e os encaminhará para nossos 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)
}
E é aí que a magia acontece:
- Nosso algoritmo de round-robin nos dá o próximo backend disponível.
- No caso de nenhum backend estar disponível, devolvemos um erro 503 Serviço indisponível.
- Caso contrário, nós proxy a solicitação para o backend selecionado através do proxy reverso interno do Go.
Observe como onet/http/httputil
O pacote fornece oReverseProxy
tipo, que lida com todas as complexidades da proxy de solicitações HTTP para nós.
Colocando tudo em conjunto
Por fim, vamos implementar omain
Função para configurar e iniciar o nosso balançador 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)
}
}
Aqui está o que está acontecendo na função principal:
- Nós configuramos a porta com base nas bandeiras da linha de comando que parsamos.
- Criamos uma lista de servidores 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
- Realizamos um primeiro check-up de saúde.
- Iniciamos uma goroutina para exames de saúde periódicos.
- Finalmente, iniciamos o servidor HTTP com o nosso balanceador de carga como manipulador.
Testando o Balanço de Carga
Então, novamente, para testar nosso balanceador de carga, precisamos de alguns servidores 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 várias instâncias deste backend em diferentes portas:
go build -o backend backend.go
./backend -port 8081 &
./backend -port 8082 &
./backend -port 8083 &
Em seguida, crie e execute o balançador de carga:
go build -o load-balancer main.go
./load-balancer
Agora, faça alguns pedidos para testá-lo:
curl http://localhost:8080/test
Faça múltiplos pedidos e você os verá sendo distribuídos em seus servidores de backend de forma rotativa.
Possíveis melhorias
Esta é uma implementação mínima, mas há muitas coisas que você pode fazer para melhorá-la:
-
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.
Construímos um balanceador de carga HTTP simples, mas eficaz, usando apenas a biblioteca padrão do Go. Este exemplo ilustra conceitos importantes na programação de rede e o poder dos recursos de rede integrados do Go.
Embora esta não esteja pronta para a produção, fornece uma base sólida para entender como os balanceadores de carga funcionam.A solução completa é de cerca de 150 linhas de código - um testemunho da expressividade do Go e da força de sua biblioteca padrão.
Para uso de produção, você gostaria de construir mais recursos e adicionar gerenciamento de erros robusto, mas os conceitos principais são todos os mesmos.Uma compreensão desses princípios básicos irá prepará-lo para configurar e depurar melhor qualquer balançador de carga que você usará na estrada.
Você pode encontrar o código fonte aquihttps://github.com/rezmoss/simple-load-balancer