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:
- As tabelas esperadas existem.
- Cada uma tem um mínimo de linhas.
- 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`));import os
from datetime import datetime, timedelta, timezone
import psycopg
from psycopg import sql
ESPERADO = [("clientes", 1), ("pedidos", 1000)] # tabela e mínimo de linhas
RPO = timedelta(hours=24) # quanto de dados o negócio aceita perder
def verificar_restauracao(url: str, agora: datetime | None = None) -> list[str]:
"""Confere um banco restaurado; devolve a lista de problemas (vazia = restauração aceita)."""
agora = agora or datetime.now(timezone.utc)
problemas: list[str] = []
with psycopg.connect(url) as banco:
for tabela, minimo in ESPERADO:
(existe,) = banco.execute("SELECT to_regclass(%s) IS NOT NULL", (tabela,)).fetchone()
if not existe:
problemas.append(f"tabela {tabela} ausente")
continue
# sql.Identifier põe as aspas no nome da tabela com segurança
consulta = sql.SQL("SELECT count(*) FROM {}").format(sql.Identifier(tabela))
(n,) = banco.execute(consulta).fetchone()
if n < minimo:
problemas.append(f"{tabela}: {n} linhas")
if not problemas:
(ultimo,) = banco.execute("SELECT max(criado_em) FROM pedidos").fetchone()
atraso = agora - ultimo
if atraso > RPO:
horas = atraso.total_seconds() / 3600
problemas.append(f"último pedido de {horas:.0f} h atrás, acima do RPO de 24 h")
return problemas
base = os.environ["URL_RESTAURADO"]
print("restauração recente:", verificar_restauracao(f"{base}/loja"))
daqui_2_dias = datetime.now(timezone.utc) + timedelta(days=2)
print("vista daqui a 2 dias:", verificar_restauracao(f"{base}/loja", daqui_2_dias))
print("banco incompleto:", verificar_restauracao(f"{base}/incompleta"))package main
import (
"context"
"fmt"
"os"
"time"
"github.com/jackc/pgx/v5"
)
var esperado = []struct {
tabela string
minimo int64
}{{"clientes", 1}, {"pedidos", 1000}}
const rpo = 24 * time.Hour // quanto de dados o negócio aceita perder
// verificarRestauracao confere um banco restaurado; lista vazia = restauração aceita
func verificarRestauracao(ctx context.Context, url string, agora time.Time) ([]string, error) {
banco, err := pgx.Connect(ctx, url)
if err != nil {
return nil, err
}
defer banco.Close(ctx)
problemas := []string{}
for _, e := range esperado {
var existe bool
err := banco.QueryRow(ctx, "SELECT to_regclass($1) IS NOT NULL", e.tabela).Scan(&existe)
if err != nil {
return nil, err
}
if !existe {
problemas = append(problemas, fmt.Sprintf("tabela %s ausente", e.tabela))
continue
}
// pgx.Identifier põe as aspas no nome da tabela com segurança
var n int64
consulta := "SELECT count(*) FROM " + pgx.Identifier{e.tabela}.Sanitize()
if err := banco.QueryRow(ctx, consulta).Scan(&n); err != nil {
return nil, err
}
if n < e.minimo {
problemas = append(problemas, fmt.Sprintf("%s: %d linhas", e.tabela, n))
}
}
if len(problemas) == 0 {
var ultimo time.Time
if err := banco.QueryRow(ctx, "SELECT max(criado_em) FROM pedidos").Scan(&ultimo); err != nil {
return nil, err
}
if atraso := agora.Sub(ultimo); atraso > rpo {
problemas = append(problemas,
fmt.Sprintf("último pedido de %.0f h atrás, acima do RPO de 24 h", atraso.Hours()))
}
}
return problemas, nil
}
func main() {
ctx := context.Background()
base := os.Getenv("URL_RESTAURADO")
r, err := verificarRestauracao(ctx, base+"/loja", time.Now())
fmt.Println("restauração recente:", r, err)
r, err = verificarRestauracao(ctx, base+"/loja", time.Now().Add(48*time.Hour))
fmt.Println("vista daqui a 2 dias:", r, err)
r, err = verificarRestauracao(ctx, base+"/incompleta", time.Now())
fmt.Println("banco incompleto:", r, err)
}import java.sql.DriverManager;
import java.time.Duration;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class Verificar {
// Tabela e mínimo de linhas
static final Map<String, Integer> ESPERADO = Map.of("clientes", 1, "pedidos", 1000);
static final List<String> ORDEM = List.of("clientes", "pedidos");
static final Duration RPO = Duration.ofHours(24); // quanto de dados o negócio aceita perder
// Confere um banco restaurado; devolve a lista de problemas (vazia = restauração aceita)
static List<String> verificarRestauracao(String url, Instant agora) throws Exception {
var problemas = new ArrayList<String>();
try (var banco = DriverManager.getConnection(url)) {
for (var tabela : ORDEM) {
try (var c = banco.prepareStatement("SELECT to_regclass(?) IS NOT NULL")) {
c.setString(1, tabela);
try (var r = c.executeQuery()) {
r.next();
if (!r.getBoolean(1)) {
problemas.add("tabela " + tabela + " ausente");
continue;
}
}
}
// enquoteIdentifier põe as aspas no nome da tabela com segurança
try (var c = banco.createStatement();
var r = c.executeQuery("SELECT count(*) FROM " + c.enquoteIdentifier(tabela, true))) {
r.next();
long n = r.getLong(1);
if (n < ESPERADO.get(tabela)) problemas.add(tabela + ": " + n + " linhas");
}
}
if (problemas.isEmpty()) {
try (var c = banco.createStatement();
var r = c.executeQuery("SELECT max(criado_em) FROM pedidos")) {
r.next();
var ultimo = r.getObject(1, OffsetDateTime.class).toInstant();
var atraso = Duration.between(ultimo, agora);
if (atraso.compareTo(RPO) > 0) {
problemas.add("último pedido de " + atraso.toHours()
+ " h atrás, acima do RPO de 24 h");
}
}
}
}
return problemas;
}
public static void main(String[] args) throws Exception {
var base = System.getenv("JDBC_RESTAURADO");
var loja = base + "/loja?user=postgres&password=x";
var incompleta = base + "/incompleta?user=postgres&password=x";
var daqui2Dias = Instant.now().plus(Duration.ofDays(2));
System.out.println("restauração recente: " + verificarRestauracao(loja, Instant.now()));
System.out.println("vista daqui a 2 dias: " + verificarRestauracao(loja, daqui2Dias));
System.out.println("banco incompleto: " + verificarRestauracao(incompleta, Instant.now()));
}
}using Npgsql;
(string Tabela, long Minimo)[] esperado = [("clientes", 1), ("pedidos", 1000)];
var rpo = TimeSpan.FromHours(24); // quanto de dados o negócio aceita perder
// Confere um banco restaurado; devolve a lista de problemas (vazia = restauração aceita)
async Task<List<string>> VerificarRestauracao(string url, DateTimeOffset agora)
{
var problemas = new List<string>();
await using var banco = new NpgsqlConnection(url);
await banco.OpenAsync();
foreach (var (tabela, minimo) in esperado)
{
await using var existe = new NpgsqlCommand("SELECT to_regclass($1) IS NOT NULL", banco);
existe.Parameters.Add(new NpgsqlParameter { Value = tabela });
if (!(bool)(await existe.ExecuteScalarAsync())!)
{
problemas.Add($"tabela {tabela} ausente");
continue;
}
// O nome vem da lista acima, nunca do usuário; as aspas protegem o identificador
var identificador = "\"" + tabela.Replace("\"", "\"\"") + "\"";
await using var contar = new NpgsqlCommand($"SELECT count(*) FROM {identificador}", banco);
var n = (long)(await contar.ExecuteScalarAsync())!;
if (n < minimo) problemas.Add($"{tabela}: {n} linhas");
}
if (problemas.Count == 0)
{
await using var ultimo = new NpgsqlCommand("SELECT max(criado_em) FROM pedidos", banco);
var atraso = agora - (DateTime)(await ultimo.ExecuteScalarAsync())!;
if (atraso > rpo)
problemas.Add($"último pedido de {atraso.TotalHours:F0} h atrás, acima do RPO de 24 h");
}
return problemas;
}
var base_ = Environment.GetEnvironmentVariable("URL_RESTAURADO")!;
var (loja, incompleta) = (base_ + ";Database=loja", base_ + ";Database=incompleta");
var agora = DateTimeOffset.UtcNow;
Console.WriteLine($"restauração recente: [{string.Join("; ", await VerificarRestauracao(loja, agora))}]");
var daqui2Dias = await VerificarRestauracao(loja, agora.AddDays(2));
Console.WriteLine($"vista daqui a 2 dias: [{string.Join("; ", daqui2Dias)}]");
var falta = await VerificarRestauracao(incompleta, agora);
Console.WriteLine($"banco incompleto: [{string.Join("; ", falta)}]");O resultado, nas cinco linguagens
| Cenário | Problemas encontrados |
|---|---|
| Restauração recente | nenhum |
| A mesma, avaliada daqui a 2 dias | último pedido de 48 h atrás, acima do RPO de 24 h |
| Banco incompleto | tabela 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 | |
|---|---|
| TypeScript | Aspas duplas em volta do nome, que vem de uma lista fixa |
| Python | psycopg.sql.Identifier |
| Go | pgx.Identifier{...}.Sanitize() |
| Java | Statement.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 testeArquivamento 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/baseRecuperaçã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]