Blogue

Como Chegámos a Zero Erros 502 nos Deploys

Sempre que fazíamos deploy do backend do Mythex, um punhado de pedidos falhava 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 uma compilação que alguém estava a ver parou por um momento. Seguimos isto até dois problemas separados: um watchdog de autorrecuperação que lutava contra os nossos deploys, e um intervalo entre o momento em que um servidor deixava de escutar e o momento em que o balanceador de carga deixava de lhe enviar tráfego. Em três deploys consecutivos de produção depois das correções, a contagem passou de 10 para 6 e depois para 0.

A configuração

A nossa API e o orquestrador correm como contentores num pequeno grupo de servidores atrás de um Application Load Balancer (ALB) da AWS. Um deploy atualiza um servidor de cada vez: drena os contentores antigos, arranca os novos e passa ao seguinte. Um watchdog em cada servidor verifica se os contentores estão no ar e reinicia qualquer um que não esteja — existe para que um processo que falhe às 3 da manhã volte sem que ninguém tenha de acordar.

Ambos são razoáveis por si. Juntos, não funcionavam bem.

Problema 1: o watchdog competia com o deploy

O watchdog corria uma vez por minuto e "curava" ao primeiro único check de saúde falhado. Um deploy põe deliberadamente os serviços em drenagem durante até 150 segundos — durante os quais um check de saúde deve falhar. Por isso, o watchdog disparava em cada deploy.

Na maioria das vezes perdia a corrida sem consequências. Uma vez, ganhou: reescreveu o ficheiro compose (a descrição dos contentores que devem correr) a meio de um deploy, e deixou um servidor sem contentor de gateway nenhum — enquanto o job de deploy reportava sucesso.

As correções:

  • Um único lock. O deploy passa agora a usar o mesmo lock de ficheiro do script de bootstrap do servidor, para que o watchdog e um deploy nunca corram ao mesmo tempo.
  • Quatro falhas, não uma. O watchdog precisa de quatro checks de saúde falhados consecutivos antes de agir. Um serviço em drenagem já não parece morto.
  • Uma só definição. O ficheiro compose tem agora uma única fonte, publicada para os servidores, em vez de cópias que podiam divergir.
  • Verificar, não confiar. O job de deploy já não passa só porque um comando devolveu sucesso. 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 de o tráfego sair

O segundo problema era mais discreto. Quando um deploy drena um servidor, este passa a responder ao seu endpoint de saúde com 503 para que o balanceador de carga o retire de rotação. O nosso fazia isso durante 5 segundos e depois desligava-se.

Mas o ALB só verificava a saúde de tempos a tempos, e precisava de várias falhas seguidas antes de deixar de encaminhar para um alvo — até cerca de 60 segundos no total. Por isso, durante uns 55 segundos, o balanceador continuava a enviar pedidos reais para um servidor que já tinha deixado de escutar. Esses eram os nossos 502.

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

  • O target group verifica agora a cada 5 segundos e marca um alvo como não saudável após 2 falhas — cerca de 10 segundos para notar.
  • A drenagem continua a servir tráfego real durante 15 segundos depois de começar a falhar os checks de saúde, para haver sempre sobreposição.
  • Estas definições vivem no script que provisiona a infraestrutura, e não numa consola que alguém clicou uma vez.

Fazer os números sustentarem-se uns aos outros

Timeouts como estes dependem uns dos outros. A drenagem tem de durar mais do que o tempo que o balanceador precisa para notar; a drenagem mais o encerramento têm de terminar antes de o contentor ser morto à força. É fácil alguém mudar um número daqui a seis meses e reintroduzir o bug sem dar por isso.

Por isso, escrevemos essas relações como testes: a drenagem tem de ser mais longa do que o intervalo de check de saúde vezes o limiar de falhas; o tempo para voltar a ser marcado como saudável tem de continuar curto; a drenagem inteira tem de caber no período de graça de paragem do contentor. Muda um número sem mudar os outros e o CI falha. Também fizemos mutation testing aos próprios testes — mudámos cada número para um valor errado e verificámos que um teste o apanhava.

Medir um deploy é fácil de fazer mal

A nossa primeira medição dizia que um deploy limpo estava cerca de 3% partido. Não estava. Estávamos a contar todas as respostas que não eram 200, e um 503 de um endpoint de saúde durante uma drenagem não é uma falha — é a drenagem a fazer o seu trabalho, dizendo ao balanceador para não escolher esse servidor enquanto continua a servir pedidos reais.

O que os utilizadores sentem é um 502: o pedido não chegou a lado nenhum. Contar só esses deu a imagem real — 10, depois 6, depois 0 em três deploys de produção.

O que tirámos disto

  • A autorrecuperação tem de conhecer o downtime planeado. Um watchdog que não consegue distinguir um deploy de um crash acaba por lutar contra o deploy.
  • Drenar é um aperto de mão. O servidor tem de continuar a servir até o balanceador de carga ter provado que deixou de enviar tráfego, e esse timing é definido no balanceador, não no servidor.
  • Codifica os invariantes como testes. Os números operacionais derivam. Testes que os relacionam entre si não derivam.
  • Decide o que estás a medir antes de medir. "Erros" era a métrica errada; "pedidos que não chegaram a lado nenhum" era a certa.

Se publicas uma app no Mythex, nada disto é visível para ti — que é precisamente o objetivo. Relacionado: como a tua app publicada continua no ar enquanto a republicas.

Mythex · Pricing · Crie qualquer coisa com IA · Modelos 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 (Brasil) Română Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)