O arquivo de swap que faltava e travou os nossos servidores
Em 24 de setembro, um dos nossos servidores de produção parou de responder. O balanceador de carga o marcou como não saudável e a nossa ferramenta de comandos remotos não conseguia alcançá-lo, mas nada tinha travado e nada tinha sido morto por usar memória demais. O servidor estava em thrashing: sem espaço de swap e com contêineres autorizados a usar mais memória do que a máquina tinha, o Linux passava o tempo despejando e relendo os arquivos de que precisava para rodar, e quase não avançava. Adicionar um arquivo de swap de 2 GB em cada servidor resolveu. A lição mais interessante foi sobre health checks.
Como era o "travamento"
- Os health checks do balanceador de carga expiravam nesse servidor.
- Os comandos enviados pelo AWS Systems Manager ficavam em "Pendente" e nunca rodavam.
- Não havia morte por falta de memória nos logs. Nenhum processo tinha morrido.
Uma semana antes, tínhamos movido a produção para servidores ARM menores com 2 GB de RAM cada. Nosso servidor de testes, ainda menor, tinha travado da mesma forma ao descompactar uma imagem de contêiner durante um deploy.
Por que nenhum erro de memória?
O Linux só mata um processo quando de fato esgota a memória que pode recuperar. Antes desse ponto, ele recupera memória do cache de páginas, as cópias de arquivos que mantém na RAM, incluindo o código de todo programa em execução.
Nos nossos servidores, a soma dos limites de memória dos contêineres chegava a cerca de 3,3 GB numa máquina com cerca de 1,8 GB utilizáveis. Normalmente eles não usam o limite todo ao mesmo tempo. Quando usavam, o kernel continuava despejando código de programas do cache para abrir espaço, e depois tinha de lê-lo de novo do disco no momento em que esse código voltava a rodar. Esse ciclo, o thrashing, pode deixar uma máquina quase totalmente sem resposta sem nunca esgotar tecnicamente a memória. Então nada é morto, e nada se recupera.
Sem swap, o kernel não tem onde colocar memória alocada mas raramente usada, então precisa tirá-la do cache.
A correção
O script de bootstrap dos servidores agora cria um arquivo de swap de 2 GB com um valor baixo de swappiness (10) toda vez que um servidor sobe ou é reparado. Swappiness baixo significa que o kernel ainda prefere manter a memória de trabalho na RAM, mas páginas pouco tocadas podem ir para o disco em vez de forçar o código dos programas para fora do cache. Aplicamos ao vivo nos dois servidores de produção e no de testes, e o pipeline de deploy republica o script de bootstrap a cada deploy de produção, para que um servidor substituído também receba a configuração.
A parte que mais nos preocupou: o servidor não saudável continuou no grupo
Nosso grupo de servidores usava health checks do EC2 ("a máquina está rodando?") e não health checks do balanceador de carga ("ela está respondendo requisições?"). Assim, um servidor que o balanceador já tinha descartado continuava saudável do ponto de vista do grupo. Ele nunca seria substituído. A produção ficaria discretamente rodando em um único servidor até alguém notar e reiniciar o outro.
Trocar o grupo para health checks do balanceador parece a correção óbvia, mas não é direto para nós: durante um deploy, os servidores respondem de propósito com erro no seu endpoint de saúde para que o balanceador os drene (veja como chegamos a zero 502s nos deploys). Um grupo que substitui servidores com base nesse sinal os substituiria no meio de cada deploy.
Por isso, por enquanto, acrescentamos uma etapa à nossa checklist de deploy: depois de cada deploy, verificar a saúde de cada destino no balanceador, e não só se o job de deploy passou. Se os comandos enviados a um servidor ficarem em "Pendente", ele está em thrashing: reinicie-o; ele já está fora de rotação.
O que tiramos disso
- "Não travar" não é "saudável". O thrashing não produz erro, só uma lentidão tão extrema que a máquina para de responder.
- Servidores pequenos precisam de swap. Mesmo pouco, com swappiness baixo, dá ao kernel um lugar para colocar memória fria.
- Limites de memória que somam mais do que a máquina são uma aposta. Às vezes você perde. Saiba quando fez a aposta.
- Saiba em qual health check o seu autoscaling confia. Um health check que só pergunta "a VM está ligada?" não substitui um servidor que parou de funcionar.