Resumo teórico
Imagine a cantina da escola com um único caixa e uma fila enorme. A fila anda devagar, as crianças ficam bravas e, se o caixa passa mal e vai embora, ninguém come. Agora imagine que a cantina abre quatro caixas e uma professora fica na porta dizendo "você vai para o caixa 1, você vai para o caixa 2...". A fila anda quatro vezes mais rápido e, se um caixa passa mal, a professora simplesmente para de mandar crianças para lá. Essa professora é um balanceador de carga (load balancer).
Na internet é igual. Um site ou um aplicativo é mantido por um programa em um computador chamado servidorum computador que espera pedidos de outros computadores e responde a eles. Um servidor só consegue fazer um tanto de trabalho. Quando milhões de pessoas pedem páginas ao mesmo tempo, usamos muitos servidores, e alguma coisa precisa decidir qual servidor responde a cada pessoa. Essa coisa é o balanceador de carga. Você encontra um toda vez que abre um site grande, um app de vídeo ou um jogo online, mesmo sem nunca vê-lo.
Neste resumo você vai aprender, passo a passo: por que usamos muitos servidores em vez de um servidor gigante, o que é um balanceador de carga e como ele se diferencia de um proxy reverso e de um API gateway, em qual "andar" da rede ele pode trabalhar, como ele escolhe um servidor (os algoritmos), como ele percebe que um servidor morreu, como mantém um usuário no mesmo servidor quando é preciso, como lida com o cadeado secreto do HTTPS, como evitar que o próprio balanceador derrube tudo e como configurá-lo no NGINX e no Caddy. No final, você será capaz de responder ao quiz desta área.
O quadro geral e as palavras que usamos
Vamos começar com algumas palavras que você verá o tempo todo. Um cliente é quem pede alguma coisa (seu navegador, o app do celular). Uma requisição (request) é uma pergunta que o cliente envia, como "me dê a página inicial". Um servidor responde. Quando colocamos muitos servidores iguais atrás de uma única porta, cada um deles se chama back end (ou instância, uma cópia do mesmo programa). A porta é o balanceador de carga.
┌──────────┐ ┌─> back end 1
clientes ─> │ balance- │ ─────┼─> back end 2
│ ador │ └─> back end 3
└──────────┘
um endereço várias cópias iguais
que os clientes da aplicação
enxergamOs clientes só conhecem o endereço do balanceador. Eles nunca descobrem quantos back ends existem. Isso é muito útil: podemos adicionar ou remover back ends e ninguém de fora percebe.
Os dois trabalhos de um balanceador de carga
- Repartir o trabalho: para que nenhum servidor seja esmagado enquanto outros ficam parados. Isso dá mais capacidade (mais requisições por segundo).
- Sobreviver a falhas: quando um servidor morre, parar de mandar requisições para ele. Os usuários continuam sendo atendidos pelos outros. Isso dá disponibilidadea fração do tempo em que um serviço está funcionando e utilizável.
Escalabilidade horizontal contra vertical
Sua loja recebe mais clientes do que consegue atender. Há duas coisas que você pode fazer. Pode comprar uma máquina gigante e super rápida para trocar a antiga. Ou pode comprar mais máquinas comuns e usá-las juntas. Esses dois caminhos têm nome.
| Aspecto | Escalabilidade vertical (scale up) | Escalabilidade horizontal (scale out) |
|---|---|---|
| O que você faz | Deixa uma máquina maior: mais núcleos, mais memória, discos mais rápidos | Adiciona mais instâncias do mesmo serviço |
| Imagem | Trocar o caixa por um super caixa | Abrir mais caixas |
| Precisa mudar a aplicação? | Normalmente não, é o jeito mais simples | Sim: ela precisa funcionar com várias cópias ao mesmo tempo |
| Precisa de balanceador de carga? | Não, existe uma só máquina | Sim, algo precisa repartir as requisições |
| Limites | Existe um tamanho máximo, o preço sobe rápido perto dele, redimensionar muitas vezes exige parar o serviço, e continua sendo UMA máquina (um ponto único de falha) | Não há teto de uma máquina só, mas partes compartilhadas (como um único banco de dados) podem virar o novo limite |
Por que serviços stateless escalam fácil na horizontal
Um serviço é statelessnão guarda, entre uma requisição e a seguinte, nada de que a seguinte dependa quando qualquer cópia pode responder a qualquer requisição. Os dados que precisam sobreviver (sessões, carrinhos, pedidos) ficam em um armazenamento compartilhado, como um banco de dados. Assim o balanceador pode mandar cada requisição para qualquer lugar, e as cópias podem surgir ou sumir à vontade.
Dimensionamento: N+1
Se uma instância atende 400 requisições por segundo e o momento de maior movimento traz 1500, você precisa de ceil(1500 / 400) = ceil(3,75) = 4 instâncias só para atender o pico (sempre arredondamos para cima). Chame esse número de N. Para continuar atendendo o pico inteiro quando uma instância morrer, você acrescenta uma reserva: N + 1 = 5.
A reserva fica mais barata quando as máquinas são pequenas. Para sobreviver a uma falha com 2 máquinas grandes, cada uma capaz de carregar 100% do pico, você paga 200% do pico. Com 8 máquinas pequenas, cada uma carregando 1/7 do pico, você paga 8/7, cerca de 114%. Uma reserva em 8 é uma fração menor do que uma reserva em 2.
- 2 máquinas grandes
- 200 % do pico
- 8 máquinas pequenas
- 114.3 % do pico
Elasticidade e suas armadilhas
Com a escalabilidade horizontal você pode ligar instâncias no horário de pico e desligá-las nas horas calmas, uma de cada vez, enquanto as outras continuam trabalhando. Isso se chama elasticidadecapacidade que acompanha a demanda, para cima e para baixo. Uma máquina escalada na vertical fica dimensionada para o pico o dia inteiro, ou precisa ser parada a cada mudança de tamanho.
↑ Voltar ao sumárioProxy reverso, balanceador de carga e API gateway
Um proxyum programa intermediário que repassa pedidos e respostas entre dois lados fica entre duas partes. A pergunta é: para quem ele trabalha?
| Tipo | Onde fica | Quem ele representa |
|---|---|---|
| Proxy direto (forward proxy) | Ao lado dos clientes (por exemplo, uma rede de empresa em que todos saem para a internet por ele) | Os clientes. O servidor não vê quem está por trás |
| Proxy reverso (reverse proxy) | Ao lado dos servidores | Os servidores. O cliente fala só com ele e não precisa saber que ele existe |
Um proxy reverso é um lugar natural para fazer trabalhos que toda requisição precisa: TLS (o cadeado do HTTPS), cache, compressão, esconder o endereço real da origem e segurar clientes lentos para que a aplicação fique livre para a próxima requisição. Por isso um proxy reverso é útil mesmo com UM SÓ servidor atrás dele. O que ele não consegue dar é redundância, que exige mais de um servidor.
Um balanceador de carga de camada 7 é um proxy reverso que também decide qual de várias cópias equivalentes atende cada requisição. É por isso que o NGINX e o Caddy fazem os dois papéis com a mesma configuração.
Um API gateway é um proxy reverso especializado em APIs. Em um único ponto ele aplica regras comuns a todas as rotas: quem pode chamar (autenticação), com que frequência (rate limiting), para qual serviço vai cada rota e como requisições ou respostas são adaptadas. A lógica de negócio fica nos serviços.
┌─> réplica 1 de pedidos
/orders │ (balanceador de pedidos)
clientes ─> API ───────┼─> réplica 2 de pedidos
gateway │
/users ├─> réplica 1 de usuários
│ (balanceador de usuários)
└─> réplica 2 de usuáriosO que muda quando há um proxy no meio
O proxy mantém duas conexões separadas: uma com o cliente, outra com o back end. Isso traz três consequências práticas.
- A aplicação vê o endereço do proxy, não o do cliente. O proxy passa o endereço do cliente em um cabeçalho chamado
X-Forwarded-For. Ele é uma lista que cresce para a direita: cada proxy acrescenta o endereço de quem lhe entregou a requisição. Com saltos honestos,X-Forwarded-For: 203.0.113.7, 10.0.0.5, 10.0.0.9significa que o cliente é203.0.113.7(o primeiro), depois veio o proxy10.0.0.5, e10.0.0.9é outro salto. O último proxy não aparece na lista, porque é o par da conexão que a aplicação enxerga. - Nunca confie nesse cabeçalho às cegas. Qualquer cliente pode enviar
X-Forwarded-Forcom um valor inventado, e os proxies acrescentam no FIM. Então a entrada mais à esquerda é a que o cliente controla. Para limites de taxa ou controle de acesso, leia da direita para a esquerda, pule os endereços dos seus próprios proxies confiáveis e fique com o primeiro que sobrar. - Cabeçalhos hop-by-hop são removidos. Alguns cabeçalhos (
Connection,Keep-Alive,Transfer-Encoding,Upgrade) valem apenas para uma conexão entre dois vizinhos, ao contrário dos cabeçalhos end-to-end, que viajam do cliente até a origem. Como o proxy tem duas conexões independentes, ele descarta os hop-by-hop que recebeu e cria os que servem para a outra conexão.
Camada 4 contra camada 7
Descrevemos as redes como um prédio com andares, chamados camadas. O modelo OSIuma forma padrão de descrever redes em sete camadas, dos cabos até as aplicações tem sete: física, enlace de dados, rede, transporte, sessão, apresentação e aplicação. Só duas importam aqui. A camada 4 é o andar do transporte, onde moram o TCP e o UDP, com endereços e portas. A camada 7 é o andar da aplicação, onde mora o HTTP, com URLs, cabeçalhos e cookies. "L4" e "L7" dizem até qual andar um balanceador lê o tráfego antes de decidir.
| Aspecto | Camada 4 | Camada 7 |
|---|---|---|
| Enxerga | Endereços e portas de origem e destino | A mensagem HTTP inteira: caminho, cabeçalho Host, cookies, outros cabeçalhos |
| Unidade de trabalho | A conexão: uma decisão e depois só repassa bytes | A requisição: cada requisição é lida, interpretada e enviada de novo |
| Conexões | Repassa os bytes de uma conexão | Termina a conexão TCP do cliente e abre (ou reaproveita) outra para o back end, então há duas conexões |
| Protocolos | Qualquer um (não se importa com o que há dentro) | Os que ele entende, normalmente HTTP |
| Custo por requisição | Menor | Maior (mais CPU e memória) |
| Regras de roteamento | Pobres: só endereço e porta | Ricas: por caminho, host, cabeçalho, cookie |
Então a regra de escolha é simples: olhe o que a sua regra de roteamento precisa. Separar serviços por endereços ou portas diferentes funciona na camada 4. Se a diferença está no caminho da URL (/api/* aqui, /static/* ali, mesmo host e mesma porta), em um cabeçalho ou em um cookie, essa informação só aparece depois de ler o HTTP, então você precisa de pelo menos a camada 7.
A armadilha da conexão longa
Um balanceador de camada 4 escolhe uma vez por conexão. Se os clientes mantêm uma conexão aberta e mandam milhares de requisições por ela (keep-alive do HTTP/1.1), todas vão para o mesmo back end, e conexões com volumes diferentes geram carga desigual. O mesmo acontece, com mais força, com HTTP/2 e gRPC, que multiplexam muitas chamadas em uma só conexão. Um cliente muito ativo pode sobrecarregar um back end enquanto dois outros ficam parados. A correção é balancear na unidade certa, a requisição ou o stream: um proxy de camada 7, ou um cliente que abre conexões com vários back ends e se alterna entre elas.
camada 4: [conn A: req req req req req ...] -> back end 1
[conn B: req ................] -> back end 2
camada 7: req -> back end 1
req -> back end 2
req -> back end 3 (uma escolha por requisição)Com HTTPS, um balanceador de camada 4 pode trabalhar em passthrough: ele repassa os bytes criptografados e o túnel secreto vai do cliente até o back end. Ele ainda vê o que está fora da criptografia (endereços, portas, quando as conexões abrem e fecham), mas caminho, cabeçalhos e cookies são invisíveis para ele.
↑ Voltar ao sumárioAlgoritmos de balanceamento
Um algoritmouma receita fixa de passos para resolver um problema de balanceamento responde a uma pergunta: qual back end recebe a próxima requisição? Vamos conhecer os mais comuns.
Round robin
Os back ends recebem requisições em turnos, um depois do outro, e quando a lista acaba ela recomeça, como distribuir cartas ao redor de uma mesa. Ele ignora a carga e a velocidade de cada servidor. Com os back ends A, B, C começando por A, a requisição número k vai para a posição (k − 1) mod 3 da lista, contada a partir de zero. Para k = 7, (7 − 1) mod 3 = 0, então a 7ª requisição vai para A.
Round robin ponderado
Cada back end tem um peso, um número escrito na configuração. A parcela de um servidor é o seu peso dividido pela soma de todos os pesos. Com pesos 3, 2 e 1, a soma é 6, então as parcelas são 3/6, 2/6 e 1/6. De 600 requisições: 300, 200 e 100.
- servidor de peso 3
- 300 requisições
- servidor de peso 2
- 200 requisições
- servidor de peso 1
- 100 requisições
Os pesos são estáticos: são um palpite feito de antemão e não mudam sozinhos quando um servidor fica mais lento.
Menos conexões (least connections)
O balanceador mantém um contador para cada back end: soma 1 quando envia uma requisição e subtrai 1 quando ela termina. A próxima requisição vai para o back end com menos requisições em andamento. É uma estimativa simples do quão ocupado cada servidor está. Ele vence claramente o round robin quando as requisições têm durações muito diferentes, porque o round robin iguala o número de requisições enviadas, não o trabalho.
O least connections ponderado divide as conexões ativas pelo peso e escolhe o menor resultado. Exemplo: A tem 12 conexões e peso 1, B tem 20 e peso 4, C tem 9 e peso 1. As razões são 12, 5 e 9, então B é escolhido: um servidor de peso 4 aguenta quatro vezes mais.
Um servidor lento, dois algoritmos
Considere três back ends: dois respondem em 10 ms e um em 40 ms, e um número fixo de clientes manda a próxima requisição assim que a anterior termina. Com round robin, cada back end recebe um terço das requisições. Mas o lento precisa de quatro vezes mais tempo por requisição, então as requisições se acumulam nele. Com least connections, todo back end mantém o mesmo número de requisições em andamento. Um back end que mantém certo número de requisições as termina a uma taxa de (em andamento) dividido por (tempo por requisição), então as taxas ficam na proporção 1/40 : 1/10 : 1/10 = 1 : 4 : 4. Isso soma 9 partes, então o back end lento recebe 1/9 das requisições, cerca de 11%, e cada rápido 4/9, cerca de 44%.
- round robin
- 33.3 %
- least connections
- 11.1 %
Aleatório e o poder de duas escolhas
O aleatório (random) escolhe um servidor por sorteio a cada requisição. Em poucas requisições a divisão parece desigual, mas em muitas cada um de N servidores iguais recebe cerca de 1/N. Ele não precisa de contador nem de posição, então vários balanceadores podem aplicá-lo sem conversar entre si. O poder de duas escolhas (random two no NGINX, random_choose 2 no Caddy) sorteia dois servidores e fica com o menos carregado dos dois. Isso é bem mais equilibrado que o aleatório simples e custa só uma comparação. Também reduz o "efeito manada", em que vários balanceadores mandam tudo para o único servidor que parece o mais folgado.
Hash: IP hash e URL hash
Um hashuma função que transforma qualquer entrada em um número, sempre o mesmo número para a mesma entrada de uma chave escolhe o servidor. No IP hash a chave é o endereço do cliente, então o mesmo cliente volta ao mesmo servidor sem nenhuma tabela (isso vale enquanto o conjunto de servidores não muda). No URL hash a chave é o endereço pedido, então a mesma URL sempre vai para o mesmo servidor. É o melhor encaixe para caches: cada nó guarda respostas das URLs que atende, e a taxa de acerto sobe.
Mais duas armadilhas do least connections
- Contadores não compartilhados. Se dois processos de balanceador mantêm contadores separados, cada um decide com metade do quadro e as decisões podem se acumular no mesmo back end. O estado precisa ser compartilhado (uma zona de memória compartilhada), ou você usa uma política que tolera informação parcial.
- O servidor novo e vazio. Um servidor recém-chegado tem zero conexões, então num pool movimentado ele atrai quase todo o tráfego novo de uma vez, enquanto ainda está frio (caches vazios, nenhuma conexão aberta). Um slow start aumenta a parcela dele aos poucos.
| Algoritmo | Decide por | Melhor quando | Ponto fraco |
|---|---|---|---|
| Round robin | Um contador (turnos) | Servidores iguais, requisições parecidas | Cego à carga e à velocidade |
| Round robin ponderado | Contador e pesos | Servidores de tamanhos diferentes | Os pesos são palpites fixos |
| Least connections | Requisições em andamento | Requisições com durações muito diferentes | Precisa de contadores compartilhados, servidores novos são inundados |
| Aleatório / duas escolhas | Sorteio (e uma comparação) | Vários balanceadores sem coordenação | Desigualdade de curto prazo (aleatório simples) |
| IP hash / URL hash | Um hash de uma chave | Afinidade, caches | Desigual quando as chaves se concentram |
Sessões fixas (sticky sessions)
Às vezes o usuário precisa voltar ao mesmo back end. A afinidade de sessão (session affinity, também chamada de sticky session) é a regra pela qual o balanceador manda todas as requisições de um cliente para o mesmo back end, em vez de escolher de novo a cada vez. O balanceador reconhece o cliente por um cookie ou pelo endereço IP.
Por que alguém precisaria disso? Por causa de estado guardado dentro de uma instância. Se o seu login ou o seu carrinho de compras vive só na memória do processo que atendeu você, uma requisição enviada a outra instância não encontra nada. Prender o cliente à instância contorna o problema, mas não o resolve.
Duas formas de reconhecer o cliente
| Aspecto | Por cookie | Por endereço IP |
|---|---|---|
| Como | O balanceador escolhe um back end na primeira requisição e acrescenta à resposta um cookie que identifica a escolha. O navegador devolve o cookie toda vez, e o balanceador repete o destino. O estado mora no cliente, não numa tabela do balanceador | Um hash do endereço do cliente escolhe o back end (ip_hash no NGINX, client_ip_hash no Caddy) |
| Precisa de camada 7? | Sim (lê e escreve cabeçalhos HTTP) | Não |
| Ponto fraco | O cookie é um dado controlado pelo cliente | Supõe que um endereço = um usuário, o que falha nos dois sentidos: o mesmo usuário muda de endereço ao trocar de rede, e muitos usuários atrás de um NAT parecem um só |
O que a afinidade não dá
- Ela não protege o estado. Se a sessão existe só na memória de uma instância e essa instância cai, a sessão morre. O failover apenas garante que o usuário chegue a um servidor que não o conhece.
- Ela atrapalha o autoscaling. As instâncias sobrecarregadas mantêm todas as sessões que já tinham, e as instâncias novas recebem só clientes que ainda não estão presos. Quanto mais longas as sessões, mais lenta a redistribuição. Serviços stateless se reequilibram já na requisição seguinte.
- Ela pode desequilibrar a carga quando endereços são compartilhados (veja IP hash).
A cura de verdade: remover o estado
A afinidade só é necessária enquanto o estado da sessão vive dentro de uma instância. Tire o estado de lá, seja para um armazenamento compartilhado que todas as instâncias consultam, seja para um token assinado que o cliente envia a cada requisição, e as instâncias passam a ser stateless. O balanceador pode escolher qualquer uma, e instâncias podem cair ou ser adicionadas sem perder sessões.
↑ Voltar ao sumárioHashing consistente
Usar o hash de uma chave para escolher um servidor tem um grande problema. A receita simples é hash(chave) mod N, onde N é o número de servidores (mod é o resto da divisão). Se N muda, a resposta para quase toda chave muda.
Confira com números. Chaves 0, 1, 2, ... com chave mod N. Indo de N = 4 para N = 5, uma chave mantém o servidor só se o resto antigo e o novo forem iguais. O padrão se repete a cada 20 chaves, e os restos coincidem apenas nas chaves 0, 1, 2 e 3, ou seja, 4 em 20. Então 80% das chaves trocam de servidor. Para um cache, isso significa perder quase todo o conteúdo aquecido de uma vez.
O hashing consistente foi inventado para que uma mudança no conjunto de servidores mova apenas uma pequena fração das chaves, cerca de 1/N.
0
S1 * * k1
* *
k2 * * S2
* *
S3 * * k3
uma chave pertence ao primeiro servidor
encontrado andando no sentido horário
a partir da posição delaA mesma função de hash dá uma posição no círculo a cada servidor e a cada chave. No desenho clássico, cada chave pertence ao primeiro servidor encontrado andando no sentido horário a partir da posição da chave. A regra depende só de posições, então quando um servidor entra ou sai, só as chaves de um trecho do anel mudam de dono.
Exemplo: 4 servidores iguais dividem as chaves por igual, e um 5º servidor igual entra. As únicas chaves que se movem são as que o novo servidor agora possui: 1/5. Cada servidor antigo passa de 1/4 para 1/5, cedendo 1/20 do total, e 4 × 1/20 = 1/5.
- chave mod N
- 80 %
- hashing consistente
- 20 %
Nós virtuais
Se cada servidor tem apenas UMA posição no anel, os trechos (arcos) podem ter tamanhos muito diferentes, e quando um servidor sai, o vizinho no sentido horário herda a parcela inteira dele. Esse vizinho passa a carregar duas parcelas e, se também falhar, passa três ao próximo: uma cascata de falhas.
A solução são os nós virtuais (virtual nodes): cada servidor aparece dezenas ou centenas de vezes no anel, em muitas posições. Assim os arcos ficam uniformes, um servidor maior pode receber mais posições (mais parcela), e quando um servidor sai, as chaves dele se espalham por muitos outros em vez de um só.
↑ Voltar ao sumárioVerificações de saúde e failover
Um balanceador que continua mandando requisições para um servidor morto é pior do que nenhum balanceador. Por isso ele precisa descobrir quem está vivo. Isso é uma verificação de saúde (health check). Quando ele descobre um problema e passa o trabalho para os outros, isso é failover: a transferência automática do trabalho de um componente que falhou para um saudável. O failover precisa de duas coisas: redundância (outro componente pode assumir) e detecção (algo percebe a falha).
| Aspecto | Verificação ativa | Verificação passiva |
|---|---|---|
| O que faz | O balanceador envia as próprias requisições de teste (probes) em intervalo fixo, por exemplo para /health | O balanceador não envia probes. Observa as requisições reais e conta falhas (erros de conexão, timeouts) |
| Precisa de tráfego? | Não, funciona até sem clientes | Sim, precisa de requisições reais para aprender |
| Custo de descobrir um servidor morto | Só tráfego de teste | As primeiras requisições reais enviadas ao servidor morto falham ou esperam um timeout, então usuários reais pagam |
| Exemplo | Caddy: health_uri, health_interval | NGINX: max_fails dentro de fail_timeout |
As duas se completam. A passiva é imediata, mas precisa de tráfego. A ativa funciona sem tráfego e é a única que consegue trazer um servidor de volta, porque ninguém manda requisições reais para um servidor que está fora.
Um back end marcado como não saudável (unhealthy) não é apagado. O balanceador apenas para de escolhê-lo para novas requisições e espalha o tráfego pelos outros. Quando a verificação passa de novo, ele volta à rotação sem nenhum passo manual.
Em qual camada você verifica?
Uma verificação só detecta problemas na camada em que trabalha. Abrir uma conexão TCP prova que algum processo está escutando na porta, nada além. Se a aplicação responde status HTTP 500 a tudo, uma verificação TCP diz "saudável". Para perceber erros da aplicação, o probe precisa fazer uma requisição HTTP e conferir o código de status (e o corpo, quando necessário).
Quanto tempo até um servidor morto ser removido?
No pior caso, tempo de detecção = intervalo entre probes × número de falhas exigidas + timeout do probe. Se os probes saem a cada 2 s, três falhas consecutivas são exigidas e o timeout é minúsculo, um servidor que morre logo depois de um probe bom é removido 2 × 3 = 6 segundos depois. Durante esses 6 segundos ele ainda recebe requisições. Intervalos curtos detectam mais rápido, mas aumentam os alarmes falsos: esse é o equilíbrio a ser buscado.
Os limites também dão à decisão uma histerese (um atraso que evita mudar de decisão com facilidade demais). Se um servidor que alterna entre funcionar e falhar pudesse entrar e sair do pool a cada probe, cada entrada custaria requisições de clientes. Exigir vários sucessos consecutivos antes de voltar troca um retorno mais lento por estabilidade.
Tentar de novo ou não
Quando uma tentativa falha, o balanceador pode enviar a mesma requisição a outro back end? Depende do que o primeiro back end viu.
| O que aconteceu | O back end viu a requisição? | Tentar em outro back end? |
|---|---|---|
| Conexão recusada ou timeout de conexão | Não | Sempre seguro, qualquer método |
| A requisição foi enviada e depois a conexão caiu ou a resposta não veio | Talvez, e talvez tenha sido executada | Somente para requisições idempotentesuma operação que dá o mesmo resultado se executada duas vezes ou uma vez só, como GET, HEAD e OPTIONS |
Liveness, readiness e verificações rasas
- Liveness pergunta: o processo travou e precisa de reinício? Readiness pergunta: ele pode receber tráfego agora? Uma instância que ainda está carregando caches e abrindo conexões com o banco está viva, mas não pronta. O balanceador deve pulá-la, e ninguém deve reiniciá-la.
- Uma verificação profunda (que testa dependências compartilhadas, como rodar uma consulta no banco compartilhado) faz todos os back ends falharem juntos quando a dependência falha. Removê-los todos não ajuda, e ainda derruba rotas que continuariam funcionando (como páginas em cache). A verificação usada pelo balanceador deve ser rasa: ESTA instância consegue atender? Acompanhe as dependências por outros meios.
Retirar um servidor sem perder requisições
O connection draining faz isso em dois passos. Primeiro, o balanceador para de escolher a instância para novas requisições. Depois espera as requisições em andamento terminarem, até um prazo limite. Só então o processo é parado. Tentativas repetidas (retries) não substituem o draining, porque requisições não idempotentes não podem ser repetidas com segurança.
↑ Voltar ao sumárioAlta disponibilidade e pontos únicos de falha
Um ponto único de falha (SPOF, single point of failure) é qualquer parte do sistema que não tem substituto: se ela para, tudo para, por mais redundantes que sejam as outras partes. A pergunta útil para cada componente é: o que acontece se só este falhar? Alta disponibilidade é o trabalho de achar os SPOFs e duplicá-los.
Aqui vai a surpresa: cinco servidores de aplicação atrás de UM balanceador de carga. O balanceador remove o SPOF da camada de aplicação, mas, se estiver sozinho, vira um novo SPOF. Por isso os balanceadores são colocados em pares ou grupos, com um jeito de fazer o tráfego chegar ao que está saudável, como um endereço IP virtualum endereço que não está preso a uma máquina e pode se mudar para outra que troca de dono, ou o DNS. Redundância em uma camada não compensa a falta dela em outra.
Ativo-passivo e ativo-ativo
| Aspecto | Ativo-passivo | Ativo-ativo |
|---|---|---|
| Quem atende | Um nó atende todo o tráfego, o outro espera em standby | Os dois atendem ao mesmo tempo |
| Quando um falha | O standby assume | O outro absorve toda a carga |
| Recursos | O standby fica ocioso | Melhor aproveitamento dos recursos |
| O senão | A troca leva algum tempo | Cada nó precisa de folga suficiente para carregar tudo sozinho |
A aritmética da disponibilidade
Em paralelo (componentes redundantes, o conjunto funciona enquanto pelo menos um estiver de pé): o conjunto só falha se TODOS falharem juntos. Com falhas independentes, a indisponibilidade do conjunto é o produto das indisponibilidades. Dois balanceadores de 99% cada: indisponibilidade 0,01 × 0,01 = 0,0001, então disponibilidade 1 − 0,0001 = 99,99%.
Em série (as duas partes são necessárias): a disponibilidade do conjunto é o produto das disponibilidades. O par de 99,99% na frente de UM banco de dados de 99,9%: 0,9999 × 0,999 = 0,9989001, cerca de 99,89%. Nunca passa da parte mais fraca. Adicionar ainda mais balanceadores quase não ajuda. O ganho está em tornar o banco de dados redundante.
- um balanceador
- 99 %
- par de balanceadores
- 99.99 %
- par + um banco de dados
- 99.89 %
Split brain
Dois balanceadores em um par ativo-passivo se vigiam por um heartbeat (uma mensagem regular de "estou vivo"). Suponha que o enlace entre eles se quebre enquanto os dois estão saudáveis. Nenhum consegue distinguir a morte do par de uma falha do enlace, pois nos dois casos o heartbeat para. Os dois podem decidir "devo ser eu o ativo" e brigar pelo mesmo recurso, como o endereço IP virtual. Isso é o split brain (cérebro dividido). Para evitá-lo você precisa de informação de fora do enlace: um quórum com um terceiro participante (uma testemunha) que decide quem pode continuar ativo, ou fencing, que garante que o outro nó foi realmente desligado.
↑ Voltar ao sumárioTerminação TLS
O TLSa tecnologia que criptografa uma conexão; é o cadeado por trás do HTTPS cria um túnel secreto entre duas pontas. Quem é a ponta do túnel guarda o certificado (a carteira de identidade do site) e a chave privada (o segredo que prova que a identidade é dele). Terminar o TLS é ser essa ponta: você descriptografa os dados. Onde o túnel deve terminar quando há um balanceador? Há três jeitos.
| Modo | O que acontece | Lê o HTTP (regras de camada 7)? | Tráfego interno criptografado? | Onde ficam certificado e chave? |
|---|---|---|---|---|
| Passthrough | O balanceador (camada 4) não descriptografa. O túnel vai do cliente ao back end | Não | Sim, de ponta a ponta | Em cada back end (distribuir e renovar em cada um) |
| Terminação (simples) | O balanceador descriptografa e depois fala HTTP puro com os back ends | Sim | Não, o trecho interno é em texto puro | Só no balanceador |
| Recriptografia | O balanceador descriptografa, inspeciona e depois abre uma nova conexão TLS com o back end | Sim | Sim | No balanceador e nos back ends |
passthrough: cliente ════════════════════> back end
terminação: cliente ═══════> balanceador ───> back end
(HTTPS) (HTTP puro)
recriptogr.: cliente ═══════> balanceador ═══> back end
(HTTPS) (novo HTTPS)O principal benefício de terminar no balanceador é operacional: o TLS (certificados, chaves, versões, cifras) é configurado em um único lugar. Os back ends ficam mais simples e gastam a CPU com a aplicação. O preço é que o trecho interno deixa de ser criptografado, a menos que você recriptografe.
Consequências que você precisa conhecer
- O esquema se perde. Depois da terminação, a aplicação enxerga só a conexão interna, que é HTTP. Se os usuários navegam em
https://mas a aplicação monta redirecionamentos que começam comhttp://, essa é a causa. O proxy informa o esquema original no cabeçalhoX-Forwarded-Proto, e a aplicação deve usá-lo. Como qualquer cliente pode enviar esse cabeçalho, aceite-o apenas de proxies confiáveis. - Criptografar não é autenticar. Se o NGINX abre
proxy_pass https://backend;sem nenhuma diretivaproxy_ssl_*, ele não verifica o certificado do back end. Alguém dentro da rede que se passe pelo back end recebe tráfego que consegue descriptografar. Com recriptografia você precisa ligar a verificação explicitamente (proxy_ssl_verify on;) e informar a lista de autoridades certificadoras aceitas.
Configuração do NGINX
O NGINX (aqui, a versão open source 1.30) recebe suas ordens por um arquivo de configuração. Para balancear, você faz duas coisas: declara um grupo de servidores com nome e aponta uma diretiva de proxy para esse nome.
upstream api {
server app1:3000;
server app2:3000;
server app3:3000;
}
server {
location /api/ {
proxy_pass http://api/;
proxy_set_header Host $host;
}
}upstream api { ... }declara um grupo de servidores e dá um nome a ele. O grupo sozinho não recebe tráfego.proxy_passdentro de umlocationtransforma-o em um proxy reverso. Quando o nome na URL é um grupo upstream, o destino é escolhido entre os servidores do grupo pelo método de balanceamento.- O método padrão é o round robin ponderado, e cada servidor tem peso 1 a menos que você escreva
weight. Os outros métodos (least_conn,ip_hash,hash,random) só valem quando você escreve a diretiva dentro do bloco. Noip_hash, para IPv4 a chave são apenas os três primeiros octetos do endereço, então192.168.1.10e192.168.1.200(mesma rede /24) vão para o mesmo servidor, e para IPv6 é o endereço inteiro.
Parâmetros do servidor
| Parâmetro | Significado | Padrão |
|---|---|---|
weight=3 | Proporção entre os servidores na rotação | 1 |
max_fails=2 | Quantas tentativas sem sucesso, dentro de fail_timeout, fazem o servidor ser considerado indisponível | 1 |
fail_timeout=15s | Ao mesmo tempo a janela de contagem E por quanto tempo o servidor fica indisponível | 10s |
backup | Um reserva: fica fora da rotação normal e só é usado quando os servidores primários estão indisponíveis | não é backup |
Então um grupo com server a weight=3;, server b; e server c backup; manda tráfego para a e b na proporção 3 para 1, e o c só recebe requisições quando a e b estão indisponíveis.
Verificações passivas e novas tentativas
O NGINX open source tem apenas verificação passiva (o health_check ativo pertence à versão comercial). Ele julga um servidor pelas requisições reais. Quando max_fails é atingido dentro de fail_timeout, o servidor é pulado durante fail_timeout, e depois disso uma requisição real de cliente é usada para testá-lo de novo. Enquanto o servidor continua morto, esse ciclo se repete. Nesse meio tempo, proxy_next_upstream (padrão error timeout) repassa a requisição que falhou para outro servidor. Requisições não idempotentes (POST, LOCK, PATCH) já enviadas ficam fora dessa nova tentativa, a menos que você acrescente non_idempotent.
Dois detalhes que mordem
- A barra final do proxy_pass. Se o
proxy_passtem uma parte de URI depois do nome do destino (mesmo uma única/), o NGINX substitui a parte da requisição que casou com o location por essa URI. Comlocation /api/ { proxy_pass http://backend/; }, uma requisição para/api/userschega ao back end como/users. Sem a barra (http://backend) o caminho chega como/api/users. - O cabeçalho Host. O NGINX reescreve o
Hostcom$proxy_host, o destino escrito noproxy_pass. Uma aplicação que usa oHostpara escolher o site ou montar links veria um nome interno.proxy_set_header Host $host;repassa o nome que o cliente realmente pediu.
Configuração do Caddy
O Caddy (versão 2.11) faz o mesmo trabalho com um arquivo mais curto, chamado Caddyfile. Não existe um bloco separado para declarar o grupo: a diretiva reverse_proxy é proxy reverso e balanceador de carga ao mesmo tempo. Você lista os endereços na mesma linha, e as opções vão em um bloco entre chaves.
example.com {
reverse_proxy app1:3000 app2:3000 app3:3000 {
lb_policy round_robin
health_uri /health
health_interval 5s
lb_try_duration 5s
}
}HTTPS automático
Quando o endereço de um site é um nome de domínio público (como example.com acima), o Caddy liga o HTTPS sem nenhuma configuração extra. Ele obtém o certificado de uma autoridade certificadora usando ACME, renova automaticamente e redireciona o HTTP para HTTPS. Como balanceador de carga, isso é terminação TLS sem gerenciar arquivos de certificado.
Padrões diferentes dos do NGINX
| Ideia | NGINX (open source) | Caddy |
|---|---|---|
| Grupo de back ends | upstream nome { server ...; } mais proxy_pass | reverse_proxy a b c |
| Algoritmo padrão | Round robin ponderado | random |
| Outras políticas | least_conn, ip_hash, hash, random two | lb_policy round_robin, least_conn, client_ip_hash, cookie, random_choose 2 |
| Verificação ativa | Não (só na versão comercial) | health_uri (desligada até você escrevê-la); health_interval padrão 30s, health_timeout padrão 5s |
| Verificação passiva | Ligada: max_fails=1, fail_timeout=10s | Desligada até fail_duration ser maior que 0 |
| Nova tentativa após uma falha | Ligada: proxy_next_upstream error timeout | Desligada até lb_try_duration (ou lb_retries) ser definido; lb_try_interval é a pausa, 250 ms por padrão |
lb_try_duration 5s diz ao Caddy para continuar escolhendo upstreams e tentando de novo por até 5 segundos. Isso esconde do cliente a perda de um back end, ao preço de alguma espera. Nas verificações ativas, health_uri faz o próprio proxy enviar requisições a esse caminho em cada upstream, haja ou não tráfego de clientes. Um upstream que falha sai da seleção e volta quando o probe tem sucesso de novo.
Juntando tudo
Agora podemos ler a história inteira. Um servidor não basta, então escalamos na horizontal (mais instâncias) e colocamos um balanceador de carga na frente. O balanceador é um proxy reverso que decide qual cópia igual atende cada requisição. Na camada 4 ele decide por conexão, na camada 7 por requisição, com regras mais ricas. Ele escolhe por um algoritmo, evita servidores mortos graças às verificações de saúde, termina o TLS em um só lugar e precisa ser duplicado para não ser ele mesmo um ponto único de falha.
| Se você precisa de... | Olhe para... |
|---|---|
| Mais capacidade | Escalabilidade horizontal, dimensionamento N+1, achar o gargalo real |
| Rotear por caminho, host ou cookie | Balanceador de camada 7 ou API gateway |
| Balancear gRPC ou outras conexões longas | Proxy de camada 7 ou um cliente que espalha as chamadas |
| Divisão igual com servidores desiguais | Pesos, ou least connections |
| Um acerto de cache no mesmo nó | URL hash, hashing consistente |
| Manter um usuário em um servidor | Afinidade por cookie (mas prefira estado compartilhado ou token assinado) |
| Sobreviver a um servidor morto | Verificações de saúde, novas tentativas só para requisições idempotentes, draining |
| Sobreviver a um balanceador morto | Um par com IP virtual, quórum ou fencing |
| HTTPS em um só lugar | Terminação TLS, com X-Forwarded-Proto |
Erros comuns a evitar
- Acreditar no
X-Forwarded-Foràs cegas: leia da direita, pulando os proxies confiáveis. - Adicionar instâncias quando o banco de dados é o verdadeiro limite, ou quando os pools de conexões não vão caber.
- Usar round robin simples com servidores de velocidades diferentes.
- Usar IP hash quando muitos usuários compartilham um endereço.
- Confiar nas sticky sessions para proteger estado: use armazenamento compartilhado ou tokens.
- Esperar que o Caddy tente de novo e verifique a saúde sem configuração, ou que o NGINX open source faça verificações ativas.
- Tentar de novo um POST que talvez já tenha sido executado.
- Esquecer que um balanceador de carga sozinho é um SPOF, e que partes dependentes em série limitam a disponibilidade.
- Criptografar até o back end mas não verificar o certificado dele.