Joao Brietzke Blog

← voltar

Load balancer: o que é e como a lógica dele funciona por dentro

Um único servidor tem um teto. Não é um teto de configuração, é físico: um número finito de núcleos, de memória e de conexões TCP abertas ao mesmo tempo. Quando o tráfego passa desse ponto, a resposta óbvia é colocar mais máquinas. E aí aparece um problema que não existia com uma máquina só: o cliente digita um endereço, e esse endereço precisa apontar para alguma coisa. Para qual das máquinas?

O load balancer é a resposta a essa pergunta. Por fora ele parece simples, um distribuidor de requisições. Mas é na lógica que decide para onde cada requisição vai que mora quase tudo que importa.


O problema que ele resolve

Existem dois caminhos para aguentar mais carga. O vertical troca a máquina por uma maior: simples, mas tem teto físico e continua sendo um ponto único de falha. O horizontal coloca várias máquinas em paralelo: não tem teto teórico e tolera falhas, mas exige que alguém, na frente, decida quem atende cada requisição.

                     ┌────────────────┐
  cliente ──────────▶│ load balancer  │
                     └────────┬───────┘
                              │  escolhe um alvo
                ┌─────────────┼─────────────┐
                ▼             ▼             ▼
            servidor A    servidor B    servidor C

Esse intermediário entrega três coisas ao mesmo tempo, e vale separar as três porque elas costumam ser tratadas como uma só:

  • Distribuição: nenhum servidor afoga enquanto outro fica ocioso.
  • Disponibilidade: um servidor morto sai de rotação sozinho, sem intervenção humana.
  • Abstração: o cliente conhece um endereço estável, enquanto você adiciona, remove e troca máquinas atrás dele sem que ninguém perceba.

A terceira é a mais subestimada. É ela que torna deploy sem downtime, blue/green e canary possíveis, porque desacopla "o endereço que o mundo conhece" de "as máquinas que existem agora".

O caminho de uma requisição

O fluxo completo, do lado de fora para dentro:

  1. O cliente resolve app.exemplo.com no DNS e recebe o IP do load balancer.
  2. O cliente abre uma conexão TCP com o load balancer e envia a requisição.
  3. O load balancer consulta sua lista de alvos, filtrada apenas pelos que estão marcados como saudáveis naquele instante.
  4. Ele aplica um algoritmo de seleção para escolher um deles.
  5. Encaminha a requisição, recebe a resposta e devolve ao cliente.
  6. Registra métricas: latência, código de status, conexões abertas, bytes trafegados.

Os passos 3 e 4 são a lógica propriamente dita. O resto é encanamento. Vamos abrir os dois.

A lista de alvos e os health checks

O load balancer não confia cegamente na configuração dele. Ele sonda cada alvo continuamente, num intervalo fixo, tipicamente algo como:

GET /health HTTP/1.1
Host: 10.0.1.42

espera: HTTP 200 em até 5 segundos, a cada 30 segundos

Cada alvo tem uma máquina de estados própria:

Estado Como se entra O que acontece
healthy N sondagens bem-sucedidas seguidas Recebe tráfego normalmente
unhealthy N sondagens falhas seguidas Sai de rotação imediatamente
draining Remoção deliberada (deploy, scale in) Para de receber conexões novas, termina as abertas

O draining é a parte que passa despercebida e resolve um problema real: ao remover uma instância, cortar as conexões em andamento derruba requisições de usuários que já estavam sendo atendidos. O período de drenagem existe para que a instância termine o que começou antes de morrer.

Duas decisões de configuração que definem se o health check ajuda ou atrapalha:

Limiares consecutivos, nunca falha única. Uma sondagem falha pode ser um soluço de rede. Exigir duas ou três falhas seguidas evita o flapping, o servidor entrando e saindo de rotação a cada minuto. O outro extremo também custa caro: um limiar folgado demais mantém tráfego indo para um buraco negro por mais tempo do que deveria.

Profundidade certa no endpoint. Um /health que só responde 200 sem verificar nada mente: a instância pode estar de pé sem conseguir falar com o banco. Um /health que verifica todas as dependências mente ao contrário: se todas as instâncias compartilham o mesmo banco e o banco engasga, todos os checks falham juntos e o load balancer tira a frota inteira de rotação, transformando uma degradação em queda total. O equilíbrio prático é verificar o que é local à instância e tratar dependências compartilhadas com timeout curto e tolerância.

Os algoritmos de seleção

Aqui está o coração da lógica. Cada algoritmo assume uma coisa diferente sobre o seu tráfego, e o algoritmo certo é o que assume o que é verdade no seu caso.

Round robin. Alterna em ordem: A, B, C, A, B, C. Não guarda estado nenhum além do último índice usado. É justo só quando duas condições valem ao mesmo tempo: todas as requisições custam mais ou menos o mesmo, e todos os servidores têm a mesma capacidade. Na prática raramente as duas valem.

Weighted round robin. Mesma ideia, com pesos. O servidor A tem o dobro de vCPU, então recebe peso 2 enquanto B e C ficam com 1. Também é o mecanismo por trás de deploy canário: peso 19 na versão antiga, peso 1 na nova, e você observa as métricas com 5% do tráfego real antes de comprometer o resto.

Least connections. Escolhe o alvo com menos conexões abertas no momento. Esse é adaptativo, e a diferença é grande: se o servidor B travou numa consulta lenta, a contagem de conexões dele fica alta e ele naturalmente para de receber trabalho novo, sem que ninguém precise detectar nada. Sempre que a duração das requisições varia, e ela quase sempre varia, esse algoritmo ganha do round robin.

Least outstanding requests. É o refinamento que o ALB da AWS usa. Conta requisições em voo, não conexões. A distinção importa em HTTP/2 e gRPC, onde uma única conexão carrega dezenas de requisições simultâneas e contar conexões deixa de dizer qualquer coisa sobre carga.

Least response time. Combina conexões ativas com a latência observada. Reage ao servidor que está tecnicamente vivo, passa no health check, mas está degradado e respondendo em 800ms em vez de 40ms.

Hash. Calcula um hash de algo estável na requisição e mapeia para um alvo:

alvo = hash(ip_origem, porta_origem, ip_destino, porta_destino) % qtd_alvos

O mesmo fluxo cai sempre no mesmo servidor. É assim que o NLB funciona, e é isso que torna o balanceamento de camada 4 tão barato: não há estado por requisição para manter, é aritmética. A fraqueza clássica está no % qtd_alvos, porque mudar o número de alvos remapeia praticamente todos os fluxos de uma vez. Por isso sistemas sérios usam consistent hashing, onde alvos e chaves são posicionados num anel e adicionar um servidor move apenas 1/N das chaves em vez de todas.

Random with two choices. Sorteia dois alvos e manda para o menos carregado dos dois. Parece grosseiro, mas o resultado chega muito perto do least connections verdadeiro sem precisar manter uma visão global do estado da frota, o que importa quando o balanceamento é feito por muitos proxies distribuídos que não se falam.

Sticky sessions, e por que elas brigam com o objetivo

Se a sua aplicação guarda sessão na memória do servidor, a requisição 2 precisa chegar no mesmo servidor da requisição 1. O load balancer resolve isso injetando um cookie:

Set-Cookie: AWSALB=<alvo codificado>; Expires=Wed, 26 Aug 2026 12:00:00 GMT

Na requisição seguinte ele lê o cookie e roteia direto, ignorando o algoritmo.

Funciona, mas repare no que aconteceu: você desligou justamente a lógica que fazia o load balancer valer a pena. O servidor que morre leva as sessões dos usuários grudados nele. A carga fica desigual porque usuários se acumulam onde caíram. Deploy vira evento disruptivo. Auto scaling perde eficácia, já que as instâncias novas só recebem usuários novos.

A saída quase sempre é a mesma: tornar a aplicação stateless e mover a sessão para Redis, DynamoDB ou um token assinado, deixando o load balancer livre para balancear de verdade. Sticky session é remédio para uma decisão de arquitetura da aplicação, não um recurso para se buscar de propósito.

Camada 4 e camada 7, mecanicamente

Essa é a divisão que produz as duas famílias de load balancer, e ela fica muito mais clara olhando o que acontece com os pacotes. Se as camadas ainda não estão frescas, elas estão em Modelo OSI e TCP/IP.

Camada 4 opera sobre TCP e UDP. Ele enxerga endereços IP e portas, e nada além disso. Decide uma única vez, quando a conexão é aberta, e todo pacote daquele fluxo segue o mesmo caminho. O payload é um bloco opaco de bytes que ele nunca precisa decifrar, então TLS passa direto sem ser terminado. A implementação chega a ser pouco mais do que reescrever cabeçalhos e encaminhar, e é por isso que ele alcança milhões de conexões por segundo com latência na casa dos microssegundos.

Camada 7 termina a conexão. Ele completa o handshake TCP com o cliente, decifra o TLS, faz o parse da requisição HTTP e só então abre uma conexão própria com o backend. São duas conexões independentes, e o load balancer é um proxy completo no meio.

É essa terminação que destrava tudo:

se path começa com /api/        → target group: api-servers
se host = admin.exemplo.com     → target group: admin-servers
se header X-Beta = true         → target group: canary
caso contrário                  → target group: web-servers

Como ele reconstrói a requisição, também pode reescrever cabeçalhos, injetar o X-Forwarded-For com o IP real do cliente (que se perdeu no momento em que o load balancer passou a ser a origem da conexão com o backend), exigir autenticação antes de encaminhar, responder um redirect sem tocar em servidor nenhum, e reaproveitar conexões com o backend entre clientes diferentes.

O preço é CPU e latência. Decifrar e interpretar cada requisição é trabalho real, medido em milissegundos em vez de microssegundos.

Camada 4 Camada 7
O que enxerga IP e porta Requisição inteira
Quando decide Uma vez, na abertura da conexão A cada requisição
TLS Pode passar sem decifrar Termina e decifra
IP do cliente Preservado Vai para o X-Forwarded-For
Latência Microssegundos Milissegundos
Na AWS NLB ALB

O DNS também é load balancing

Vale fechar com uma camada que fica acima de tudo isso. Devolver IPs diferentes para o mesmo nome já distribui tráfego antes mesmo de existir uma conexão. É o que o Route 53 faz com roteamento por latência, por geolocalização ou por pesos, e é assim que se balanceia entre regiões, já que um load balancer vive dentro de uma região só.

A fraqueza é o cache: o cliente segura o registro DNS pelo TTL e às vezes bem além dele, então failover por DNS não é rápido e não é confiável no detalhe. Daí a divisão de trabalho usual: DNS resolve a distribuição global e grosseira, o load balancer resolve a decisão fina dentro de cada região.

O modelo mental

Um load balancer é um ponto de decisão com estado, que mantém uma visão viva da saúde da sua frota e aplica uma política a cada requisição que chega. A camada em que ele opera define quanto da requisição ele consegue enxergar, o que ele enxerga define quão inteligente a política pode ser, e essa inteligência custa latência.

Todo o resto, os algoritmos, os health checks, a stickiness, é detalhe pendurado nesse único trade-off.

Como isso se conecta com o resto

O load balancer é uma peça de um laço maior. Ele extrai o máximo dos servidores que existem, mas não cria o próximo servidor quando os atuais saturam, e isso é trabalho do auto scaling, como vimos em Escalando para alta demanda. Os dois nem se falam diretamente, se comunicam por métricas num sistema central.

Do lado da rede, o endereço estável que ele expõe é o mesmo tipo de problema resolvido em IP privado, público e Elastic IP, e o motivo pelo qual failover por ENI só funciona dentro de uma AZ, discutido em ENI, é exatamente o motivo pelo qual o load balancer existe: ele é a estratégia de failover que atravessa zonas.

Referências