DevGuideTK

Backup e recuperação

RPO, RTO e por que um backup que nunca foi restaurado não conta.

O código desta página foi executado nas cinco linguagens contra o banco recuperado num ponto do tempo do nível Conceito, num PostgreSQL 18.

Verificar uma restauração, em cada linguagem

Depois de restaurar o backup num servidor descartável, um verificador decide se a restauração está boa. Cada versão confere três coisas e devolve a lista de problemas, vazia quando tudo está certo:

  1. As tabelas esperadas existem.
  2. Cada uma tem um mínimo de linhas.
  3. O registro mais recente está dentro do RPO: um backup que restaura, mas com dados de três dias atrás, não atende a um RPO de 24 horas.

O teste rodou três vezes: na restauração recente; na mesma restauração, avaliada como se fosse daqui a 2 dias; e num banco incompleto, só com a tabela de clientes.

import pg from "pg";

type Esperado = { tabela: string; minimoDeLinhas: number };
const ESPERADO: Esperado[] = [
  { tabela: "clientes", minimoDeLinhas: 1 },
  { tabela: "pedidos", minimoDeLinhas: 1000 },
];
const RPO_HORAS = 24; // quanto de dados o negócio aceita perder

// Confere um banco restaurado; devolve a lista de problemas (vazia = restauração aceita)
async function verificarRestauracao(url: string, agora = new Date()): Promise<string[]> {
  const banco = new pg.Client({ connectionString: url });
  await banco.connect();
  const problemas: string[] = [];
  try {
    for (const { tabela, minimoDeLinhas } of ESPERADO) {
      const existe = await banco.query("SELECT to_regclass($1) IS NOT NULL AS ok", [tabela]);
      if (!existe.rows[0].ok) {
        problemas.push(`tabela ${tabela} ausente`);
        continue;
      }
      // O nome vem da lista acima, nunca do usuário; as aspas protegem o identificador
      const { rows } = await banco.query(`SELECT count(*)::int AS n FROM "${tabela}"`);
      if (rows[0].n < minimoDeLinhas) problemas.push(`${tabela}: ${rows[0].n} linhas`);
    }
    if (problemas.length === 0) {
      const { rows } = await banco.query("SELECT max(criado_em) AS ultimo FROM pedidos");
      const horas = (agora.getTime() - rows[0].ultimo.getTime()) / 3_600_000;
      if (horas > RPO_HORAS) {
        problemas.push(
          `último pedido de ${horas.toFixed(0)} h atrás, acima do RPO de ${RPO_HORAS} h`);
      }
    }
  } finally {
    await banco.end();
  }
  return problemas;
}

const base = process.env.URL_RESTAURADO!;
console.log("restauração recente:", await verificarRestauracao(`${base}/loja`));
const daqui2Dias = new Date(Date.now() + 2 * 86_400_000);
console.log("vista daqui a 2 dias:", await verificarRestauracao(`${base}/loja`, daqui2Dias));
console.log("banco incompleto:", await verificarRestauracao(`${base}/incompleta`));

O resultado, nas cinco linguagens

CenárioProblemas encontrados
Restauração recentenenhum
A mesma, avaliada daqui a 2 diasúltimo pedido de 48 h atrás, acima do RPO de 24 h
Banco incompletotabela pedidos ausente

Num job de restauração automática, a lista não vazia vira um alerta e um código de saída diferente de zero.

O que muda de uma linguagem para outra

Os nomes das tabelas vêm da lista do próprio verificador, mas entram na consulta como identificadores, que não podem ir como parâmetros. Cada versão os protege com o recurso da sua biblioteca:

Proteger o nome da tabela
TypeScriptAspas duplas em volta do nome, que vem de uma lista fixa
Pythonpsycopg.sql.Identifier
Gopgx.Identifier{...}.Sanitize()
JavaStatement.enquoteIdentifier
C#Aspas duplas, duplicando as aspas internas

A existência da tabela é conferida com to_regclass, que devolve nulo em vez de erro, para o verificador listar todos os problemas de uma vez.

Os comandos do teste

Dump lógico e as definições globais, que o dump não leva:

pg_dump -Fc -f loja.dump loja                 # 6,0 s e 39 MB no teste
pg_dumpall --globals-only > globais.sql       # usuários e permissões

# Num servidor novo: primeiro as globais, depois o banco
psql -f globais.sql
createdb -O app loja
pg_restore -j 4 -d loja loja.dump             # 9,8 s no teste

Arquivamento contínuo do log, no postgresql.conf do servidor de teste:

wal_level = replica
archive_mode = on
archive_command = 'test ! -f /arquivo/%f && cp %p /arquivo/%f'

Cópia física, verificada:

pg_basebackup -h banco -U postgres -D /backup/base -X stream -c fast
pg_verifybackup /backup/base

Recuperação num ponto do tempo: na cópia restaurada, um arquivo recovery.signal vazio e, no postgresql.auto.conf:

restore_command = 'cp /arquivo/%f %p'
recovery_target_time = '2026-11-15 11:53:56.242946+00'
recovery_target_action = 'promote'

Ao subir, o servidor reaplicou o log até o horário indicado e se promoveu. No teste, a primeira tentativa da cópia física falhou com no pg_hba.conf entry for replication connection: o pg_basebackup usa uma conexão de replicação, que precisa de uma linha própria no pg_hba.conf.

Em produção, o archive_command com cp não basta: ele não confere se a cópia chegou inteira nem a envia para outro lugar. Ferramentas como pgBackRest, Barman e WAL-G fazem o arquivamento com verificação, compressão, criptografia e envio para armazenamento de objetos.

Lista de revisão

Objetivos

  • O RPO e o RTO estão definidos pelo negócio, por escrito?
  • A técnica de backup atende aos dois (PITR para RPO curto)?

Cópias

  • Há pelo menos uma cópia em outra região e outra conta, imutável?
  • Os backups são criptografados, com a chave guardada em outro lugar?
  • Usuários, permissões, configurações e extensões estão no backup?
  • A retenção cobre erros descobertos tarde e respeita os prazos legais?

Testes

  • A integridade de cada backup é conferida?
  • Há restauração automática num servidor separado, com verificação por consultas?
  • O RTO real foi medido numa restauração completa?

Recuperação

  • Existe um roteiro escrito, que restaura num servidor novo sem sobrescrever o danificado?
  • O time ensaiou a recuperação?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Configurar backup com recuperação num ponto do tempo

    Quero configurar backups com recuperação num ponto do tempo no meu PostgreSQL.
    
    Pergunte se uso servidor próprio ou gerenciado, a versão, o tamanho e onde quero guardar os backups.
    
    Vamos em etapas, com os comandos e arquivos completos:
    1. A ferramenta (pgBackRest, Barman ou WAL-G) e a configuração.
    2. O arquivamento contínuo do log e as cópias físicas periódicas.
    3. A retenção e o envio para armazenamento de objetos com bloqueio contra exclusão.
    4. Uma recuperação num ponto do tempo de teste.
    5. O monitoramento do arquivamento (atraso e falhas).
  • Automatizar o teste de restauração

    Quero automatizar a restauração e a verificação dos meus backups.
    
    Pergunte qual banco, ferramenta de backup, linguagem e agendador eu uso.
    
    Me ajude a montar um job que:
    1. Cria um servidor descartável.
    2. Restaura o backup mais recente, com as definições globais.
    3. Roda um verificador: tabelas esperadas, contagens mínimas, último registro dentro do RPO.
    4. Mede o tempo total (o RTO real).
    5. Alerta se algo falhar e destrói o servidor no fim.
  • Revisar uma estratégia de backup

    Vou colar abaixo a configuração de backup do meu sistema (ferramentas, agendamentos, destinos e retenção). Revise.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. O RPO e o RTO estão definidos e a técnica os atende?
    2. Há cópia fora da região e da conta, imutável?
    3. Usuários, permissões e configurações estão no backup?
    4. Os backups são criptografados, com a chave em outro lugar?
    5. Há restauração automática com verificação?
    6. A retenção cobre erros descobertos tarde e respeita os prazos legais?
    
    Para cada problema, descreva o incidente em que ele apareceria e mostre a correção.
    
    Configuração:
    [cole aqui]