MÓDULO 07 · CONCEITO 03 DE 12

Load balancing

Algoritmos (round-robin, least connections, weighted, random com 2 escolhas), L4 vs L7, health checks, connection draining. Da NGINX clássica à Envoy moderna — a peça que torna scale out viável e que costuma ser tratada como "caixa preta" quando deveria ser articulada.

Tempo de leitura ~22 min Pré-requisito Conceito 02 (stateless services) Próximo Consistent hashing

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.

C# — Kubernetes Service + ALB
# 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.

Python — Uvicorn + NGINX/ALB
# 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).

Go — Envoy (service mesh) ou ALB
// 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%).

armadilha em produção

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).

heurística do sênior

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

  1. 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.
  2. 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.
  3. 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

  1. artigo The C10K Problem — Dan Kegel (1999, atualizado). kegel.com/c10k.html — O artigo fundador que motivou a arquitetura event-driven (NGINX, Node, libuv). Histórico mas relevante.
  2. paper Balanced Allocations — Yossi Azar, Andrei Broder, Anna Karlin, Eli Upfal (STOC, 1994). A prova original do "power of two choices". Muito citado em literatura de load balancing.
  3. livro Designing Data-Intensive Applications — Martin Kleppmann (O'Reilly, 2017). Cap. 6 cobre roteamento de requests entre nós, incluindo load balancing em sistemas distribuídos.
  4. livro Kubernetes Patterns — Bilgin Ibryam, Roland Huß (O'Reilly, 2019). Cap. sobre Health Probe, Predictable Demands, e Service Discovery cobrem load balancing em K8s. Conexão direta com a prática moderna.
  5. livro Site Reliability Engineering — Beyer et al., Google (O'Reilly, 2016). Cap. 19 (Load Balancing at the Frontend) cobre a perspectiva do Google sobre L4, L7, e edge balancing. Cap. 20 cobre balancing no datacenter.
  6. livro NGINX Cookbook — Derek DeJonghe (O'Reilly, 2019). Receitas práticas para configuração NGINX. Cobre balancing, health, SSL, caching com exemplos imediatamente úteis.
  7. docs NGINX Load Balancing. nginx.org/en/docs/http/load_balancing.html — Documentação oficial. Curta, clara, atualizada. Cobre todos os algoritmos com sintaxe.
  8. docs HAProxy Configuration Manual. cbonte.github.io/haproxy-dconv — A documentação canônica do HAProxy. Densa mas completa.
  9. docs Envoy Documentation. envoyproxy.io/docs — Documentação canônica do Envoy. Modelo de configuração rico, conceitos de cluster/listener/filter.
  10. docs Kubernetes Probes. kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes — Documentação oficial sobre as três probes e quando usar cada uma.
  11. artigo The Power of Two Random Choices — Tim Roughgarden (Stanford notes, 2018). Análise didática do algoritmo. Útil para entender por que ele funciona surpreendentemente bem.
  12. vídeo Envoy Proxy at Lyft — Matt Klein (várias palestras 2017–2020). YouTube. Klein é criador do Envoy. As palestras cobrem motivação, design, e padrões de uso. Indispensável para entender service mesh moderno.