Pular para o conteúdo
Quiz
← Início

Balanceamento de carga

100 questões

Certas
0
Erradas
0
Não respondidas
100
Dificuldade
Número de questões

100 questões neste filtro

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 . 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
  enxergam
O desenho mais simples de balanceamento de carga

Os 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á .
↑ Voltar ao sumário

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.

AspectoEscalabilidade vertical (scale up)Escalabilidade horizontal (scale out)
O que você fazDeixa uma máquina maior: mais núcleos, mais memória, discos mais rápidosAdiciona mais instâncias do mesmo serviço
ImagemTrocar o caixa por um super caixaAbrir mais caixas
Precisa mudar a aplicação?Normalmente não, é o jeito mais simplesSim: ela precisa funcionar com várias cópias ao mesmo tempo
Precisa de balanceador de carga?Não, existe uma só máquinaSim, algo precisa repartir as requisições
LimitesExiste 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 é 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.

Capacidade que você precisa comprar para sobreviver à perda de uma máquina (pico = 100)
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 . 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ário

Proxy reverso, balanceador de carga e API gateway

Um fica entre duas partes. A pergunta é: para quem ele trabalha?

TipoOnde ficaQuem 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 servidoresOs 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ários
Duas decisões diferentes

O 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.9 significa que o cliente é 203.0.113.7 (o primeiro), depois veio o proxy 10.0.0.5, e 10.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-For com 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.
↑ Voltar ao sumário

Camada 4 contra camada 7

Descrevemos as redes como um prédio com andares, chamados camadas. O 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.

AspectoCamada 4Camada 7
EnxergaEndereços e portas de origem e destinoA mensagem HTTP inteira: caminho, cabeçalho Host, cookies, outros cabeçalhos
Unidade de trabalhoA conexão: uma decisão e depois só repassa bytesA requisição: cada requisição é lida, interpretada e enviada de novo
ConexõesRepassa os bytes de uma conexãoTermina a conexão TCP do cliente e abre (ou reaproveita) outra para o back end, então há duas conexões
ProtocolosQualquer um (não se importa com o que há dentro)Os que ele entende, normalmente HTTP
Custo por requisiçãoMenorMaior (mais CPU e memória)
Regras de roteamentoPobres: só endereço e portaRicas: 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)
A camada 4 vê uma conexão, a camada 7 vê muitas requisições

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ário

Algoritmos de balanceamento

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

Pesos 3, 2 e 1: como 600 requisições são repartidas
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%.

Parcela das requisições que o back end lento recebe
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 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.
AlgoritmoDecide porMelhor quandoPonto fraco
Round robinUm contador (turnos)Servidores iguais, requisições parecidasCego à carga e à velocidade
Round robin ponderadoContador e pesosServidores de tamanhos diferentesOs pesos são palpites fixos
Least connectionsRequisições em andamentoRequisições com durações muito diferentesPrecisa de contadores compartilhados, servidores novos são inundados
Aleatório / duas escolhasSorteio (e uma comparação)Vários balanceadores sem coordenaçãoDesigualdade de curto prazo (aleatório simples)
IP hash / URL hashUm hash de uma chaveAfinidade, cachesDesigual quando as chaves se concentram
↑ Voltar ao sumário

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

AspectoPor cookiePor endereço IP
ComoO 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 balanceadorUm 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 fracoO cookie é um dado controlado pelo clienteSupõ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ário

Hashing 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 dela
O anel (a saída do hash é tratada como um círculo)

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

Chaves que trocam de servidor ao ir de 4 para 5 servidores
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ário

Verificaçõ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).

AspectoVerificação ativaVerificação passiva
O que fazO balanceador envia as próprias requisições de teste (probes) em intervalo fixo, por exemplo para /healthO 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 clientesSim, precisa de requisições reais para aprender
Custo de descobrir um servidor mortoSó tráfego de testeAs primeiras requisições reais enviadas ao servidor morto falham ou esperam um timeout, então usuários reais pagam
ExemploCaddy: health_uri, health_intervalNGINX: 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 aconteceuO back end viu a requisição?Tentar em outro back end?
Conexão recusada ou timeout de conexãoNãoSempre seguro, qualquer método
A requisição foi enviada e depois a conexão caiu ou a resposta não veioTalvez, e talvez tenha sido executadaSomente para requisições , 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ário

Alta 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 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

AspectoAtivo-passivoAtivo-ativo
Quem atendeUm nó atende todo o tráfego, o outro espera em standbyOs dois atendem ao mesmo tempo
Quando um falhaO standby assumeO outro absorve toda a carga
RecursosO standby fica ociosoMelhor aproveitamento dos recursos
O senãoA troca leva algum tempoCada 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.

Disponibilidade dos exemplos
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ário

Terminação TLS

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

ModoO que aconteceLê o HTTP (regras de camada 7)?Tráfego interno criptografado?Onde ficam certificado e chave?
PassthroughO balanceador (camada 4) não descriptografa. O túnel vai do cliente ao back endNãoSim, de ponta a pontaEm cada back end (distribuir e renovar em cada um)
Terminação (simples)O balanceador descriptografa e depois fala HTTP puro com os back endsSimNão, o trecho interno é em texto puroSó no balanceador
RecriptografiaO balanceador descriptografa, inspeciona e depois abre uma nova conexão TLS com o back endSimSimNo 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)
Onde o túnel termina

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 com http://, essa é a causa. O proxy informa o esquema original no cabeçalho X-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 diretiva proxy_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.
↑ Voltar ao sumário

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;
    }
}
Um grupo e um location que o usa
  • upstream api { ... } declara um grupo de servidores e dá um nome a ele. O grupo sozinho não recebe tráfego.
  • proxy_pass dentro de um location transforma-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. No ip_hash, para IPv4 a chave são apenas os três primeiros octetos do endereço, então 192.168.1.10 e 192.168.1.200 (mesma rede /24) vão para o mesmo servidor, e para IPv6 é o endereço inteiro.

Parâmetros do servidor

ParâmetroSignificadoPadrão
weight=3Proporção entre os servidores na rotação1
max_fails=2Quantas tentativas sem sucesso, dentro de fail_timeout, fazem o servidor ser considerado indisponível1
fail_timeout=15sAo mesmo tempo a janela de contagem E por quanto tempo o servidor fica indisponível10s
backupUm reserva: fica fora da rotação normal e só é usado quando os servidores primários estão indisponíveisnã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_pass tem 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. Com location /api/ { proxy_pass http://backend/; }, uma requisição para /api/users chega ao back end como /users. Sem a barra (http://backend) o caminho chega como /api/users.
  • O cabeçalho Host. O NGINX reescreve o Host com $proxy_host, o destino escrito no proxy_pass. Uma aplicação que usa o Host para escolher o site ou montar links veria um nome interno. proxy_set_header Host $host; repassa o nome que o cliente realmente pediu.
↑ Voltar ao sumário

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
    }
}
Um Caddyfile com balanceamento, verificação de saúde e novas tentativas

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

IdeiaNGINX (open source)Caddy
Grupo de back endsupstream nome { server ...; } mais proxy_passreverse_proxy a b c
Algoritmo padrãoRound robin ponderadorandom
Outras políticasleast_conn, ip_hash, hash, random twolb_policy round_robin, least_conn, client_ip_hash, cookie, random_choose 2
Verificação ativaNã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 passivaLigada: max_fails=1, fail_timeout=10sDesligada até fail_duration ser maior que 0
Nova tentativa após uma falhaLigada: proxy_next_upstream error timeoutDesligada 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.

↑ Voltar ao sumário

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 capacidadeEscalabilidade horizontal, dimensionamento N+1, achar o gargalo real
Rotear por caminho, host ou cookieBalanceador de camada 7 ou API gateway
Balancear gRPC ou outras conexões longasProxy de camada 7 ou um cliente que espalha as chamadas
Divisão igual com servidores desiguaisPesos, ou least connections
Um acerto de cache no mesmo nóURL hash, hashing consistente
Manter um usuário em um servidorAfinidade por cookie (mas prefira estado compartilhado ou token assinado)
Sobreviver a um servidor mortoVerificações de saúde, novas tentativas só para requisições idempotentes, draining
Sobreviver a um balanceador mortoUm par com IP virtual, quórum ou fencing
HTTPS em um só lugarTerminaçã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.
↑ Voltar ao sumário