Em outubro de 2004, Igor Sysoev — engenheiro russo do site Rambler — lançou a versão inicial do NGINX ("engine X"). O motivo original: o servidor web Apache não escalava bem para picos de C10k (problema famoso de Dan Kegel, 1999, sobre como atender 10 mil conexões simultâneas). NGINX usava arquitetura event-driven com poucos processos, e em poucos anos virou a escolha dominante em sistemas web de alta carga. Em 2026, NGINX permanece relevante; HAProxy (Willy Tarreau, 2001) continua escolha forte; Envoy (Lyft, 2016) se firmou como referência moderna em service mesh.
Um load balancer é a peça que distribui requests entre instâncias de uma frota de serviços stateless. Sua função aparenta simples — "escolha uma máquina e mande para lá" —, mas o algoritmo da escolha, o ponto na pilha de rede onde atua, a verificação de saúde das instâncias, e o comportamento durante deploy ou falha definem qualidades operacionais que separam sistemas saudáveis de sistemas que parecem funcionar até pico de tráfego.
Este conceito articula em precisão. Os algoritmos mais usados (e onde cada um ganha), a distinção entre L4 (transport layer) e L7 (application layer), health checks ativos vs passivos, connection draining em deploy, e os anti-padrões clássicos. O conceito 04 (consistent hashing) destrincha o algoritmo especial usado em CDNs e caches; aqui o foco é a primitiva geral — load balancing entre réplicas de aplicação.
A relevância prática é universal. Mesmo sistema pequeno em Kubernetes tem load balancer no front (Service de tipo LoadBalancer), e quem articula a configuração corretamente (algoritmo, health check, timeout) entrega sistema bem comportado por anos. Quem trata como caixa preta acaba pagando em incidentes evitáveis.
Algoritmos — qual usar e por quê
Há cinco algoritmos clássicos, cada um com perfil próprio de uso. Conhecer cada um permite a escolha apropriada — ou identificar que o atual está errado.
Round-robin — o mais simples
Itera as instâncias em ordem fixa: request 1 vai para A, request 2 para B, request 3 para C, request 4 volta para A. Assume que todas as instâncias têm capacidade igual e requests têm custo igual. Implementação trivial; comportamento previsível em condições homogêneas.
Ganha quando: instâncias homogêneas (mesmo hardware), requests com custo similar (sem outliers de tempo ou recurso). Exemplo típico: API REST simples atrás de pool de réplicas idênticas.
Perde quando: requests heterogêneas (algumas pesadas, outras leves). Round-robin pode mandar 5 requests pesadas seguidas para a mesma instância sem perceber, deixando outras ociosas.
Least connections — sensível a carga atual
Mantém contagem de conexões ativas por instância; manda nova request para a com menor contagem. Adapta-se a heterogeneidade — instância processando request lenta acumula conexões, e o balancer naturalmente prefere outras.
Ganha quando: requests com duração variável; instâncias podem ficar sobrecarregadas em condições específicas. Exemplo típico: APIs com uploads ou downloads (alguns longos, outros curtos).
Perde quando: contagem de conexões não reflete carga real (HTTP/2 multiplex, gRPC streaming — uma conexão pode ter muitas operações ativas ou nenhuma).
Weighted (round-robin ou least-connections com pesos)
Cada instância tem peso proporcional à sua capacidade. Instância com peso 2 recebe duas vezes mais requests que a com peso 1. Útil para heterogeneidade explícita — frota com tipos diferentes de máquina, ou rolling update onde nova versão recebe peso menor enquanto está sendo monitorada.
Ganha quando: instâncias têm capacidades distintas conhecidas. Em deploys canary, peso baixo nas réplicas novas durante avaliação.
IP hash — sticky por hash do cliente
Hash do IP de origem decide instância. Mesmo cliente sempre vai para mesma instância. Útil para sticky sessions sem cookie, ou para afinidade em cache local.
Ganha quando: precisa de stickiness e prefere usar IP em vez de cookie (ex.: clientes não-HTTP). Perde quando: clientes atrás de NAT compartilhado (escritório, ISP) viram um único IP — todos vão para a mesma instância, criando hot spot. Em web moderno, raramente é a escolha certa.
Random with two choices ("power of two")
Algoritmo com base no paper "Balanced Allocations" de Azar, Broder, Karlin & Upfal (1994): escolha duas instâncias aleatoriamente, mande para a com menor contagem de conexões. Surpreendentemente, tem performance próxima de "least connections puro" sem precisar manter ordem global de contagens — mais escalável quando há muitas instâncias.
Ganha quando: pool de instâncias grande (centenas ou mais); precisa de balanceamento adaptativo sem overhead de contagem global. Adotado por Cloudflare em alguns serviços, e defaults modernos consideram-no para cenários específicos.
L4 vs L7 — onde o balancer atua
Load balancers operam em duas camadas distintas da pilha de rede, com capacidades fundamentalmente diferentes.
Layer 4 (transport)
L4 atua no nível TCP/UDP. Vê IP de origem, porta, protocolo — não vê o conteúdo HTTP. Decide instância no SYN inicial; depois disso, packets seguem para a instância escolhida transparentemente.
Ganhos: extremamente performático (não termina TLS, não faz parsing). Pouca CPU. Funciona com qualquer protocolo TCP (HTTP, gRPC, WebSocket, banco, queue).
Limitações: não pode rotear por URL, header, ou cookie. Não pode terminar TLS (cliente faz handshake direto com instância). Não tem visibilidade sobre HTTP — não pode aplicar rate limit por endpoint, não pode redirect, não sabe sobre 4xx vs 5xx no health check.
Implementações: AWS NLB, Linux IPVS, HAProxy mode TCP, Envoy TCP proxy. Para bancos, queues, e protocolos não-HTTP, L4 é tipicamente a escolha.
Layer 7 (application)
L7 atua no nível HTTP. Termina TCP/TLS no balancer,
faz parsing do request, vê headers, URL, cookies.
Pode rotear por Host, por path
(/api/v1/* para uma frota,
/api/v2/* para outra), por header
customizado, ou por valor de cookie.
Ganhos: roteamento sofisticado. WAF possível (filtragem de ataques L7). Métricas ricas (status code, path). Health check HTTP real (verifica 200 OK em endpoint).
Limitações: latência adicional (parse, TLS termination); custos maiores; gargalo potencial sob carga muito alta.
Implementações: AWS ALB, NGINX, HAProxy mode HTTP, Envoy, Traefik, Caddy. Para web e APIs HTTP, L7 é tipicamente a escolha.
Health checks — ativo vs passivo
Load balancer precisa saber quais instâncias estão saudáveis. Mandar request para instância morta gera erro; mandar para instância sobrecarregada degrada latência. Há duas formas de aferir.
Health check ativo: balancer faz
request periódica (a cada 5-30 segundos) a um
endpoint específico (/health,
/healthz). Se responde 200 OK em tempo
curto, instância é "saudável"; se falha múltiplas
vezes consecutivas, balancer remove do pool. Quando
passa a responder de novo, balancer adiciona de
volta.
Health check passivo: balancer observa o tráfego real. Se uma instância retorna muitos 5xx ou timeouts, balancer reduz tráfego ou remove temporariamente. Não exige endpoint dedicado.
Sistemas modernos usam ambos. Ativo para detectar crash ou hang completo (instância não responde nem ao health check); passivo para detectar degradação sutil (instância responde mas erra). Envoy expõe ambos como recursos primeira classe; AWS ALB tem apenas ativo (passivo via outras camadas).
Os três endpoints típicos
Sistemas Kubernetes-friendly tipicamente expõem três endpoints distintos:
/livez (liveness):
"estou vivo?". Se não responde, instância é
reiniciada (deadlock, hang). Endpoint mínimo, deve
sempre responder se o processo está rodando.
/readyz (readiness):
"estou pronto para receber tráfego?". Verifica
dependências críticas — banco conectado, cache
conectado, configuração carregada. Se falha,
instância é removida do pool mas não
reiniciada (talvez seja transitório).
/startupz (startup):
"terminei o startup?". Útil para aplicações com
warm-up longo. Diferente de readiness por permitir
timeout maior antes de declarar morta.
Kubernetes consome esses três como
livenessProbe, readinessProbe,
startupProbe. Confundir os três é
anti-padrão recorrente.
Connection draining — deploy sem perder requests
Quando uma instância vai descer (deploy de nova versão, scale down, manutenção), há requests em voo sendo processadas. Cortar imediatamente perde essas requests. Connection draining (ou "graceful shutdown") é o protocolo para finalizar sem perda.
O fluxo:
1. Balancer marca a instância como "draining" — não envia novas requests. Tipicamente disparado quando o orquestrador (Kubernetes, ECS) sinaliza shutdown.
2. Instância continua processando as requests em voo até completar.
3. Tipicamente há um
terminationGracePeriod (30-60s comum em
K8s); se as requests não completarem, são
forçadamente terminadas.
4. Quando todas terminam (ou o grace period expira), instância recebe SIGTERM/SIGKILL e desce.
Configurar isso corretamente exige coordenação entre app, balancer, e orquestrador. A app precisa escutar SIGTERM e parar de aceitar novas conexões; o balancer precisa observar que a instância está "draining" (em K8s, via mudança de readinessProbe antes do SIGTERM); o orquestrador precisa dar tempo adequado. Sem coordenação, deploy faz erros 5xx visíveis para usuários.
Latência adicional — o custo do balancer
Load balancer não é grátis. Cada request paga latência adicional (parse, decisão de roteamento, transmissão para instância). Em L4, tipicamente 50–500 µs; em L7 com TLS termination, 1–5 ms.
Para sistemas com SLO agressivo (P99 < 50 ms), essa latência merece consideração. Algumas estratégias:
Service mesh com sidecar. Em vez de balancer central, cada pod tem um sidecar (Envoy/Istio) que faz balancing local. Latência inter-pod ainda existe, mas o roteamento decide perto do consumidor.
L4 onde puder. Para tráfego entre microsserviços onde roteamento por path não importa, L4 reduz latência ao mínimo.
Direct server return (DSR). Cliente envia para balancer, mas resposta volta direto da instância para o cliente. Reduz latência em uma direção. Configuração técnica não-trivial; útil em tráfego streaming alto.
Os algoritmos em três stacks
Cada ecossistema tem seu balancer canônico em 2026. Vale conhecer a configuração mínima.
# em K8s, Service de tipo LoadBalancer expõe ALB
apiVersion: v1
kind: Service
metadata:
name: catalog-api
annotations:
# AWS ALB ingress controller
alb.ingress.kubernetes.io/load-balancer-name: catalog-api
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/healthcheck-path: /healthz
alb.ingress.kubernetes.io/healthcheck-interval-seconds: "10"
alb.ingress.kubernetes.io/healthy-threshold-count: "2"
alb.ingress.kubernetes.io/unhealthy-threshold-count: "3"
spec:
selector: { app: catalog-api }
ports:
- port: 443
targetPort: 8080
---
# na app — endpoints de health
app.MapGet("/livez", () => Results.Ok());
app.MapGet("/readyz", async (IDbContext db) => {
try { await db.HealthCheckAsync(); return Results.Ok(); }
catch { return Results.StatusCode(503); }
});
# graceful shutdown
app.Lifetime.ApplicationStopping.Register(() => {
// marca como não pronto para parar tráfego novo
// espera requests em voo concluírem
});
ASP.NET Core integra com K8s via probes nativos.
ALB faz round-robin por padrão; pode ser
configurado para least outstanding requests com
routing.http2.enabled.
# NGINX como L7 LB (configuração típica)
upstream catalog_api {
least_conn;
server app1:8000 weight=1 max_fails=3 fail_timeout=30s;
server app2:8000 weight=1 max_fails=3 fail_timeout=30s;
server app3:8000 weight=1 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
listen 443 ssl http2;
location / {
proxy_pass http://catalog_api;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
location /healthz {
proxy_pass http://catalog_api;
access_log off; # não polui logs
}
}
# em FastAPI
@app.get("/livez")
async def liveness(): return {"status": "ok"}
@app.get("/readyz")
async def readiness():
try:
await db.execute("SELECT 1")
return {"status": "ready"}
except Exception:
raise HTTPException(503)
# graceful shutdown via signal handler
import signal, asyncio
async def shutdown_event():
# marca readyz para falhar antes de parar
...
signal.signal(signal.SIGTERM, lambda *_: asyncio.create_task(shutdown_event()))
NGINX é stack típica em Python (junto com ALB se
em AWS). least_conn é boa escolha
padrão para FastAPI (request com latência
variável).
// Envoy config para roteamento L7 (sidecar pattern)
static_resources:
listeners:
- address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
route_config:
virtual_hosts:
- name: catalog
domains: ["*"]
routes:
- match: { prefix: "/api/v1/" }
route: { cluster: catalog_v1 }
- match: { prefix: "/api/v2/" }
route: { cluster: catalog_v2 }
clusters:
- name: catalog_v1
type: STRICT_DNS
lb_policy: LEAST_REQUEST
health_checks:
- timeout: 1s
interval: 5s
unhealthy_threshold: 2
healthy_threshold: 2
http_health_check:
path: "/readyz"
load_assignment:
cluster_name: catalog_v1
endpoints:
- lb_endpoints:
- endpoint: { address: { socket_address: { address: app, port_value: 8080 }}}
// na app Go
func main() {
srv := &http.Server{Addr: ":8080", Handler: router}
go func() { srv.ListenAndServe() }()
// graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// marca readyz para retornar 503 antes do shutdown
ready.Store(false)
// aguarda requests em voo
srv.Shutdown(ctx)
}
Envoy é o load balancer moderno em Go-heavy
stacks (Lyft, Stripe). LEAST_REQUEST
é equivalente a least connections. Configuração
rica permite roteamento por path, header,
stickiness, weights — tudo declarativo.
Anti-padrões frequentes
Health check que sempre retorna 200.
Endpoint /health que é
return Ok() sem verificar dependências.
Instância pode estar sem conexão de banco — health
check passa, balancer manda tráfego, requests falham.
Defesa: separar liveness (mínimo) de readiness
(verifica deps).
Health check com lookup pesado. O
oposto: /health que faz query a 5
tabelas, valida cache, etc. Em pico, health checks
contribuem para sobrecarga. Defesa: readiness deve
ser sentinela rápida (uma query simples,
ping ao Redis), não auditoria completa.
Sem connection draining. Deploy instala nova versão; antiga é morta com SIGKILL enquanto requests em voo. 5xx visíveis durante deploy. Defesa: lifecycle hooks corretos (preStop em K8s), readyz que muda antes do SIGTERM, grace period adequado.
Round-robin com requests heterogêneas. Sistema com mix de requests curtas e longas; round-robin manda 5 longas seguidas para mesma instância. Defesa: usar least connections ou similar quando há heterogeneidade conhecida.
Sticky sessions sem failover plan. Sticky garante mesmo cliente vai mesma instância; quando essa instância morre, balancer escolhe outra, e o estado local (sessão) é perdido. Sticky deveria vir com plano de fallback. Defesa preferida: não use sticky; externalize sessão.
Timeout do balancer maior que da instância. Instância tem timeout de 30s; balancer tem timeout de 60s. Em request lenta, instância cancela mas balancer ainda espera; usuário recebe 504 atrasado. Defesa: balancer timeout deve ser ligeiramente maior que app timeout (margem de ~10%).
Cascade failure causada por health check muito sensível. Cenário: balancer faz health check com threshold de 2 falhas consecutivas. Banco fica ligeiramente lento. Algumas health checks timeout. Balancer remove instâncias do pool. Tráfego concentra nas restantes. Restantes saturam sob carga concentrada. Mais health checks falham. Mais instâncias removidas. Sistema entra em cascade — todas removidas, todas tentando reconectar simultaneamente. Defesa: health check threshold mais tolerante (3-5 falhas, intervalo adequado); health check que não dependa do banco em readiness (apenas que app está respondendo); circuit breaker entre instância e dependência (não apenas no balancer).
Antes de aceitar configuração de load balancer em revisão, articule cinco perguntas. "Qual algoritmo e por quê?". "L4 ou L7 e por quê?". "Health checks — endpoint quais, intervalo qual, threshold qual?". "Connection draining — grace period suficiente para a request mais longa?". "Em deploy ou auto- scaling, o sistema mantém SLA?". As respostas determinam se a configuração vai sobreviver a produção. Quem aceita "tá bom assim, deixa o default" perde a oportunidade de prevenir cascade que vai aparecer em incidente meses depois.
Por que importa para a sua carreira
Load balancing é assunto recorrente em entrevistas de design. "Como você balancearia carga entre réplicas?" é convite para articular algoritmos, L4 vs L7, e health checks. A resposta forte cita least-connections vs round-robin, articula trade-off L7 (poder vs latência), e menciona connection draining em deploy. Em revisão de configuração de infra, perceber health check sem readiness próprio, ou ausência de connection draining, é serviço prestado ao time. Em pos-mortem de "deploy causou erros 5xx", diagnosticar como connection draining mal-configurado é trabalho de senior. Em sistemas Kubernetes, articular as três probes (liveness, readiness, startup) e suas distinções é vocabulário básico para quem faz operação séria.
Como praticar
- Stack local com NGINX. Suba três instâncias da sua aplicação localmente (docker compose). Configure NGINX como L7 LB com round-robin, depois com least_conn. Use ferramenta de carga (k6) para enviar requests com tempos variáveis. Compare distribuição entre instâncias entre os dois algoritmos. Esse exercício torna concreta a diferença.
- Connection draining experimento. Configure deploy local com lifecycle preStop e readiness probe. Simule deploy: durante carga sustentada, derrube uma instância. Observe se há erros 5xx ou requests perdidas. Ajuste grace period e probe até zero erros. Esse setup, feito uma vez, vira template para projetos futuros.
- Anatomia de Envoy config. Pegue o exemplo de Envoy do lang-compare e adapte para um sistema seu. Configure roteamento por path (versionamento de API), health check com readiness, e stickiness por header customizado. Esse é exercício de consolidação que vale para qualquer service mesh moderno.
Referências para aprofundar
- artigo The C10K Problem — Dan Kegel (1999, atualizado).
- paper Balanced Allocations — Yossi Azar, Andrei Broder, Anna Karlin, Eli Upfal (STOC, 1994).
- livro Designing Data-Intensive Applications — Martin Kleppmann (O'Reilly, 2017).
- livro Kubernetes Patterns — Bilgin Ibryam, Roland Huß (O'Reilly, 2019).
- livro Site Reliability Engineering — Beyer et al., Google (O'Reilly, 2016).
- livro NGINX Cookbook — Derek DeJonghe (O'Reilly, 2019).
- docs NGINX Load Balancing.
- docs HAProxy Configuration Manual.
- docs Envoy Documentation.
- docs Kubernetes Probes.
- artigo The Power of Two Random Choices — Tim Roughgarden (Stanford notes, 2018).
- vídeo Envoy Proxy at Lyft — Matt Klein (várias palestras 2017–2020).