"Coloque um load balancer na frente" é a frase mais comum em system design, e ela esconde um punhado de decisões que importam em produção: em que camada ele trabalha, como escolhe um backend, como sabe que um backend está saudável e o que acontece com as requisições em andamento quando um é removido.
Este artigo passa por essas decisões e desenha a configuração em dois níveis a que chega a maioria dos deploys grandes: um balanceador de camada 4 na frente de uma frota de proxies de camada 7.
Camada 4 vs. camada 7
| Camada 4 | Camada 7 | |
|---|---|---|
| Enxerga | Endereços IP, portas, conexões TCP ou UDP | Requisições HTTP: host, caminho, headers, cookies |
| Balanceia | Conexões | Requisições |
| TLS | Repassa ou termina | Em geral termina |
| Roteia por | Porta, endereço de origem | Caminho, host, header, método |
| Também faz | Vazão muito alta, baixa latência | Retries, timeouts, reescritas, autenticação, rate limiting |
| Exemplos | Load balancers de rede das nuvens, IPVS, balanceadores no estilo Maglev | Envoy, NGINX, HAProxy em modo HTTP, load balancers de aplicação das nuvens |
A diferença que dói: um balanceador de camada 4 balanceia conexões, não requisições. Com HTTP/2 ou gRPC, um cliente abre uma única conexão de longa duração e manda milhares de requisições por ela, e todas caem no mesmo backend. Balancear bem gRPC exige um proxy de camada 7 ou balanceamento no cliente.
Por que dois níveis
Configurações grandes costumam empilhar os dois. Um nível de camada 4 fica com os endereços IP públicos, absorve enxurradas de conexões e espalha as conexões por uma frota de proxies de camada 7 fazendo hash de cada fluxo. Os proxies terminam o TLS, aplicam as regras de roteamento e balanceiam cada requisição entre os pools de backends.
Cada nível escala por conta própria: o de camada 4 por pacotes e conexões, a frota de proxies por requisições e handshakes TLS. Separá-los também limita o raio de impacto, já que uma mudança ruim de configuração dos proxies atinge os proxies atrás de um nível, não os endereços a que os usuários se conectam. E como os proxies não têm estado e compartilham uma única configuração, adicionar um é só adicionar capacidade.
Algoritmos de balanceamento
| Algoritmo | Como escolhe | Bom para | Cuidado com |
|---|---|---|---|
| Round robin | O próximo backend da vez | Backends idênticos, requisições parecidas | Requisições lentas se acumulam em backends lentos |
| Round robin ponderado | Round robin na proporção dos pesos | Instâncias de tamanhos diferentes, mudanças graduais | Os pesos são estáticos |
| Least connections | Menos conexões abertas | Conexões de longa duração | Precisa de estado global no balanceador |
| Least request (duas escolhas aleatórias) | Sorteia dois e manda para o menos ocupado | A maioria dos serviços HTTP | Precisa da contagem de requisições por backend |
| Hashing consistente | Hash de uma chave, como o ID do usuário | Caches, afinidade | Carga desigual com chaves quentes |
Least request com duas escolhas aleatórias é um bom padrão para serviços HTTP. A documentação de load balancers do Envoy descreve assim: sorteie dois hosts (dois por padrão) e mande a requisição para o que tiver menos requisições ativas. Pesquisas de Mitzenmacher e outros mostram que isso é quase tão bom quanto verificar todos os backends, sem o custo. O hashing consistente, como o ring hash ou o algoritmo Maglev, mantém a mesma chave no mesmo backend e move só uma pequena parte das chaves quando os backends mudam, que é o que os caches querem.
Health checks
Um balanceador precisa parar de mandar tráfego para backends que não conseguem atendê-lo. Dois mecanismos trabalham juntos:
- Checagens ativas: o balanceador chama um endpoint de saúde, por exemplo
GET /healthza cada 5 segundos, e remove um backend depois de um número de falhas seguidas. - Checagens passivas (outlier detection): o balanceador observa as respostas reais, ejeta um backend que retorna erros ou estoura timeouts e o traz de volta depois de um período de resfriamento.
Faça o endpoint de saúde honesto, mas barato. Se ele checar todas as dependências, um único banco lento deixa todos os backends doentes ao mesmo tempo e o balanceador fica sem lugar para mandar o tráfego. Cheque o que esta instância precisa para atender requisições e deixe as dependências compartilhadas para o tratamento de erros da própria aplicação.
Draining e sessões fixas
Quando um backend é removido para um deploy ou uma redução de escala, ele deve parar de receber requisições novas, mas terminar as que estão em andamento. Configure um período de draining (os balanceadores das nuvens chamam de deregistration delay) maior que a sua requisição normal mais lenta, e faça a aplicação parar de aceitar trabalho novo ao desligar.
Sessões fixas (sticky sessions) mandam um usuário para o mesmo backend toda vez, geralmente com um cookie. Elas ajudam quando o estado mora na memória de um servidor e atrapalham todo o resto: carga desigual, sessões perdidas quando um backend cai, deploys mais difíceis. Leve o estado da sessão para um armazenamento compartilhado e desligue a afinidade onde puder.
Além de uma região
Tudo acima acontece dentro de um único site. Espalhar usuários entre regiões é outra camada: roteamento por DNS que responde com a região saudável mais próxima (roteamento por latência ou geolocalização), ou anycast, em que vários sites anunciam o mesmo endereço IP e o roteamento da internet leva cada usuário a um site próximo. O balanceamento global escolhe a região; os dois níveis deste artigo escolhem o servidor. Faça os sinais de saúde subirem, para que uma região cujos proxies não alcançam seus pools pare de receber usuários em vez de devolver erros a eles.
O diagrama de arquitetura
Conteúdo do diagrama
- Clients
- DNS: one name
- Layer 4 load balancer
- Layer 7 proxy 1
- Layer 7 proxy 2
- Orders pool: 6 instances
- Search pool: 4 instances
- Static files bucket
- Proxy fleet, same config
- resolve
- TCP 443
- flow hash
- /api/orders
- /api/search
- /static
Como ler:
- Esquerda: os clientes resolvem um nome DNS e abrem conexões TCP na porta 443 com o load balancer de camada 4.
- Centro: o nível de camada 4 espalha as conexões pela frota de proxies por hash de fluxo. A moldura mostra que todos os proxies rodam a mesma configuração.
- Direita: os proxies terminam o TLS e roteiam cada requisição pelo caminho:
/api/orderse/api/searchpara seus pools,/staticpara um bucket. - O bloco Markdown traz a tabela de roteamento, o algoritmo de cada pool e as configurações dos health checks, detalhes que normalmente só existem num arquivo de configuração.
O que rotular num diagrama de load balancer
- A camada e o protocolo em cada salto: TCP 443, HTTP/2, gRPC.
- Onde o TLS termina. Isso decide o que cada nível consegue ver e onde ficam os certificados.
- O algoritmo de cada pool, e a chave de hash, se você usa hash.
- Caminho, intervalo e limite dos health checks.
- As configurações de draining e afinidade, se houver.
O diagrama de arquitetura de API gateway cobre os recursos de camada 7 que ficam em cima do roteamento, o diagrama de ingress do Kubernetes mostra as mesmas ideias dentro de um cluster, e o diagrama de topologia de rede posiciona os load balancers na rede como um todo.
Monte no Archboard
- Entre com Google ou GitHub e crie um diagrama.
- Adicione uma Pessoa do pack Modelo C4 para os clientes, e um DNS e um Balanceador de carga do pack Rede.
- Adicione uma Moldura do pack Básico para a frota de proxies, com dois nós Contêiner dentro.
- Adicione dois nós Instância de computação e um nó Armazenamento de objetos do pack Nuvem para os pools e o bucket.
- Rotule as arestas com protocolos e rotas, e adicione um bloco Markdown do pack Básico com a tabela de roteamento e os health checks.
O exemplo usa 10 nós de diagrama e fica dentro do plano Free.
Perguntas frequentes
Qual a diferença entre um load balancer de camada 4 e um de camada 7? Um balanceador de camada 4 distribui conexões TCP ou UDP sem olhar dentro delas. Um de camada 7 entende HTTP, então consegue rotear por caminho ou host e balancear cada requisição.
Que algoritmo de balanceamento devo usar? Least request com duas escolhas aleatórias é um padrão forte para HTTP. Use hashing consistente quando a mesma chave precisar chegar ao mesmo backend, como em caches.
Por que meu serviço gRPC manda todo o tráfego para um único pod? Porque o gRPC usa conexões HTTP/2 de longa duração e um balanceador de camada 4 balanceia conexões, não requisições. Coloque um proxy de camada 7 no meio ou use balanceamento no cliente.
O que um endpoint de health check deve checar? Que esta instância consegue atender requisições: o processo está de pé e seus próprios recursos críticos estão disponíveis. Evite checar dependências compartilhadas que derrubariam todos os backends de uma vez.
Sessões fixas são uma boa ideia? Só quando o estado não pode sair do servidor. Elas causam carga desigual e sessões perdidas em falhas; um armazenamento de sessão compartilhado costuma ser melhor.