Load balancing client-side e server-side
1. Fundamentos do Balanceamento de Carga
1.1. Conceitos essenciais
Load balancing é a técnica de distribuir requisições entre múltiplos servidores para otimizar o uso de recursos, maximizar throughput, reduzir latência e garantir alta disponibilidade. Em sistemas distribuídos, é um componente crítico para evitar que um único nó se torne gargalo ou ponto único de falha.
1.2. Diferenças fundamentais
- Server-side: O balanceamento ocorre no servidor ou em um proxy intermediário. O cliente desconhece os backends individuais. Exemplos: Nginx, HAProxy, AWS ELB.
- Client-side: O cliente possui conhecimento dos backends e decide para qual enviar a requisição. Comum em arquiteturas de microserviços com service discovery.
1.3. Algoritmos clássicos
- Round-robin: Distribui requisições sequencialmente entre backends.
- Least connections: Envia para o backend com menos conexões ativas.
- Hash-based: Usa hash de IP, URL ou chave para garantir que a mesma requisição vá para o mesmo backend.
2. Load Balancing Server-Side com Golang
2.1. Proxy reverso com httputil.ReverseProxy
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func main() {
backendURL, _ := url.Parse("http://localhost:8081")
proxy := httputil.NewSingleHostReverseProxy(backendURL)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r)
})
log.Println("Servidor proxy rodando em :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
2.2. Round-robin ponderado com health checks
type Backend struct {
URL *url.URL
Weight int
Alive bool
}
type LoadBalancer struct {
backends []*Backend
current int
mu sync.Mutex
}
func (lb *LoadBalancer) Next() *Backend {
lb.mu.Lock()
defer lb.mu.Unlock()
for i := 0; i < len(lb.backends); i++ {
idx := lb.current % len(lb.backends)
lb.current++
if lb.backends[idx].Alive {
return lb.backends[idx]
}
}
return nil
}
func (lb *LoadBalancer) HealthCheck() {
for _, b := range lb.backends {
resp, err := http.Get(b.URL.String() + "/health")
b.Alive = err == nil && resp.StatusCode == http.StatusOK
if resp != nil {
resp.Body.Close()
}
}
}
2.3. Integração com service discovery (Consul)
type ConsulWatcher struct {
client *api.Client
backends []*Backend
}
func (cw *ConsulWatcher) Watch(serviceName string) {
for {
services, _, err := cw.client.Health().Service(serviceName, "", true, nil)
if err == nil {
cw.updateBackends(services)
}
time.Sleep(10 * time.Second)
}
}
func (cw *ConsulWatcher) updateBackends(services []*api.ServiceEntry) {
cw.backends = nil
for _, s := range services {
url, _ := url.Parse(fmt.Sprintf("http://%s:%d", s.Service.Address, s.Service.Port))
cw.backends = append(cw.backends, &Backend{URL: url, Alive: true})
}
}
3. Load Balancing Client-Side com Golang
3.1. Pool de conexões customizado
type ClientPool struct {
backends []string
mu sync.Mutex
client *http.Client
}
func NewClientPool(backends []string) *ClientPool {
return &ClientPool{
backends: backends,
client: &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
},
Timeout: 5 * time.Second,
},
}
}
func (cp *ClientPool) Do(req *http.Request) (*http.Response, error) {
cp.mu.Lock()
backend := cp.backends[rand.Intn(len(cp.backends))]
cp.mu.Unlock()
req.URL.Host = backend
return cp.client.Do(req)
}
3.2. gRPC com balanceamento integrado
import (
"google.golang.org/grpc"
"google.golang.org/grpc/balancer/roundrobin"
"google.golang.org/grpc/resolver"
)
func createGRPCClient() (*grpc.ClientConn, error) {
resolver.SetDefaultScheme("dns")
conn, err := grpc.Dial(
"dns:///service-discovery:8500",
grpc.WithInsecure(),
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`),
)
if err != nil {
return nil, err
}
return conn, nil
}
3.3. Estratégias de retry e fallback
type RetryClient struct {
backends []string
maxRetries int
}
func (rc *RetryClient) ExecuteWithRetry(req *http.Request) (*http.Response, error) {
var lastErr error
for i := 0; i < rc.maxRetries; i++ {
backend := rc.backends[i%len(rc.backends)]
req.URL.Host = backend
resp, err := http.DefaultClient.Do(req)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
if resp != nil {
resp.Body.Close()
}
lastErr = err
time.Sleep(time.Duration(math.Pow(2, float64(i))) * 100 * time.Millisecond)
}
return nil, fmt.Errorf("all retries failed: %w", lastErr)
}
4. Algoritmos Avançados e Personalização
4.1. Least connections
type LeastConnections struct {
backends []*BackendStats
mu sync.Mutex
}
type BackendStats struct {
URL string
Connections int32
}
func (lc *LeastConnections) Next() string {
lc.mu.Lock()
defer lc.mu.Unlock()
minConn := int32(math.MaxInt32)
var selected string
for _, b := range lc.backends {
if b.Connections < minConn {
minConn = b.Connections
selected = b.URL
}
}
return selected
}
4.2. Hash consistente
type ConsistentHash struct {
ring map[uint32]string
sortedKeys []uint32
replicas int
}
func (ch *ConsistentHash) Add(server string) {
for i := 0; i < ch.replicas; i++ {
hash := fnv.New32a()
hash.Write([]byte(fmt.Sprintf("%s-%d", server, i)))
key := hash.Sum32()
ch.ring[key] = server
ch.sortedKeys = append(ch.sortedKeys, key)
}
sort.Slice(ch.sortedKeys, func(i, j int) bool {
return ch.sortedKeys[i] < ch.sortedKeys[j]
})
}
func (ch *ConsistentHash) Get(key string) string {
hash := fnv.New32a()
hash.Write([]byte(key))
h := hash.Sum32()
idx := sort.Search(len(ch.sortedKeys), func(i int) bool {
return ch.sortedKeys[i] >= h
})
if idx == len(ch.sortedKeys) {
idx = 0
}
return ch.ring[ch.sortedKeys[idx]]
}
4.3. Selector customizado para gRPC
type CustomBalancer struct {
backends []resolver.Address
mu sync.Mutex
}
func (cb *CustomBalancer) Build(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer {
return &customPicker{
cc: cc,
}
}
type customPicker struct {
cc balancer.ClientConn
current int
}
func (cp *customPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
cp.current = (cp.current + 1) % len(cp.cc.GetState().Picker)
// Implementação do picker personalizado
return balancer.PickResult{}, nil
}
5. Health Checks e Circuit Breaker
5.1. Health checks ativos e passivos
type HealthChecker struct {
backends map[string]*BackendHealth
mu sync.RWMutex
}
type BackendHealth struct {
failures int
lastCheck time.Time
}
func (hc *HealthChecker) PassiveCheck(backend string, success bool) {
hc.mu.Lock()
defer hc.mu.Unlock()
if !success {
hc.backends[backend].failures++
if hc.backends[backend].failures > 5 {
delete(hc.backends, backend) // Remove temporariamente
}
} else {
hc.backends[backend].failures = 0
}
}
5.2. Circuit breaker com gobreaker
import "github.com/sony/gobreaker"
func createCircuitBreaker() *gobreaker.CircuitBreaker {
return gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "backend-service",
MaxRequests: 3,
Interval: 60 * time.Second,
Timeout: 10 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 3 && failureRatio >= 0.6
},
})
}
6. Observabilidade e Métricas
6.1. Métricas com Prometheus
var (
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "request_duration_seconds",
Help: "Duration of requests by backend",
Buckets: prometheus.DefBuckets,
},
[]string{"backend"},
)
requestErrors = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "request_errors_total",
Help: "Total errors by backend",
},
[]string{"backend", "error_type"},
)
)
func init() {
prometheus.MustRegister(requestDuration, requestErrors)
}
6.2. Tracing com OpenTelemetry
import "go.opentelemetry.io/otel"
func tracedHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, span := otel.Tracer("load-balancer").Start(r.Context(), "handle-request")
defer span.End()
span.SetAttributes(attribute.String("backend", r.URL.Host))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
7. Comparação Prática e Boas Práticas
7.1. Quando escolher cada abordagem
| Cenário | Client-Side | Server-Side |
|---|---|---|
| Microserviços com service discovery | ✅ | ❌ |
| Gateways de API | ❌ | ✅ |
| Latência ultra-baixa | ✅ | ❌ |
| Segurança e abstração | ❌ | ✅ |
7.2. Armadilhas comuns
- Afinidade de sessão: Usar hash consistente para manter sessões no mesmo backend
- Conexões ociosas: Configurar
IdleConnTimeoutnohttp.Transport - Timeouts: Sempre definir
Timeoutno cliente HTTP eReadTimeout/WriteTimeoutno servidor
7.3. Checklist para produção
// Configuração recomendada para produção
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DisableKeepAlives: false,
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
Referências
- Documentação oficial do net/http/httputil - Go — Documentação completa do pacote httputil com ReverseProxy e funcionalidades de proxy reverso
- gRPC Load Balancing in Go - gRPC.io — Guia oficial sobre balanceamento de carga client-side com gRPC em Go
- Sony gobreaker - GitHub — Implementação do padrão Circuit Breaker em Go, utilizada em sistemas de alta disponibilidade
- OpenTelemetry Go Documentation — Documentação oficial para instrumentação de tracing distribuído em aplicações Go
- Consul Service Discovery - HashiCorp — Documentação do Consul para descoberta de serviços, essencial para balanceamento dinâmico
- Prometheus Go Client — Guia para exportação de métricas de aplicações Go usando a biblioteca oficial do Prometheus
- Consistent Hashing in Go - Research Paper — Artigo técnico sobre implementação de hash consistente, base para algoritmos de balanceamento avançados