backupdevopsmonitoramentopostgressrerestore

Backup com dead-man switch — e o ensaio de restore que precisa saber falhar

Douglas Haruo 11 min 29/08/2026

Existe um modo de falha de backup que nenhum log de erro registra: o job que para de rodar. Ele não falha. Falhar seria bom, porque falha aparece em log. Ele é descarregado do agendador numa atualização do sistema, ou o disco de destino sai do lugar, ou a credencial expira, e a partir daí não acontece mais nada. Nenhuma linha nova no log. Nenhum alerta, porque o alerta que você configurou dispara quando o job falha, e um job que não roda não falha.

Foi exatamente assim que um job de backup noturno desta infraestrutura ficou 14 dias parado em agosto de 2026, descarregado do launchd, o agendador do sistema. Ninguém notou. O log não tinha erro nenhum: tinha uma ausência, e ausência não aciona nada.

Este post é o desenho que fecha esse buraco, em três peças. A primeira é o backup que puxa, não empurra. A segunda é o dead-man switch, o sinal invertido que transforma silêncio em alarme. A terceira, que quase todo mundo pula, é o ensaio de restore que roda de verdade. Ele traz uma verificação negativa: um passo que precisa falhar para o ensaio passar.


Peça 1: o backup puxa, e a cópia de fora é a que importa

O desenho de base: uma VPS roda os serviços e produz dumps locais a cada 6 horas (Postgres, SQLite com .backup() online, tarballs de configuração). Uma máquina fora do datacenter, no caso um Mac, puxa esses dumps toda madrugada via rsync, o utilitário que copia arquivos entre máquinas.

Puxar em vez de empurrar tem uma consequência de segurança que vale explicitar: a VPS não tem credencial nenhuma para escrever na máquina de backup. Um invasor que compromete a caixa consegue destruir os dumps locais dela. As cópias de fora ele não alcança. Na mesma linha, o rsync roda com --ignore-existing e nunca com --delete: um snapshot que já chegou não é sobrescrito nem removível pela origem. A poda é local, por idade (60 dias), feita pelo lado que puxa.

As três camadas completas, com o que cada uma sobrevive:

CamadaOndeSobrevive a
Dumps na própria VPS, a cada 6hmesmo disco do bancocorrupção lógica; não a perda do disco
Snapshots do provedor (7 rotativos)mesmo provedorperda do disco; não a perda da conta
Pull noturno para foraoutra máquina, outro lugarperda do provedor inteiro

A terceira camada é a única cópia fora do provedor. Por isso é a única que carrega o dead-man switch. As outras duas têm quem as vigie: o próprio provedor e um cron na caixa. A de fora depende de uma máquina doméstica e de um agendador que já provou que sabe morrer em silêncio.

Um detalhe que paga o custo de existir: os arquivos comprimidos recém-chegados passam por gunzip -t antes de contarem como backup. Um .gz truncado por queda de conexão é apagado na hora. Um backup corrompido descoberto no dia do restore não é um backup: é uma pegadinha com data marcada.

Peça 2: o dead-man switch — silêncio vira alarme

A correção para “o job que não roda não falha” é inverter a direção do sinal. Em vez de o job avisar quando algo dá errado, ele avisa quando dá certo. No fim de cada rodada bem-sucedida, um curl bate numa URL de healthcheck, o endereço que recebe esse sinal de vida. O serviço do outro lado espera esse ping numa cadência conhecida (uma vez por dia, com 6 horas de tolerância). Se o ping não chega, ele alerta.

A diferença é estrutural: o alerta não depende mais de o job estar vivo para ser emitido. Job descarregado do agendador, máquina desligada, rede fora, script travado antes do fim: todos esses modos de falha colapsam no mesmo sintoma observável, o silêncio. Inclusive os que ninguém previu. E silêncio agora tem dono.

Dois cuidados de implementação:

  • A URL do check fica fora do git. Ela é, na prática, uma credencial: quem a conhece consegue silenciar o alarme pingando no seu lugar. Mora num arquivo ignorado. Se o arquivo não existe, o script avisa em voz alta (“dead-man switch inativo”) em vez de seguir calado.
  • O mesmo padrão, invertido, roda dentro da VPS. Um guardião em cron a cada 10 minutos checa disco (≥85%), containers unhealthy, containers parados que tinham restart policy, e um healthcheck profundo da aplicação. Se tudo está bem, pinga OK; se algo falha, pinga a variante de falha com o motivo no corpo. E se a caixa inteira morrer? Silêncio. O serviço de healthcheck converte esse silêncio em alerta em no máximo uma hora e meia. A VPS morta não consegue avisar que morreu, e o desenho não precisa que ela consiga.

A regra geral que esses dois casos ilustram: quem vigia não pode depender da coisa vigiada para emitir o alarme. Todo o resto é detalhe de implementação.

Peça 3: o ensaio de restore — trimestral, descartável, e com falha de propósito

Backup sem restore testado é uma hipótese com bom marketing. A frase é batida; o que é menos batido é o formato de um ensaio que realmente informa alguma coisa.

O ensaio desta infraestrutura roda num script só, na máquina de backup, contra um Postgres descartável. É um container efêmero numa porta alta, derrubado por trap na saída, que nunca encosta na produção. Ele pega os artefatos da última rodada de backup, os mesmos arquivos que o restore de verdade usaria, não uma cópia preparada. Depois restaura os bancos, roda as migrações e confere.

O último ensaio completo: 37 verificações, 0 falhas, dados restaurados em menos de um minuto, contagem de linhas conferida tabela a tabela, 2.855 linhas, 0 divergências.

Os números que saem do ensaio são os que nenhum dashboard de backup mostra.

  • RTO medido, não estimado, o tempo até voltar ao ar: restaurar os dados leva segundos. A caixa inteira, do zero, é estimada em 35–60 minutos. Essa estimativa está escrita ao lado do número medido, marcada como estimativa.
  • RPO real, o tanto de dado que se perde entre uma cópia e outra: os dumps na caixa dão 6 horas. A cópia de fora, que depende do pull noturno, pode chegar a ~30 horas. É o número honesto, e é maior do que a intuição sugere.

A falha de propósito

A parte do desenho que eu mais recomendo copiar: o ensaio contém verificações que exigem uma falha para passar.

A mais importante envolve credenciais cifradas no banco. O ensaio restaura e decifra uma credencial com a chave correta, vinda do tarball de configuração do backup. Isso prova que a chave também está no backup. Depois ele troca a chave por uma errada e exige que a decifragem levante exceção. Se a chave errada decifrar, o ensaio aborta com erro: a credencial não estava cifrada de verdade, e o “sucesso” do restore estaria escondendo um vazamento.

É o mesmo princípio do teste que você vê falhar antes de deixá-lo passar, aplicado a infraestrutura. Uma verificação que nunca foi vista reprovando não distingue “seguro” de “não checado”.

Na mesma linha, o ensaio falha duro quando um insumo essencial não existe: sai com erro, não com aviso. O comentário no próprio script resume a filosofia. O ensaio existe para dizer se dá para voltar. Sem o tarball de variáveis de ambiente, o dump volta com colunas ilegíveis, e um ensaio que “passa com ressalvas” nessa condição estaria certificando um restore que não funciona.

Outras guardas na mesma categoria. Insumo com mais de 30 horas reprova, porque o RPO prometido foi quebrado e é o ensaio que deve contar isso. Postgres de destino mais velho que o de origem reprova. Schema restaurado com menos tabelas que o esperado reprova.

O ensaio que mentia

E o achado que justifica tudo isso: a primeira versão do próprio script de ensaio mentia. Um docker exec -i dentro de um while read consumia o stdin do loop, a entrada padrão que alimentava a lista de tabelas. A lista era engolida na primeira iteração. O script conferia uma tabela e reportava “0 divergências” com toda a confiança do mundo.

O instrumento de verificação tem os mesmos direitos que qualquer código: ele quebra, ele mente, ele precisa ser verificado. A correção veio acompanhada da contagem explícita (“N tabelas conferidas”) no relatório. “0 divergências em 54 tabelas” e “0 divergências” são frases muito diferentes, e a segunda esconde exatamente o bug que aconteceu.


O checklist

  1. A cópia que importa é a que a origem não alcança. Pull, não push; sem --delete; poda por idade no lado que puxa.
  2. Teste a integridade na chegada (gunzip -t ou equivalente). Backup corrompido descoberto no restore não é backup.
  3. Dead-man switch em todo job agendado que importa. O job pinga no sucesso; o silêncio alerta. A URL do ping é credencial: fora do git.
  4. O vigia de dentro pinga falha com motivo; a morte da caixa vira silêncio. E o silêncio já tem quem o escute.
  5. Ensaie o restore numa cadência fixa, contra infraestrutura descartável, com os artefatos reais. Publique RTO e RPO medidos, não desejados.
  6. Inclua verificações negativas. A chave errada tem que falhar. O insumo ausente tem que reprovar. Um ensaio incapaz de falhar não é um ensaio: é uma cerimônia.
  7. Desconfie do instrumento. O primeiro bug que o ensaio encontra costuma ser no próprio ensaio, e isso não é um vexame: é o processo funcionando.

Infraestrutura que escala sem quebrar o orçamento

Conta de cloud fora de controle? Opero a minha própria numa VPS única, sem porta aberta, com deploy automático e rollback por healthcheck. O desenho inteiro está publicado aqui.

Ver os posts de infraestrutura →