Blog

Como zeramos os erros 502 nos deploys

Toda vez que fazíamos deploy do backend do Mythex, alguns poucos pedidos falhavam com 502 Bad Gateway. Não muitos, mas um 502 durante um deploy significa que o clique de alguém não fez nada, ou que um build que a pessoa acompanhava travou por um instante. Rastreamos isso até dois problemas separados: um watchdog de autorrecuperação que brigava com os nossos deploys, e uma lacuna entre o momento em que um servidor parava de escutar e o momento em que o balanceador de carga parava de enviar tráfego para ele. Em três deploys consecutivos de produção depois das correções, a contagem foi de 10 para 6 e depois para 0.

A configuração

Nossa API e nosso orquestrador rodam como contêineres em um pequeno grupo de servidores atrás de um Application Load Balancer (ALB) da AWS. Um deploy atualiza um servidor por vez: drena os contêineres antigos, inicia os novos e passa para o próximo. Um watchdog em cada servidor verifica se os contêineres estão de pé e reinicia qualquer um que não esteja. Ele existe para que um processo travado às 3 da manhã volte sem que ninguém precise acordar.

Ambos são razoáveis isoladamente. Juntos, não eram.

Problema 1: o watchdog competia com o deploy

O watchdog rodava uma vez por minuto e "curava" ao primeiro health check que falhasse. Um deploy drena serviços deliberadamente por até 150 segundos, período em que um health check deve falhar. Então o watchdog disparava durante cada deploy.

Na maioria das vezes ele perdia a corrida sem dano. Uma vez, ganhou: reescreveu o arquivo compose (a descrição de quais contêineres devem rodar) no meio de um deploy e deixou um servidor sem nenhum contêiner de gateway, enquanto o job de deploy informava sucesso.

As correções:

  • Um só lock. O deploy agora pega o mesmo lock de arquivo do script de bootstrap do servidor, de modo que o watchdog e um deploy nunca rodam ao mesmo tempo.
  • Quatro falhas, não uma. O watchdog precisa de quatro health checks consecutivos falhando antes de agir. Um serviço em drenagem não parece mais morto.
  • Uma só definição. O arquivo compose agora tem uma única fonte, publicada nos servidores, em vez de cópias que podiam divergir.
  • Verificar, não confiar. O job de deploy não passa mais só porque um comando retornou. Ele verifica a API, o orquestrador, o gateway e o worker de publicação de cada servidor antes de poder ser bem-sucedido.

Problema 2: o servidor saiu antes do tráfego

O segundo problema era mais discreto. Quando um deploy drena um servidor, ele passa a responder com 503 no endpoint de saúde para que o balanceador o tire de rotação. O nosso fazia isso por 5 segundos e então desligava.

Mas o ALB só verificava a saúde de tempos em tempos, e precisava de várias falhas seguidas antes de parar de rotear para um destino, até cerca de 60 segundos de ponta a ponta. Assim, por cerca de 55 segundos, o balanceador continuava enviando requisições reais para um servidor que já tinha parado de escutar. Esses eram os nossos 502.

A correção foi fazer as duas linhas do tempo concordarem:

  • O target group agora verifica a cada 5 segundos e marca um destino como não saudável após 2 falhas, cerca de 10 segundos para perceber.
  • A drenagem continua atendendo tráfego real por 15 segundos depois de começar a falhar nos health checks, então sempre há sobreposição.
  • Essas configurações vivem no script que provisiona a infraestrutura, e não em um console que alguém clicou uma vez.

Fazer os números se sustentarem mutuamente

Timeouts como esses dependem uns dos outros. A drenagem precisa durar mais que o tempo que o balanceador leva para perceber; a drenagem somada ao desligamento precisa terminar antes de o contêiner ser morto à força. É fácil alguém mudar um desses números daqui a seis meses e reintroduzir o bug sem perceber.

Por isso, escrevemos essas relações como testes: a drenagem deve ser maior que o intervalo do health check vezes o limite de falhas; o tempo para voltar a ser marcado como saudável deve continuar curto; toda a drenagem precisa caber dentro do período de graça de parada do contêiner. Mude um número sem os outros e o CI falha. Também fizemos teste de mutação nos próprios testes: alteramos cada número para um valor errado e conferimos se um teste o pegava.

Medir um deploy é fácil de errar

Nossa primeira medição dizia que um deploy limpo tinha cerca de 3% de falhas. Não tinha. Estávamos contando toda resposta que não era 200, e um 503 de um endpoint de saúde durante a drenagem não é uma falha: é a drenagem fazendo o seu trabalho, avisando o balanceador para não escolher aquele servidor enquanto ele continua atendendo requisições reais.

O que o usuário sente é um 502: a requisição não alcançou nada. Contar apenas essas deu o quadro real: 10, depois 6, depois 0 em três deploys de produção.

O que tiramos disso

  • Autorrecuperação precisa saber sobre indisponibilidade planejada. Um watchdog que não consegue distinguir um deploy de um travamento vai, eventualmente, brigar com o deploy.
  • Drenar é um aperto de mãos. O servidor precisa continuar atendendo até que o balanceador tenha comprovadamente parado de enviar tráfego, e esse tempo é definido no balanceador, não no servidor.
  • Codifique invariantes como testes. Números operacionais se desalinham. Testes que os relacionam entre si não.
  • Decida o que está medindo antes de medir. "Erros" era a métrica errada; "requisições que não alcançaram nada" era a certa.

Se você publica um app no Mythex, nada disso fica visível para você, e esse é o objetivo. Relacionado: como o seu app publicado continua no ar enquanto você o publica de novo.

Mythex · Pricing · Crie qualquer coisa com IA · Templates de sites e apps · Mythex comparado · Glossário de criação de apps com IA · Ferramentas grátis para criar apps com IA · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Deutsch Ελληνικά Español Español (España) Eesti فارسی Suomi Français Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska Italiano 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål Nederlands ਪੰਜਾਬੀ Polski Português (Portugal) Română Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)