Índice do diárioDockup / nota de campo
Note / deploy-succeeded-but-site-is-down

Deploy concluído, mas o site está fora do ar

O seu dashboard mostra que está em execução, mas os utilizadores veem um erro. Saiba por que o sucesso do deploy e a saúde da aplicação são sinais diferentes e como fazer com que um deploy verde signifique que a app realmente responde.

Existe um tipo específico de manhã difícil que começa com um visto verde. O deploy foi concluído, mas o site está fora do ar, o dashboard diz em execução e alguém está a enviar-lhe uma captura de ecrã de um erro 502.

Este não é um caso raro. É o resultado previsível de uma plataforma comunicar uma coisa e medir outra — e vale a pena compreender isto com precisão, porque a solução não é "verificar melhor": é mudar o que a palavra em execução pode significar.

Três perguntas diferentes, uma única luz de estado

Quando uma plataforma diz que um serviço está em execução, pode estar a responder a qualquer uma destas perguntas:

  1. O container iniciou? O processo existe e não terminou.
  2. A porta está aberta? Existe algo a escutar onde a plataforma espera.
  3. A aplicação responde corretamente? Um pedido recebe uma resposta que significa que a app está pronta para funcionar.

Estas garantias são completamente diferentes, e a maioria dos incidentes deste tipo acontece porque o dashboard responde à pergunta 1 enquanto você presumiu que era a pergunta 3.

Um processo Node que arranca, não consegue ligar-se à base de dados e fica num ciclo de retry satisfaz a pergunta 1 para sempre. Não teve um crash. Nunca vai processar um pedido. O container está "em execução" em todos os sentidos que interessam ao orquestrador.

A lacuna onde a indisponibilidade acontece

A janela perigosa fica entre "a nova versão iniciou" e "a nova versão consegue funcionar". Nessa janela, uma plataforma ingénua já transferiu o tráfego, porque iniciar era a única coisa que media.

Isto é pior do que um crash simples por causa da estratégia de rollback. Um crash loop é ruidoso: o container termina, reinicia, termina novamente e a plataforma acaba por perceber. Um arranque seguido de bloqueio é silencioso. Nada reinicia, nenhum alerta é acionado e a versão anterior que funcionava normalmente já foi eliminada.

Essa última parte é o verdadeiro dano. A versão antiga estava bem. Foi removida porque uma nova versão iniciou, e iniciar foi confundido com funcionar.

O que faz uma verdadeira barreira de health

A solução é estrutural, não processual. O tráfego não deve ser transferido até a nova versão responder a um pedido.

No Dockup, um release funciona assim: a nova versão é criada de forma isolada, iniciada ao lado da versão que está atualmente a responder e, depois, é-lhe feita uma pergunta. Só quando responde é que o domínio passa a apontar para ela. Se nunca responder, o release para nesse ponto e a versão anterior continua a responder — ninguém fora do seu dashboard fica sequer a saber que houve uma tentativa de deploy.

É por isso que um deploy falhado no Dockup não é uma indisponibilidade. O container antigo nunca foi removido com base na suposição de que o novo estaria bem.

# The health gate is per-service configuration, not a platform default you inherit
dockup info my-project/my-api --json

O bloco healthCheck nessa saída é todo o contrato: qual o path solicitado, quanto tempo esperar por uma resposta, quantas vezes tentar e quanto tempo aguardar entre tentativas.

Configure o check para responder à pergunta 3

Um health endpoint que devolve 200 incondicionalmente é pior do que não ter nenhum, porque transforma uma barreira real num carimbo de aprovação. O objetivo do check é falhar quando a aplicação não consegue fazer o seu trabalho.

Um endpoint de readiness útil verifica aquilo sem o qual a app não consegue funcionar:

// Not this — it proves only that the process is alive
app.get('/healthz', (req, res) => res.send('ok'))

// This — it proves the app can actually serve a request
app.get('/healthz', async (req, res) => {
  try {
    await db.query('select 1')       // the dependency that is usually the problem
    if (!cacheReady) throw new Error('cache warming')
    res.status(200).json({ ok: true })
  } catch (err) {
    res.status(503).json({ ok: false, reason: err.message })
  }
})

Duas regras fazem isto funcionar na prática:

Verifique as dependências sem as quais não consegue responder e nada mais. Se a sua app consegue degradar graciosamente quando o índice de pesquisa está indisponível, não faça a readiness depender do índice de pesquisa — vai bloquear deploys por algo que não é uma indisponibilidade.

Mantenha o check barato. O endpoint é chamado repetidamente durante cada release. Um readiness check que executa uma query dispendiosa cria um problema de carga autoinfligido.

Dê-lhe tempo suficiente, mas não tempo ilimitado

Duas definições determinam se a barreira ajuda ou prejudica:

  • O timeout de cada tentativa deve ser superior ao seu cold start legítimo mais demorado. Uma app que liga a uma base de dados e aquece uma cache em oito segundos vai falhar sempre num check de três segundos, e você vai "resolver" o problema desativando a barreira — o que o deixa exatamente no ponto de partida.
  • As tentativas devem abranger o tempo total de arranque, não apenas uma tentativa. Intervalo × tentativas é o orçamento real.

No Dockup, estas definições são healthCheckInterval, healthCheckTimeout e healthCheckRetries, e aplicam-se por serviço, porque um monólito Rails e um sidecar Go não arrancam ao mesmo ritmo.

Quando já está fora do ar

Se está a ler isto durante um incidente, esta é a ordem que o resolve mais depressa:

  1. Verifique se a app responde diretamente, ignorando o domínio. Se responde na porta, mas não através do domínio, isto é um problema de routing, não um problema da aplicação, e deve parar de investigar o seu código.
  2. Leia os logs de runtime, não os logs de build. O build foi concluído com sucesso — essa é a premissa. O que precisa de saber é o que o processo fez depois de iniciar.
  3. Faça rollback antes de diagnosticar. O diagnóstico é mais barato quando ninguém está a observar.
dockup logs my-project/my-api --follow            # what the running process is saying
dockup deployments my-project/my-api              # what was live before this
dockup rollback <deployment-id> my-project/my-api # put that back

No Dockup, um rollback é uma troca, não uma nova compilação, porque a versão anterior ainda está no disco. Isto é importante às três da manhã: a recuperação mais rápida é aquela que não precisa de compilar nada.

A pergunta que deve fazer a uma plataforma

Quando estiver a escolher onde executar produção, há um teste útil que pode fazer deliberadamente: faça deploy de uma aplicação que inicia com sucesso e depois falha ao ligar-se à base de dados. Veja o que diz o dashboard.

Se disser em execução, já sabe exatamente quanto essa palavra vai valer durante o próximo incidente.

Perguntas frequentes

Por que motivo o meu dashboard diz que está em execução quando o site está fora do ar? Porque "em execução" normalmente significa que o processo do container existe, não que a aplicação consegue responder a um pedido. Um processo preso a tentar novamente a ligação à base de dados satisfaz essa definição indefinidamente.

Um health check deve aceder à base de dados? Sim, se a sua aplicação não consegue responder a pedidos sem ela. Verifique as dependências de que realmente precisa e ignore aquelas sem as quais consegue degradar.

Qual é a diferença entre liveness e readiness? Liveness pergunta se o processo deve ser reiniciado. Readiness pergunta se deve receber tráfego. A barreira que impede esta falha é a readiness, e tem de ser executada antes de o tráfego ser transferido.

Como posso impedir que um deploy problemático deixe o site fora do ar? Transfira o tráfego apenas depois de a nova versão responder a um pedido real e mantenha a versão anterior até a troca estar confirmada. Assim, um release falhado é um release que nunca aconteceu, e não uma indisponibilidade.