Runbook · PostgreSQL

Migração PostgreSQL com Zero Downtime

Runbook operacional das sete estratégias de replicação e migração de PostgreSQL com indisponibilidade zero ou mínima: replicação física com switchover, replicação lógica nativa, pg_createsubscriber, pg_upgrade, pglogical, CDC com Debezium e as ferramentas gerenciadas de AWS, Google Cloud e Azure. Cada etapa traz os comandos, a validação esperada e os erros mais comuns com a respectiva mitigação.

44 etapas
7 estratégias
Erros e mitigação por etapa
PostgreSQL 10 → 18
Como usar este runbook
01Escolha a estratégia na matriz da Fase 00. Execute sempre a Fase 01 (preparação), depois apenas a fase da estratégia escolhida, e encerre com a Fase 09 (cutover, validação e rollback).
02Ensaie o runbook inteiro em homologação, com volume representativo, e cronometre o intervalo entre PAUSE e RESUME. Esse é o downtime real que você vai comunicar.
03Congele deploys com DDL durante todo o período de replicação. A maioria dos incidentes em migração lógica vem de alteração de schema não coordenada.
04Defina critérios de abort em números antes da janela (lag, tempo de PAUSE, divergência de validação) e quem decide. Durante a janela não se negocia critério.
05Os comandos usam as variáveis da etapa "Convenções" ($OLD, $NEW, $PGB, $DB). Blocos marcados ORIGEM/DESTINO indicam onde executar.
00

Decisão da Estratégia

Escolher o caminho certo antes de digitar o primeiro comando

Matriz de decisãoBásico
Versão, plataforma, tamanho e tolerância a indisponibilidade

A estratégia é definida por quatro perguntas: a versão major muda? A plataforma muda (on-prem → cloud gerenciada)? Qual o tamanho do banco e a taxa de escrita? Qual a janela máxima de indisponibilidade aceita pelo negócio?

CenárioEstratégiaDowntime típicoFase
Mesma versão major; troca de hardware, storage, DC ou SOA · Física + switchover (manual ou Patroni)5–30 s02
Upgrade major, banco pequeno/médio (até ~2 TB)B · Replicação lógica nativa + PgBouncer10–60 s03
Upgrade major, banco de vários TB, origem PG 17+C · pg_createsubscriber + pg_upgrade10–60 s04
Upgrade major, janela de minutos aceitávelD · pg_upgrade --link / --swap1–10 min05
Origem legada 9.4–9.6E · pglogical10–60 s06
Mudança de schema / destino heterogêneo / fan-outF · CDC com Debezium + Kafkasegundos07
Destino RDS/Aurora, Cloud SQL/AlloyDB ou Azure FlexibleG · Blue/Green ou DMS do provedor< 1 min08
Princípio comumToda estratégia de zero downtime em PostgreSQL segue o mesmo padrão: (1) construir um destino que acompanha continuamente a origem (replicação física, lógica ou CDC); (2) congelar escrita por alguns segundos no pooler; (3) esperar o destino alcançar o último LSN; (4) sincronizar o que não replica (sequências); (5) apontar o tráfego para o destino; (6) manter um caminho de volta (replicação reversa). O que muda entre estratégias é só o mecanismo do passo 1.
💡As fases 01 (preparação) e 09 (cutover, validação e rollback) valem para todas as estratégias. Execute a fase 01, depois apenas a fase da estratégia escolhida, e feche com a fase 09.
Documentação oficial
Convenções usadas neste runbookBásico
Variáveis de ambiente, hosts e nomes

Todos os comandos usam as variáveis abaixo. Exporte-as no shell do operador antes de começar; assim os blocos podem ser copiados sem edição.

bash · variáveis
# Hosts e banco
export OLD=old-primary.internal        # origem (produção atual)
export NEW=new-primary.internal        # destino
export PGB=pgbouncer.internal          # pooler na frente da aplicação
export DB=app                          # banco a migrar
export REPL=replicator                 # usuário de replicação
export APP_ROLE=app_rw                 # role usada pela aplicação

# Diretórios de dados (ajuste para sua distro)
export PGDATA_OLD=/var/lib/postgresql/14/main
export PGDATA_NEW=/var/lib/postgresql/18/main
export BIN_OLD=/usr/lib/postgresql/14/bin
export BIN_NEW=/usr/lib/postgresql/18/bin

# Senhas via ~/.pgpass (nunca em linha de comando)
# formato: host:porta:banco:usuario:senha
chmod 600 ~/.pgpass
⚠️Execute os comandos de administração como o usuário de sistema postgres (ou o dono do diretório de dados). Comandos marcados -- ORIGEM e -- DESTINO indicam onde rodar.
Documentação oficial
01

Preparação Comum

Assessment, rede, parâmetros e pooler — pré-requisitos de qualquer estratégia

Inventário e assessment da origemIntermediário
Tamanho, extensões, tabelas sem PK, LOs, collation e taxa de WAL

O assessment revela, antes da migração, tudo que pode quebrar a replicação ou o cutover. Salve a saída de cada consulta no ticket da mudança.

Comandos
sql · origem
-- ORIGEM: versão, tamanho e bancos
SELECT version();
SELECT datname, pg_size_pretty(pg_database_size(datname)) AS tamanho
FROM pg_database WHERE datallowconn ORDER BY pg_database_size(datname) DESC;

-- 20 maiores tabelas (dimensiona carga inicial)
SELECT relid::regclass AS tabela, pg_size_pretty(pg_total_relation_size(relid)) AS total, n_live_tup
FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 20;

-- Extensões (todas precisam existir no destino, na mesma versão ou superior)
SELECT extname, extversion FROM pg_extension ORDER BY 1;

-- Tabelas SEM chave primária (bloqueiam UPDATE/DELETE na replicação lógica)
SELECT c.oid::regclass AS tabela, c.relreplident
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r','p') AND n.nspname NOT IN ('pg_catalog','information_schema')
  AND NOT EXISTS (SELECT 1 FROM pg_constraint k WHERE k.conrelid = c.oid AND k.contype = 'p');

-- Large objects (não replicam logicamente)
SELECT count(*) AS large_objects FROM pg_largeobject_metadata;

-- Unlogged (u), matviews (m) e foreign tables (f) — não replicam logicamente
SELECT c.relkind, c.relpersistence, count(*)
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname NOT IN ('pg_catalog','information_schema') AND c.relkind IN ('r','p','m','f')
GROUP BY 1,2;

-- Collation (PG15+: compara versão registrada com a da biblioteca atual)
SELECT datname, datcollate, datctype, datcollversion,
       pg_database_collation_actual_version(oid) AS versao_atual
FROM pg_database WHERE datallowconn;

-- Transações longas (atrasam criação de slots e snapshots)
SELECT pid, usename, now() - xact_start AS duracao, state, left(query, 80)
FROM pg_stat_activity WHERE xact_start < now() - interval '5 minutes' ORDER BY xact_start;
bash · origem
# Taxa de geração de WAL (amostra de 5 min extrapolada para 1 h)
L1=$(psql -h $OLD -d $DB -Atc "SELECT pg_current_wal_lsn()")
sleep 300
psql -h $OLD -d $DB -Atc "SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '$L1') * 12) AS wal_por_hora"

# Versão da glibc (mudança de glibc 2.28+ altera ordenação de texto)
ldd --version | head -1
Por que medir o WALA taxa de WAL por hora é o número que dimensiona tudo: o espaço em disco que um slot parado vai reter (max_slot_wal_keep_size), a banda necessária entre origem e destino e o tempo que o destino leva para alcançar a origem depois da carga inicial. Se o link entre os ambientes não suporta com folga a taxa de pico, nenhuma estratégia baseada em streaming vai convergir.
Erros possíveis e mitigação
Erroextension "xyz" is not available
CausaExtensão instalada na origem não existe (ou não na versão) no destino — comum em serviços gerenciados.
MitigaçãoCompare com SELECT name, default_version FROM pg_available_extensions no destino. Substitua ou remova a extensão antes da migração; em cloud, consulte a lista de extensões suportadas.
ErroWARNING: database "app" has a collation version mismatch
CausaDestino com glibc/ICU diferente: índices sobre texto podem ficar com ordenação inconsistente (corrupção silenciosa).
MitigaçãoEm migração física, use o mesmo SO/glibc. Em migração lógica, os índices são reconstruídos no destino (seguro). Após qualquer mudança de biblioteca: REINDEX dos índices de texto e ALTER DATABASE app REFRESH COLLATION VERSION.
ErroTabelas sem PK listadas no inventário
CausaUPDATE/DELETE nessas tabelas falharão na origem assim que entrarem em uma publicação lógica sem replica identity.
MitigaçãoCrie a PK (preferível) ou defina REPLICA IDENTITY FULL / USING INDEX antes de criar a publicação (etapa B2).
Documentação oficial
Usuário de replicação, pg_hba e redeBásico
Autenticação SCRAM, TLS e teste de conectividade

Crie um usuário dedicado à replicação, com o mínimo de privilégios, e libere o acesso apenas a partir do IP do destino.

sql · origem
-- ORIGEM
CREATE ROLE replicator WITH LOGIN REPLICATION PASSWORD 'troque-esta-senha';
-- Necessário para a cópia inicial da replicação lógica
GRANT USAGE ON SCHEMA public TO replicator;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO replicator;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO replicator;
pg_hba.conf
# pg_hba.conf da ORIGEM — física usa a pseudo-base "replication"; lógica conecta no banco
hostssl  replication  replicator  10.0.2.0/24  scram-sha-256
hostssl  app          replicator  10.0.2.0/24  scram-sha-256
bash · destino
psql -h $OLD -U postgres -c "SELECT pg_reload_conf();"

# Teste de conexão física e lógica a partir do DESTINO
psql "host=$OLD user=$REPL replication=true sslmode=require" -c "IDENTIFY_SYSTEM;"
psql "host=$OLD user=$REPL dbname=$DB replication=database sslmode=require" -c "IDENTIFY_SYSTEM;"
Erros possíveis e mitigação
Errono pg_hba.conf entry for replication connection from host ...
CausaFalta a linha com a pseudo-base replication (física) ou a linha com o nome do banco (lógica).
MitigaçãoAdicione as duas linhas e rode pg_reload_conf(); confira em SELECT * FROM pg_hba_file_rules WHERE error IS NOT NULL.
Erropassword authentication failed / SCRAM authentication requires libpq version 10
CausaSenha gravada em md5 enquanto o hba exige SCRAM, ou cliente antigo.
MitigaçãoSET password_encryption = 'scram-sha-256' e redefina a senha com \password replicator; atualize a libpq do destino.
Erroconnection timed out
CausaFirewall/Security Group bloqueando a porta 5432 entre os ambientes.
MitigaçãoLibere a porta no sentido destino → origem (e origem → destino se houver replicação reversa). Teste com pg_isready -h $OLD.
Documentação oficial
Parâmetros de servidor (origem e destino)Intermediário
wal_level, slots, senders e proteção de disco

A replicação lógica exige wal_level = logical, que só vale após restart. Agende esse restart antes da janela de migração, pois é a única parada obrigatória fora do cutover.

sql
-- ORIGEM
ALTER SYSTEM SET wal_level = 'logical';            -- restart
ALTER SYSTEM SET max_replication_slots = 20;       -- restart
ALTER SYSTEM SET max_wal_senders = 20;             -- restart
ALTER SYSTEM SET wal_log_hints = on;               -- restart; necessário para pg_rewind
ALTER SYSTEM SET max_slot_wal_keep_size = '200GB'; -- PG13+; protege o disco
ALTER SYSTEM SET wal_keep_size = '4GB';            -- PG13+
ALTER SYSTEM SET idle_in_transaction_session_timeout = '10min';

-- DESTINO (também precisa de wal_level=logical para a replicação reversa)
ALTER SYSTEM SET wal_level = 'logical';
ALTER SYSTEM SET max_replication_slots = 20;
ALTER SYSTEM SET max_wal_senders = 20;
ALTER SYSTEM SET max_worker_processes = 32;
ALTER SYSTEM SET max_logical_replication_workers = 16;
ALTER SYSTEM SET max_sync_workers_per_subscription = 6;         -- tabelas copiadas em paralelo
ALTER SYSTEM SET max_parallel_apply_workers_per_subscription = 4; -- PG16+

-- Parâmetros pendentes de restart
SELECT name, setting, pending_restart FROM pg_settings WHERE pending_restart;
bash
sudo systemctl restart postgresql@14-main   # origem, na janela combinada
psql -h $OLD -Atc "SHOW wal_level;"          # esperado: logical
⚠️Em RDS/Aurora o equivalente é rds.logical_replication = 1 no parameter group; no Cloud SQL, as flags cloudsql.logical_decoding e cloudsql.enable_pglogical; no Azure Flexible, wal_level = logical nos server parameters. Todos exigem reboot.
Erros possíveis e mitigação
Errological decoding requires wal_level >= logical
CausaParâmetro alterado mas o servidor não foi reiniciado (ou foi alterado só na origem e a replicação reversa roda no destino).
MitigaçãoReinicie e confirme com SHOW wal_level nos dois lados antes da janela.
Erroall replication slots are in use
Causamax_replication_slots insuficiente — cada subscription usa 1 slot permanente mais slots temporários durante a cópia inicial.
MitigaçãoDimensione: subscriptions + max_sync_workers_per_subscription + standbys + folga. Exige restart.
Errocould not write to file "pg_wal/...": No space left on device
CausaSlot inativo retendo WAL sem limite.
Mitigaçãomax_slot_wal_keep_size invalida o slot antes de lotar o disco (custo: recriar a replicação). Monitore wal_status em pg_replication_slots.
Documentação oficial
Pooler (PgBouncer) preparado para o cutoverIntermediário
PAUSE/RESUME: congelar escrita sem derrubar conexões

O PgBouncer é o que transforma o cutover em segundos: PAUSE espera as transações em andamento terminarem e segura as novas; após repontar o host, RESUME libera a fila. A aplicação vê apenas latência.

pgbouncer.ini
; /etc/pgbouncer/pgbouncer.ini
[databases]
app = host=old-primary.internal port=5432 dbname=app

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
pool_mode = transaction        ; PAUSE em modo session espera clientes desconectarem
admin_users = pgbouncer_admin
query_wait_timeout = 120       ; tempo máximo que um cliente espera na fila (cobre o cutover)
server_reset_query = DISCARD ALL
server_lifetime = 3600
bash · pgbouncer
# Ensaio (homologação): medir quanto tempo o PAUSE leva
time psql -h $PGB -p 6432 -U pgbouncer_admin pgbouncer -c "PAUSE app;"
psql -h $PGB -p 6432 -U pgbouncer_admin pgbouncer -c "SHOW POOLS;"
psql -h $PGB -p 6432 -U pgbouncer_admin pgbouncer -c "RESUME app;"
Sem PgBouncerSem pooler, o cutover depende de DNS com TTL baixo (30–60 s, reduzido dias antes) ou de VIP/HAProxy, e a aplicação recebe erros de conexão até reconectar. Nesse caso, o congelamento de escrita é feito na origem (etapa 09 · fencing) e o downtime percebido cresce para o tempo de reconexão dos pools da aplicação.
Erros possíveis e mitigação
ErroPAUSE não retorna (fica esperando)
CausaTransação longa ou cliente idle in transaction segurando uma conexão de servidor; ou pool_mode = session.
MitigaçãoAntes do PAUSE, liste e encerre transações longas: SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='app' AND state='idle in transaction' AND xact_start < now()-interval '30s'. Use pool_mode = transaction. Defina um tempo máximo de espera no runbook (ex.: 60 s) e aborte com RESUME.
Erroquery_wait_timeout / clientes recebendo erro durante a janela
CausaJanela congelada maior que query_wait_timeout ou que o timeout do driver da aplicação.
MitigaçãoAjuste query_wait_timeout e os timeouts do pool da aplicação (HikariCP connectionTimeout, etc.) acima da duração ensaiada do cutover.
Erroprepared statement "S_1" does not exist
CausaAplicação usa prepared statements no modo transaction com PgBouncer < 1.21.
MitigaçãoPgBouncer 1.21+ com max_prepared_statements > 0, ou desabilitar server-side prepares no driver (prepareThreshold=0 no JDBC).
Documentação oficial
Baseline de performance e plano de comunicaçãoBásico
Comparar antes e depois; janela, responsáveis e critérios de abort

Capture o comportamento atual para comparar após o cutover e documente quem decide o go/no-go e o rollback.

sql · origem
-- ORIGEM: top 50 queries por tempo total (requer pg_stat_statements)
\copy (SELECT queryid, calls, round(total_exec_time) AS total_ms, round(mean_exec_time::numeric,2) AS media_ms, rows, left(query,200) AS query FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 50) TO 'baseline_origem.csv' CSV HEADER

-- Throughput de referência
SELECT datname, xact_commit, xact_rollback, tup_inserted, tup_updated, tup_deleted, now()
FROM pg_stat_database WHERE datname = 'app';
Janela aprovada, responsáveis (DBA executor, aprovador de negócio, dev on-call) e canal de comunicação
Critérios de abort definidos em números: ex. lag > 10 s no T-0, PAUSE > 60 s, erro de validação em tabela crítica
Snapshot/backup físico da origem antes da janela (pg_basebackup ou snapshot de disco)
Ensaio completo em homologação com volume representativo e tempos cronometrados
Congelamento de deploys com DDL durante todo o período de replicação
Documentação oficial
02

Estratégia A — Replicação Física + Switchover

Mesma versão major: troca de hardware, storage, datacenter ou SO

A1 · Criar o standby com pg_basebackupIntermediário
Cópia binária + streaming com slot físico

O destino recebe uma cópia binária do cluster inteiro e passa a aplicar o WAL da origem em tempo real. Exige mesma versão major, mesma arquitetura de CPU e, idealmente, a mesma versão de glibc.

bash · destino
# DESTINO: diretório vazio, mesmo major da origem
sudo systemctl stop postgresql@14-main
sudo -u postgres rm -rf $PGDATA_NEW/*

sudo -u postgres pg_basebackup -h $OLD -U $REPL -D $PGDATA_NEW \
  -X stream -C -S standby_new -R -P \
  --checkpoint=fast --max-rate=300M
# -C -S : cria o slot físico "standby_new" na origem (evita perder WAL)
# -R    : grava standby.signal e primary_conninfo em postgresql.auto.conf
# --max-rate : limita I/O e rede para não impactar a produção

sudo -u postgres cat $PGDATA_NEW/postgresql.auto.conf   # conferir primary_conninfo/primary_slot_name
sudo systemctl start postgresql@14-main
sql
-- DESTINO: deve retornar true
SELECT pg_is_in_recovery();
Erros possíveis e mitigação
Errorequested WAL segment 0000000100000A2B000000C1 has already been removed
CausaStandby ficou para trás e a origem reciclou o WAL (sem slot ou slot invalidado).
MitigaçãoSempre usar slot (-C -S) ou -X stream. Se o slot aparece com wal_status = 'lost', refaça o basebackup.
Errocould not connect ... FATAL: number of requested standby connections exceeds max_wal_senders
Causamax_wal_senders insuficiente (o -X stream usa 2 conexões).
MitigaçãoAumente max_wal_senders na origem (restart).
ErroBasebackup lento degradando a produção
CausaI/O de leitura e rede saturados.
MitigaçãoUse --max-rate, execute fora do pico, ou restaure de backup (pgBackRest/Barman) e complete via streaming.
ErroÍndices com ordenação incorreta no novo servidor
CausaStandby em SO com glibc diferente (ex.: CentOS 7 → Rocky 9).
MitigaçãoMesma glibc nos dois lados. Se inevitável, prefira a Estratégia B, ou rode REINDEX dos índices de texto após o switchover e valide com a extensão amcheck.
Documentação oficial
A2 · Monitorar o lag da replicação físicaBásico
pg_stat_replication e funções de recovery
sql
-- ORIGEM
SELECT application_name, client_addr, state, sync_state,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS lag_bytes,
       write_lag, flush_lag, replay_lag
FROM pg_stat_replication;

SELECT slot_name, active, wal_status,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retido
FROM pg_replication_slots WHERE slot_type = 'physical';

-- DESTINO
SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn(),
       now() - pg_last_xact_replay_timestamp() AS atraso_replay;
💡Com a origem ociosa, atraso_replay cresce mesmo sem lag real (não há transações novas). Use a diferença de LSN como métrica principal.
Erros possíveis e mitigação
Erroreplay_lag crescendo continuamente
CausaDisco do destino mais lento que o da origem, rede saturada ou query longa no standby bloqueando o replay.
MitigaçãoVerifique pg_stat_activity no standby; ajuste max_standby_streaming_delay; valide IOPS do destino.
Errocanceling statement due to conflict with recovery
CausaConsultas de leitura no standby conflitando com o replay.
MitigaçãoNão use o standby de migração para leitura; se precisar, hot_standby_feedback = on (com cuidado com bloat na origem).
Documentação oficial
A3 · Switchover manual sem perda de dadosAvançado
PAUSE → shutdown limpo → conferência de LSN → promote → RESUME
Por que funcionaUm shutdown limpo do primário (-m fast ou smart) grava um checkpoint de desligamento e espera o walsender enviá-lo aos standbys conectados. Se o standby aplicou esse registro, ele tem exatamente os mesmos dados que o primário. Isso garante zero perda de dados sem depender de replicação síncrona.
bash · runbook T-0
# T-5 min: confirmar lag ~0 (A2) e encerrar transações longas

# T-0 (1) Congelar a aplicação
psql -h $PGB -p 6432 -U pgbouncer_admin pgbouncer -c "PAUSE app;"

# (2) Checkpoint prévio encurta o shutdown
psql -h $OLD -U postgres -c "CHECKPOINT;"

# (3) Parar a origem de forma limpa e impedir restart automático
sudo systemctl stop postgresql@14-main
sudo systemctl disable postgresql@14-main
sudo -u postgres $BIN_OLD/pg_controldata $PGDATA_OLD | grep -E "cluster state|Latest checkpoint location"
#   Database cluster state:  shut down
#   Latest checkpoint location: 2B/C1000028   <-- anote

# (4) No DESTINO, o replay tem que ter passado desse LSN
psql -h $NEW -U postgres -Atc "SELECT pg_last_wal_replay_lsn() >= '2B/C1000028'::pg_lsn;"   # esperado: t

# (5) Promover
psql -h $NEW -U postgres -c "SELECT pg_promote(wait => true, wait_seconds => 60);"
psql -h $NEW -U postgres -Atc "SELECT pg_is_in_recovery();"   # esperado: f

# (6) Repontar o pooler e liberar
sudo sed -i "s/host=$OLD/host=$NEW/" /etc/pgbouncer/pgbouncer.ini
psql -h $PGB -p 6432 -U pgbouncer_admin pgbouncer -c "RELOAD;" -c "RECONNECT app;" -c "RESUME app;"
Erros possíveis e mitigação
Erropg_last_wal_replay_lsn() menor que o Latest checkpoint location
CausaStandby desconectado no momento do shutdown ou lag alto.
MitigaçãoNão promova. Religue a origem (systemctl start), RESUME no pooler e investigue. É o ponto de abort natural.
ErroSplit-brain: aplicação escrevendo nos dois servidores
CausaOrigem reiniciada por automação (systemd, cron, orquestrador) sem standby.signal, ou cliente conectando direto sem passar pelo pooler.
Mitigaçãosystemctl disable na origem, crie standby.signal no diretório antigo imediatamente, remova a linha da aplicação do pg_hba.conf antigo. Localize conexões diretas em pg_stat_activity.client_addr antes da janela.
Erropg_promote retorna false / timeout
CausaReplay ainda processando WAL acumulado.
MitigaçãoAguarde e repita com wait_seconds maior; verifique o log do destino.
ErroAplicação lenta após o switchover
CausaCache frio no novo servidor.
MitigaçãoPré-aqueça as tabelas quentes antes do T-0: SELECT pg_prewarm('public.pedidos') (extensão pg_prewarm), ou use autoprewarm.
Documentação oficial
A4 · Switchover com PatroniIntermediário
Quando o cluster já é gerenciado por Patroni

Adicione o novo servidor como réplica do cluster Patroni (mesmo scope no patroni.yml). O Patroni cria o standby, e o switchover vira um comando.

bash · patroni
patronictl -c /etc/patroni/patroni.yml list
# + Cluster: pg-app ----+---------+-----------+----+-----------+
# | Member  | Host       | Role    | State     | TL | Lag in MB |
# | pg-old  | 10.0.1.10  | Leader  | running   |  7 |           |
# | pg-new  | 10.0.2.10  | Replica | streaming |  7 |         0 |

# Congelar a aplicação é opcional com Patroni + HAProxy, mas reduz erros visíveis
patronictl -c /etc/patroni/patroni.yml switchover pg-app \
  --leader pg-old --candidate pg-new --scheduled now --force

patronictl -c /etc/patroni/patroni.yml list   # pg-new = Leader; pg-old volta como Replica
Erros possíveis e mitigação
Errocandidate pg-new is lagging / not healthy
CausaLag acima de maximum_lag_on_failover ou tag nofailover: true no nó.
MitigaçãoEspere o lag zerar; revise as tags em patroni.yml e o patronictl show-config.
ErroDCS is not accessible
Causaetcd/Consul/ZooKeeper indisponível.
MitigaçãoNão execute o switchover sem DCS saudável; verifique etcdctl endpoint health.
ErroOld leader falha ao voltar como réplica
CausaTimelines divergentes e use_pg_rewind desabilitado.
MitigaçãoHabilite use_pg_rewind: true e wal_log_hints/checksums; em último caso patronictl reinit pg-app pg-old.
Documentação oficial
A5 · Rollback com pg_rewindAvançado
Transformar a origem antiga em standby do novo primário

Após o switchover, a antiga origem pode voltar como standby do novo primário. Assim o rollback vira outro switchover de segundos.

bash · rollback
# ANTIGA ORIGEM (parada)
sudo -u postgres $BIN_OLD/pg_rewind \
  --target-pgdata=$PGDATA_OLD \
  --source-server="host=$NEW port=5432 user=postgres dbname=postgres" \
  --write-recovery-conf --progress

# Ajustar slot (criar no novo primário e referenciar)
psql -h $NEW -U postgres -c "SELECT pg_create_physical_replication_slot('standby_old');"
echo "primary_slot_name = 'standby_old'" | sudo -u postgres tee -a $PGDATA_OLD/postgresql.auto.conf

sudo systemctl enable --now postgresql@14-main
psql -h $NEW -U postgres -c "SELECT application_name, state FROM pg_stat_replication;"
Erros possíveis e mitigação
Errotarget server needs to use either data checksums or "wal_log_hints = on"
CausaPré-requisito do pg_rewind ausente na origem antiga.
MitigaçãoRefaça como standby com pg_basebackup (A1). Para o futuro: wal_log_hints = on.
Errocould not find previous WAL record / required WAL missing
CausaO novo primário já reciclou o WAL necessário.
MitigaçãoConfigure restore_command apontando para o arquivo de WAL, ou refaça via basebackup.
Documentação oficial
03

Estratégia B — Replicação Lógica Nativa

Upgrade major e migração para cloud com publication/subscription (origem PG 10+)

B1 · Schema, roles e objetos no destinoIntermediário
pg_dump --schema-only dividido em pre-data e post-data

A replicação lógica não replica DDL: o schema precisa existir no destino. Para acelerar a carga inicial, aplique as tabelas e só as PKs/UNIQUE antes da cópia. Índices secundários, FKs e triggers entram depois.

bash
# Roles/globais (senhas não são exportáveis em RDS: use --no-role-passwords e redefina)
pg_dumpall -h $OLD -U postgres --roles-only --no-role-passwords -f roles.sql
psql -h $NEW -U postgres -v ON_ERROR_STOP=1 -f roles.sql

# Schema dividido por seção
pg_dump -h $OLD -U postgres -d $DB --schema-only --section=pre-data  -f pre.sql
pg_dump -h $OLD -U postgres -d $DB --schema-only --section=post-data -f post.sql

createdb -h $NEW -U postgres -O app_owner -T template0 -E UTF8 $DB
psql -h $NEW -U postgres -d $DB -v ON_ERROR_STOP=1 -f pre.sql
python
# split_post.py — separa PK/UNIQUE (antes da carga) do restante (depois da carga)
import re
sql = open('post.sql').read()
stmts = [s.strip() + ';' for s in sql.split(';\n') if s.strip() and not s.strip().startswith('--')]
keys = [s for s in stmts if re.search(r'ADD CONSTRAINT \S+ (PRIMARY KEY|UNIQUE) ', s)]
rest = [s for s in stmts if s not in keys]
open('post_keys.sql', 'w').write('\n\n'.join(keys) + '\n')
open('post_rest.sql', 'w').write('\n\n'.join(rest) + '\n')
print(len(keys), 'chaves |', len(rest), 'demais objetos')
bash
python3 split_post.py
psql -h $NEW -U postgres -d $DB -v ON_ERROR_STOP=1 -f post_keys.sql
# post_rest.sql (índices, FKs, triggers, policies) será aplicado na etapa B4
⚠️Revise post_rest.sql antes de aplicar: troque CREATE INDEX por CREATE INDEX CONCURRENTLY se o destino já estiver recebendo leitura, e confira triggers que não devem existir no destino durante a replicação.
Erros possíveis e mitigação
Errorole "app_owner" does not exist
CausaGlobais não migrados antes do schema.
MitigaçãoAplique roles.sql primeiro; em cloud, remova atributos proibidos (SUPERUSER, REPLICATION) do arquivo.
Errocould not open extension control file ... postgis.control
CausaPacote da extensão não instalado para a nova versão major.
MitigaçãoInstale o pacote (postgresql-18-postgis-3, etc.) antes de aplicar pre.sql.
Erropg_dump: error: aborting because of server version mismatch
Causapg_dump mais antigo que o servidor de origem.
MitigaçãoUse sempre o pg_dump da versão mais nova (a do destino): $BIN_NEW/pg_dump.
Documentação oficial
B2 · Replica identity e publicação na origemIntermediário
Garantir que UPDATE/DELETE continuem funcionando na produção
⛔Uma tabela sem PK e sem replica identity, ao entrar em uma publicação que publica UPDATE/DELETE, faz a aplicação de produção falhar com cannot update table ... because it does not have a replica identity. Resolva todas as tabelas do inventário antes do CREATE PUBLICATION.
sql · origem
-- ORIGEM: tabelas sem PK
ALTER TABLE public.log_eventos ADD PRIMARY KEY (id);                 -- ideal (bloqueia a tabela durante a validação)
ALTER TABLE public.auditoria   REPLICA IDENTITY USING INDEX auditoria_uk; -- índice UNIQUE sobre colunas NOT NULL
ALTER TABLE public.staging_raw REPLICA IDENTITY FULL;               -- último recurso: UPDATE/DELETE fazem seq scan no destino

-- Publicação (requer superuser para FOR ALL TABLES)
CREATE PUBLICATION mig_pub FOR ALL TABLES;
-- PG15+: por schema
-- CREATE PUBLICATION mig_pub FOR TABLES IN SCHEMA public, vendas;
-- Tabelas particionadas com estrutura diferente no destino:
-- CREATE PUBLICATION mig_pub FOR ALL TABLES WITH (publish_via_partition_root = true);

SELECT schemaname, tablename FROM pg_publication_tables WHERE pubname = 'mig_pub' ORDER BY 1,2;
Replica identityA replica identity define quais colunas vão no WAL para identificar a linha em UPDATE/DELETE. DEFAULT usa a PK. USING INDEX usa um índice único. FULL grava a linha inteira: o WAL fica maior e, no subscriber, cada UPDATE vira uma busca por todas as colunas. A partir do PG16 o subscriber pode usar qualquer índice B-tree disponível para essa busca.
Erros possíveis e mitigação
Errocannot update table "x" because it does not have a replica identity and publishes updates
CausaTabela sem PK entrou na publicação (inclusive tabela criada depois, com FOR ALL TABLES).
MitigaçãoImediato: ALTER TABLE x REPLICA IDENTITY FULL (resolve na hora). Definitivo: criar PK. Previna com revisão de DDL durante a migração.
Erromust be superuser to create FOR ALL TABLES publication
CausaUsuário sem superuser (comum em cloud).
MitigaçãoUse FOR TABLE t1, t2, ... como owner das tabelas, ou o role administrativo do provedor (rds_superuser, azure_pg_admin).
Documentação oficial
B3 · Subscription e carga inicialAvançado
copy_data, workers paralelos e acompanhamento por tabela
sql · destino
-- DESTINO
CREATE SUBSCRIPTION mig_sub
  CONNECTION 'host=old-primary.internal port=5432 dbname=app user=replicator sslmode=require'
  PUBLICATION mig_pub
  WITH (copy_data = true,
        slot_name = 'mig_sub',
        binary = true,            -- PG14+; cópia inicial binária a partir do PG16
        streaming = parallel,     -- PG16+ (em PG14/15 use 'on')
        disable_on_error = true); -- PG15+: para em erro em vez de entrar em loop

-- Progresso por tabela: i=init, d=copiando, f=copiado, s=sincronizando, r=pronto
SELECT srsubstate, count(*) FROM pg_subscription_rel GROUP BY 1;
SELECT srrelid::regclass, srsubstate FROM pg_subscription_rel WHERE srsubstate <> 'r';

-- COPY em andamento (PG14+)
SELECT relid::regclass, bytes_processed, bytes_total, tuples_processed FROM pg_stat_progress_copy;

-- Erros acumulados (PG15+; PG18 inclui contadores de conflito)
SELECT * FROM pg_stat_subscription_stats;

Para bancos grandes ou origem muito carregada, carregue em lotes: a publicação começa com parte das tabelas e cresce aos poucos. Cada lote segura um snapshot só pelo tempo da sua própria cópia.

sql · lotes
-- ORIGEM: publicação inicial parcial
CREATE PUBLICATION mig_pub FOR TABLE public.clientes, public.produtos;
-- ... após o lote ficar 'r' no destino:
ALTER PUBLICATION mig_pub ADD TABLE public.pedidos, public.itens_pedido;

-- DESTINO: sincroniza apenas as tabelas novas
ALTER SUBSCRIPTION mig_sub REFRESH PUBLICATION WITH (copy_data = true);
Erros possíveis e mitigação
ErroCREATE SUBSCRIPTION não retorna
CausaA criação do slot lógico espera terminarem todas as transações abertas na origem.
MitigaçãoNa origem: SELECT pid, now()-xact_start, query FROM pg_stat_activity WHERE backend_xid IS NOT NULL OR backend_xmin IS NOT NULL ORDER BY xact_start — encerre a transação antiga.
Erroduplicate key value violates unique constraint "pedidos_pkey"
CausaTabela de destino não estava vazia (execução anterior, carga manual).
MitigaçãoCom disable_on_error a subscription desabilita. Para recomeçar: DROP SUBSCRIPTION, TRUNCATE das tabelas de destino e recriar. Para pular uma transação conflitante (PG15+): ALTER SUBSCRIPTION mig_sub SKIP (lsn = '0/14C0378') com o LSN do log, sabendo que ela será perdida.
Erroout of logical replication worker slots
Causamax_logical_replication_workers/max_worker_processes insuficientes no destino.
MitigaçãoAumente os dois parâmetros (restart) — são 1 apply worker + N sync workers por subscription.
ErroBloat e lentidão na origem durante a carga
CausaO snapshot de cada tabela copiada segura o xmin global; o autovacuum não limpa tuplas mortas até a cópia acabar.
MitigaçãoCarga em lotes, fora do pico; para vários TB, use a Estratégia C (sem snapshot longo).
Erroreplication slot "mig_sub" ... wal_status = lost
CausaSlot ultrapassou max_slot_wal_keep_size (subscription parada tempo demais).
MitigaçãoA replicação não é recuperável: DROP SUBSCRIPTION, TRUNCATE no destino e recarga completa. Monitore o lag para agir antes.
Documentação oficial
B4 · Pós-carga: índices, FKs e estatísticasIntermediário
Aplicar post_rest.sql e preparar o destino para o tráfego real
bash · destino
# Somente após todas as tabelas em 'r'
psql -h $NEW -d $DB -Atc "SELECT count(*) FROM pg_subscription_rel WHERE srsubstate <> 'r';"   # esperado: 0

# Índices e constraints restantes (em paralelo por arquivo, se desejar)
psql -h $NEW -U postgres -d $DB -v ON_ERROR_STOP=1 -f post_rest.sql

# Estatísticas
vacuumdb -h $NEW -U postgres -d $DB --analyze-only --jobs=8
sql · destino
-- DESTINO: índices inválidos (CONCURRENTLY interrompido)
SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid;
-- FKs criadas como NOT VALID podem ser validadas sem lock exclusivo
ALTER TABLE public.itens_pedido VALIDATE CONSTRAINT itens_pedido_pedido_id_fkey;
💡No destino, o apply worker roda com session_replication_role = replica: triggers comuns e as FKs (implementadas como triggers de sistema) não disparam para as mudanças replicadas. Só disparam triggers marcadas ENABLE REPLICA ou ENABLE ALWAYS. Revise triggers que gravam em outras tabelas para não duplicar efeitos.
Erros possíveis e mitigação
Erroindex "idx_x" is invalid
CausaCREATE INDEX CONCURRENTLY falhou (deadlock, cancelamento).
MitigaçãoDROP INDEX CONCURRENTLY idx_x e recrie.
Erroinsert or update on table violates foreign key constraint
CausaDados órfãos na origem (FK desabilitada ou criada NOT VALID lá).
MitigaçãoCrie a FK como NOT VALID no destino e trate os órfãos antes do VALIDATE.
Documentação oficial
B5 · Operação contínua: lag e DDL durante a migraçãoIntermediário
Monitoramento e ordem correta de alterações de schema
sql
-- ORIGEM: lag por slot lógico
SELECT s.slot_name, s.active, s.wal_status,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), s.confirmed_flush_lsn)) AS lag,
       r.write_lag, r.flush_lag, r.replay_lag
FROM pg_replication_slots s
LEFT JOIN pg_stat_replication r ON r.pid = s.active_pid
WHERE s.slot_type = 'logical';

-- DESTINO
SELECT subname, pid, received_lsn, latest_end_lsn, latest_end_time FROM pg_stat_subscription;
Ordem do DDLSe um DDL for inevitável durante a replicação, a regra é: o destino precisa aceitar tudo que a origem envia. Por isso, adicionar coluna/tabela começa no destino e depois vai para a origem; remover começa na origem e depois vai para o destino. Tabela nova na origem exige REFRESH PUBLICATION no destino.
sql · ddl
-- 1. DESTINO primeiro
ALTER TABLE public.clientes ADD COLUMN segmento text;
-- 2. ORIGEM depois
ALTER TABLE public.clientes ADD COLUMN segmento text;

-- Tabela nova: criar nos dois lados e então, no DESTINO:
ALTER SUBSCRIPTION mig_sub REFRESH PUBLICATION;
Erros possíveis e mitigação
Errological replication target relation "public.clientes" is missing replicated column: "segmento"
CausaDDL aplicado primeiro na origem.
MitigaçãoAplique o mesmo ALTER no destino; o apply worker retoma sozinho (ou ALTER SUBSCRIPTION mig_sub ENABLE se estiver com disable_on_error).
Errological replication target relation "public.nova" does not exist
CausaTabela criada na origem e incluída automaticamente pelo FOR ALL TABLES.
MitigaçãoCrie a tabela no destino e execute REFRESH PUBLICATION.
ErroLag alto e constante com CPU 100% em um apply worker
CausaTransação gigante (UPDATE em massa) sendo aplicada serialmente.
Mitigaçãostreaming = parallel (PG16+); evite batches gigantes na origem durante a migração; ajuste logical_decoding_work_mem.
Documentação oficial
B6 · Cutover da replicação lógicaAvançado
Congelar, alcançar o LSN, sequências, replicação reversa, repontar

Sequência cronometrada do T-0. Ensaie em homologação até o tempo entre o PAUSE e o RESUME ficar estável, tipicamente entre 10 e 60 s.

bash · runbook T-0
set -euo pipefail
PGBA="psql -h $PGB -p 6432 -U pgbouncer_admin pgbouncer"

# (1) Congelar escrita: pooler + fencing da role da aplicação na origem
$PGBA -c "PAUSE app;"
psql -h $OLD -U postgres -d $DB -c "ALTER ROLE $APP_ROLE SET default_transaction_read_only = on;"
psql -h $OLD -U postgres -d $DB -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity
  WHERE datname = '$DB' AND usename = '$APP_ROLE';"

# (2) Esperar o subscriber alcançar o LSN atual da origem
LSN=$(psql -h $OLD -U postgres -d $DB -Atc "SELECT pg_current_wal_lsn()")
until [ "$(psql -h $OLD -U postgres -d $DB -Atc \
  "SELECT confirmed_flush_lsn >= '$LSN' FROM pg_replication_slots WHERE slot_name = 'mig_sub'")" = "t" ]; do
  sleep 1
done
echo "Destino alcançou $LSN"

# (3) Sincronizar sequências (script da etapa 09 · Sequências)
psql -h $OLD -U postgres -d $DB -Atq -f gera_setval.sql > seq_sync.sql
psql -h $NEW -U postgres -d $DB -v ON_ERROR_STOP=1 -f seq_sync.sql

# (4) Encerrar o fluxo origem -> destino (mantém o slot para auditoria)
psql -h $NEW -U postgres -d $DB -c "ALTER SUBSCRIPTION mig_sub DISABLE;"

# (5) Replicação reversa ANTES de liberar escrita (caminho de rollback)
psql -h $NEW -U postgres -d $DB -c "CREATE PUBLICATION rev_pub FOR ALL TABLES;"
psql -h $OLD -U postgres -d $DB -c "CREATE SUBSCRIPTION rev_sub
  CONNECTION 'host=$NEW port=5432 dbname=$DB user=$REPL sslmode=require'
  PUBLICATION rev_pub WITH (copy_data = false, origin = none);"   # origin: PG16+

# (6) Repontar e liberar
sudo sed -i "s/host=$OLD/host=$NEW/" /etc/pgbouncer/pgbouncer.ini
$PGBA -c "RELOAD;" -c "RECONNECT app;" -c "RESUME app;"
⚠️O fencing via default_transaction_read_only é aplicado na role da aplicação, não no banco, para não bloquear o apply worker da replicação reversa. É uma proteção contra acidente, não um controle de segurança: quem tem privilégio pode desfazer com SET.
Erros possíveis e mitigação
ErroLoop de espera do LSN não termina
CausaApply worker parado por erro (DDL, conflito) ou transação enorme ainda sendo decodificada.
MitigaçãoVeja o log do destino e pg_stat_subscription_stats. Critério de abort (ex.: 3 min): ALTER ROLE ... RESET default_transaction_read_only na origem e RESUME app. O serviço volta para a origem sem perda.
Erroduplicate key value violates unique constraint "pedidos_pkey" (após o cutover)
CausaSequências/identity não sincronizadas no destino.
MitigaçãoRode o script de sequências de novo, agora do lado certo (valores do destino não podem ser menores que max(id)). Veja a etapa 09 · Sequências.
Errocannot execute INSERT in a read-only transaction
CausaAlgum serviço conecta direto na origem, sem pooler (job, cron, BI).
MitigaçãoIdentifique por client_addr/application_name em pg_stat_activity da origem e reaponte. Mapeie isso antes da janela.
Errocould not create replication slot "rev_sub": logical decoding requires wal_level >= logical
CausaDestino sem wal_level = logical.
MitigaçãoPré-requisito da fase 01. Se esquecido, siga sem replicação reversa (rollback só com perda do delta) ou aborte a janela.
Erroorigin = none não reconhecido
CausaDestino e/ou origem abaixo do PG16.
MitigaçãoSem risco de loop porque mig_sub foi desabilitada no passo (4); crie rev_sub sem a opção origin.
Documentação oficial
B7 · Descomissionamento da replicaçãoBásico
Remover subscriptions, publicações e slots com segurança
sql
-- DESTINO (após o período de rollback, ex.: 7 dias)
DROP SUBSCRIPTION mig_sub;          -- também remove o slot remoto na origem
DROP PUBLICATION rev_pub;

-- ORIGEM antiga
DROP SUBSCRIPTION rev_sub;
DROP PUBLICATION mig_pub;
SELECT slot_name FROM pg_replication_slots;   -- esperado: vazio
Erros possíveis e mitigação
Errocould not connect to the publisher / could not drop replication slot
CausaO outro lado já foi desligado.
MitigaçãoDesassocie o slot antes do drop: ALTER SUBSCRIPTION mig_sub DISABLE; ALTER SUBSCRIPTION mig_sub SET (slot_name = NONE); DROP SUBSCRIPTION mig_sub; e remova manualmente na origem (se ainda existir): SELECT pg_drop_replication_slot('mig_sub');
ErroDisco da origem crescendo após a migração
CausaSlot esquecido retendo WAL.
MitigaçãoSELECT slot_name, active FROM pg_replication_slots e drop dos slots inativos.
Documentação oficial
04

Estratégia C — pg_createsubscriber + pg_upgrade

Bancos de vários TB: cópia física rápida convertida em replicação lógica (PG17+)

C1 · Pré-requisitos e standby físicoIntermediário
Converter um standby existente em vez de copiar via COPY
Como funcionaO pg_createsubscriber (PG17+) pega um standby físico parado e: cria publicações e slots lógicos na origem; define um recovery_target_lsn igual ao ponto de consistência do slot; promove o standby exatamente nesse ponto; cria as subscriptions começando dali, sem copiar dados; e troca o system identifier. A carga inicial vira um pg_basebackup, várias vezes mais rápido que o COPY lógico e sem snapshot longo na origem.
Origem e destino na mesma versão major, 17 ou superior
Destino é standby físico da origem (etapa A1) e está sincronizado
Origem com wal_level = logical e max_replication_slots/max_wal_senders com folga para 1 slot por banco
Destino com max_logical_replication_workers ≥ número de bancos e max_worker_processes compatível
Todas as tabelas com PK ou replica identity (etapa B2), pois o fluxo seguinte é lógico
Snapshot do diretório do standby: se o processo falhar após a promoção, o standby não é reaproveitável
bash · destino
# Standby físico criado como em A1 (mesmo major 17)
sudo -u postgres pg_basebackup -h $OLD -U $REPL -D /var/lib/postgresql/17/main \
  -X stream -C -S standby_conv -R -P --checkpoint=fast
sudo systemctl start postgresql@17-main
# ... aguardar lag ~0 (A2) e então parar
sudo systemctl stop postgresql@17-main
Documentação oficial
C2 · Executar o pg_createsubscriberAvançado
Dry-run primeiro, depois a conversão real
bash · destino
# Simulação: valida todos os pré-requisitos sem alterar nada
sudo -u postgres /usr/lib/postgresql/17/bin/pg_createsubscriber --dry-run --verbose \
  -D /var/lib/postgresql/17/main \
  -P "host=$OLD port=5432 user=postgres dbname=$DB" \
  -d $DB \
  --publication=mig_pub --subscription=mig_sub \
  --subscriber-port=50432 --recovery-timeout=600

# Execução real (mesma linha sem --dry-run)
sudo -u postgres /usr/lib/postgresql/17/bin/pg_createsubscriber --verbose \
  -D /var/lib/postgresql/17/main \
  -P "host=$OLD port=5432 user=postgres dbname=$DB" \
  -d $DB --publication=mig_pub --subscription=mig_sub \
  --subscriber-port=50432 --recovery-timeout=600
# Vários bancos: repita -d/--publication/--subscription (PG18: --all converte todos)

sudo systemctl start postgresql@17-main
sql
-- DESTINO: agora é um primário com subscription ativa
SELECT pg_is_in_recovery();                         -- f
SELECT subname, subenabled FROM pg_subscription;    -- mig_sub | t
SELECT srsubstate, count(*) FROM pg_subscription_rel GROUP BY 1;   -- tudo 'r'

-- ORIGEM: slot lógico ativo; o slot físico do standby não deve mais existir
SELECT slot_name, slot_type, active FROM pg_replication_slots;
Erros possíveis e mitigação
Errostandby server is running / target server must be stopped
CausaO standby não foi parado antes.
Mitigaçãosystemctl stop e repita.
Erroprimary server does not have enough replication slots / wal senders
CausaParâmetros da origem sem folga.
MitigaçãoAumente max_replication_slots/max_wal_senders na origem (restart) e refaça o dry-run.
Errorecovery timed out
CausaStandby não alcançou o LSN de consistência no tempo definido (lag alto).
MitigaçãoAumente --recovery-timeout e garanta lag ~0 antes. Se falhou após a promoção, restaure o snapshot ou refaça o basebackup.
ErroSlot físico standby_conv continua na origem retendo WAL
CausaSlot não removido ao fim do processo.
MitigaçãoNa origem: SELECT pg_drop_replication_slot('standby_conv');
ErroOutros bancos do cluster ficaram sem replicação
CausaSó os bancos passados em -d recebem subscription; os demais ficam congelados no ponto da promoção.
MitigaçãoListe todos os bancos na chamada (ou --all no PG18) ou remova os que não serão migrados.
Documentação oficial
C3 · Upgrade do subscriber com pg_upgradeAvançado
Ex.: 17 → 18 preservando o estado das subscriptions

A partir do PG17, o pg_upgrade preserva as subscriptions e o ponto de replicação de um subscriber. O destino sobe de versão enquanto a origem continua retendo o WAL no slot, e depois o destino retoma de onde parou.

sql
-- DESTINO (17): pré-condições
SELECT srsubstate, count(*) FROM pg_subscription_rel GROUP BY 1;  -- somente 'r' (ou 'i')
ALTER SUBSCRIPTION mig_sub DISABLE;
bash · destino
# Novo cluster 18 com MESMAS opções de locale/encoding/checksums (ver Estratégia D)
# PG18: max_active_replication_origins >= número de subscriptions (padrão 10)
sudo systemctl stop postgresql@17-main
sudo -u postgres $BIN_NEW/pg_upgrade \
  -b /usr/lib/postgresql/17/bin -B $BIN_NEW \
  -d /var/lib/postgresql/17/main -D $PGDATA_NEW \
  -o "-c config_file=/etc/postgresql/17/main/postgresql.conf" \
  -O "-c config_file=/etc/postgresql/18/main/postgresql.conf" \
  --link --jobs=$(nproc) --check
# sem --check para executar; depois:
sudo systemctl start postgresql@18-main
psql -h $NEW -U postgres -d $DB -c "ALTER SUBSCRIPTION mig_sub ENABLE;"

A partir daqui o fluxo é idêntico à Estratégia B: monitoramento (B5), cutover (B6) e descomissionamento (B7).

Erros possíveis e mitigação
Erropg_upgrade: subscription ... has tables not in 'i' or 'r' state
CausaSincronização de tabela em andamento.
MitigaçãoAguarde todas em r antes de desabilitar e atualizar.
Erromax_active_replication_origins / max_replication_slots too low
CausaNovo cluster sem origens suficientes para as subscriptions migradas.
MitigaçãoAjuste no postgresql.conf do cluster novo e rode o --check de novo.
ErroOrigem com disco crescendo durante o upgrade
CausaSlot retendo WAL enquanto o subscriber está parado.
MitigaçãoDimensione o tempo de parada do subscriber × taxa de WAL (fase 01) contra max_slot_wal_keep_size.
Documentação oficial
05

Estratégia D — pg_upgrade In-Place

Upgrade major com minutos de parada, praticamente independente do tamanho

D1 · Instalar binários e criar o cluster novoBásico
Mesmo locale, encoding e configuração de checksums
bash
# Debian/Ubuntu (repositório PGDG) — instale também as extensões usadas na origem
sudo apt install -y postgresql-18 postgresql-18-postgis-3 postgresql-18-pg-stat-kcache

# Conferir configurações da origem
sudo -u postgres $BIN_OLD/pg_controldata $PGDATA_OLD | grep -i checksum   # 0 = desabilitado
psql -U postgres -Atc "SELECT datname, pg_encoding_to_char(encoding), datcollate, datctype FROM pg_database;"

# Criar o cluster novo espelhando a origem
sudo -u postgres $BIN_NEW/initdb -D $PGDATA_NEW \
  --encoding=UTF8 --locale=pt_BR.UTF-8 \
  --no-data-checksums      # PG18 habilita checksums por padrão; espelhe a origem!
⚠️No PG18, o initdb passou a habilitar data checksums por padrão. Se a origem não usa checksums, o pg_upgrade recusa a execução. Crie o cluster novo com --no-data-checksums, ou habilite checksums na origem antes com pg_checksums --enable (com o cluster parado).
Erros possíveis e mitigação
Errolc_collate values for database "postgres" do not match
CausaCluster novo criado com locale diferente.
MitigaçãoRecrie com initdb --locale igual à origem (ou --lc-collate/--lc-ctype). Se o locale mudar de propósito, use a Estratégia B.
Erroold cluster does not use data checksums but the new one does
CausaDefault do PG18.
Mitigaçãoinitdb --no-data-checksums.
Documentação oficial
D2 · pg_upgrade --checkIntermediário
Validação completa com a origem ainda no ar
bash
sudo -u postgres bash -c "cd /tmp && $BIN_NEW/pg_upgrade \
  -b $BIN_OLD -B $BIN_NEW -d $PGDATA_OLD -D $PGDATA_NEW \
  -o '-c config_file=/etc/postgresql/14/main/postgresql.conf' \
  -O '-c config_file=/etc/postgresql/18/main/postgresql.conf' \
  --link --jobs=$(nproc) --check"
# Saída esperada: *Clusters are compatible*
# Em caso de falha, os detalhes ficam em $PGDATA_NEW/pg_upgrade_output.d/
Erros possíveis e mitigação
ErroYour installation references loadable libraries that are missing from the new installation
CausaExtensão sem pacote para a nova versão (lista em loadable_libraries.txt).
MitigaçãoInstale o pacote correspondente ou remova a extensão da origem (DROP EXTENSION) se não for usada.
ErroYour installation contains the "reg*" data types in user tables
CausaColunas regproc/regclass etc. (OIDs mudam no upgrade).
MitigaçãoConverta as colunas para text na origem antes do upgrade.
ErroYour installation contains tables declared WITH OIDS
CausaOrigem < 12 com tabelas WITH OIDS.
MitigaçãoALTER TABLE ... SET WITHOUT OIDS na origem.
ErroThe source cluster contains prepared transactions
CausaPREPARE TRANSACTION pendentes.
MitigaçãoSELECT * FROM pg_prepared_xacts e COMMIT PREPARED/ROLLBACK PREPARED.
Errological replication slot ... has not consumed all the WAL
CausaOrigem 17+ com slots lógicos atrasados (o pg_upgrade migra slots a partir do PG17).
MitigaçãoDeixe os subscribers alcançarem, ou remova os slots que não serão preservados.
Documentação oficial
D3 · Execução do upgradeAvançado
--link (hard links), --clone (reflink) ou --swap (PG18)
bash · janela
# (0) Snapshot/backup: com --link, a origem NÃO pode mais ser iniciada depois que o novo cluster subir
# (1) Parar a aplicação (PgBouncer PAUSE) e o cluster antigo
psql -h $PGB -p 6432 -U pgbouncer_admin pgbouncer -c "PAUSE app;"
sudo systemctl stop postgresql@14-main

# (2) Upgrade
sudo -u postgres bash -c "cd /tmp && $BIN_NEW/pg_upgrade \
  -b $BIN_OLD -B $BIN_NEW -d $PGDATA_OLD -D $PGDATA_NEW \
  -o '-c config_file=/etc/postgresql/14/main/postgresql.conf' \
  -O '-c config_file=/etc/postgresql/18/main/postgresql.conf' \
  --link --jobs=$(nproc)"
#   --clone : XFS/Btrfs com reflink — rápido e a origem continua íntegra
#   --swap  : PG18 — move diretórios; mais rápido que --link em muitos arquivos

# (3) Subir o novo (mesma porta da antiga) e liberar
sudo systemctl start postgresql@18-main
psql -h $PGB -p 6432 -U pgbouncer_admin pgbouncer -c "RESUME app;"
Erros possíveis e mitigação
Errocould not create hard link ... Invalid cross-device link
CausaDiretórios antigo e novo em filesystems diferentes.
MitigaçãoColoque $PGDATA_NEW no mesmo filesystem ou use o modo padrão (--copy), mais lento.
ErroFalha no meio do pg_upgrade --link
CausaErro após o início da transferência de arquivos.
MitigaçãoSe o novo cluster não foi iniciado, a origem ainda pode subir (renomeie global/pg_control.old de volta para global/pg_control, conforme a documentação). Caso contrário, restaure do snapshot do passo 0.
ErroPorta em uso ao subir o cluster 18
CausaCluster 14 configurado para iniciar automaticamente.
Mitigaçãosystemctl disable postgresql@14-main e ajuste a porta no postgresql.conf do 18.
Documentação oficial
D4 · Pós-upgrade: estatísticas, extensões e limpezaIntermediário
Evitar planos ruins nas primeiras horas
bash
# PG18 preserva as estatísticas do otimizador; só completa as ausentes (ex.: estatísticas estendidas)
sudo -u postgres $BIN_NEW/vacuumdb --all --analyze-in-stages --missing-stats-only --jobs=8
# Versões anteriores ao 18: ANALYZE completo em estágios
# sudo -u postgres $BIN_NEW/vacuumdb --all --analyze-in-stages --jobs=8

# Scripts gerados pelo pg_upgrade (no diretório onde foi executado)
sudo -u postgres psql -f /tmp/update_extensions.sql
# Após o período de rollback:
sudo -u postgres /tmp/delete_old_cluster.sh
Erros possíveis e mitigação
ErroQueries lentas logo após o upgrade
CausaEstatísticas ausentes (origem < 18) ou plano diferente no novo otimizador.
Mitigaçãovacuumdb --analyze-in-stages imediatamente; compare com a baseline de pg_stat_statements e investigue regressões com EXPLAIN (ANALYZE, BUFFERS).
Erroextension "x" has version "1.2" but the library is "1.4"
CausaExtensões não atualizadas.
MitigaçãoExecute update_extensions.sql ou ALTER EXTENSION x UPDATE.
Documentação oficial
D5 · Standbys físicos após o upgradeAvançado
rsync com hard links, sem refazer o basebackup

Procedimento documentado no pg_upgrade para atualizar standbys sem copiar os dados de novo. Todos os nós precisam estar parados e o novo primário ainda não iniciado.

bash
# 1. Conferir que o standby aplicou o checkpoint final da origem (valores idênticos)
sudo -u postgres $BIN_OLD/pg_controldata $PGDATA_OLD | grep "Latest checkpoint location"
ssh standby1 "sudo -u postgres $BIN_OLD/pg_controldata $PGDATA_OLD | grep 'Latest checkpoint location'"

# 2. No PRIMÁRIO, a partir do diretório pai dos clusters
cd /var/lib/postgresql
rsync --archive --delete --hard-links --size-only --no-inc-recursive \
  14 18 standby1:/var/lib/postgresql

# 3. No standby: standby.signal + primary_conninfo no cluster 18 e iniciar
ssh standby1 "sudo -u postgres touch /var/lib/postgresql/18/main/standby.signal"
Erros possíveis e mitigação
ErroCheckpoint locations diferentes entre primário e standby
CausaStandby não recebeu o shutdown checkpoint.
MitigaçãoNão use rsync: recrie o standby com pg_basebackup a partir do novo primário.
ErroStandby corrompido após rsync
CausaExecução com o novo primário já iniciado ou fora da ordem.
MitigaçãoRecriar o standby com pg_basebackup.
Documentação oficial
06

Estratégia E — pglogical

Origens legadas 9.4–9.6 ou recursos que a replicação nativa não oferece

E1 · Instalação e configuração dos nósIntermediário
Extensão nos dois lados, provider e subscriber
postgresql.conf
# postgresql.conf — ORIGEM e DESTINO (restart)
wal_level = logical
shared_preload_libraries = 'pglogical'
max_worker_processes = 16
max_replication_slots = 10
max_wal_senders = 10
track_commit_timestamp = on     # necessário para resolução de conflitos last_update_wins
sql
-- ORIGEM (provider)
CREATE EXTENSION pglogical;
SELECT pglogical.create_node(node_name := 'provider',
       dsn := 'host=old-primary.internal port=5432 dbname=app user=replicator');
SELECT pglogical.replication_set_add_all_tables('default', ARRAY['public']);
SELECT pglogical.replication_set_add_all_sequences('default', ARRAY['public']);
-- tabelas sem PK: apenas INSERT
SELECT pglogical.replication_set_add_table('default_insert_only', 'public.log_eventos');

-- DESTINO (subscriber) — schema aplicado antes (B1)
CREATE EXTENSION pglogical;
SELECT pglogical.create_node(node_name := 'subscriber',
       dsn := 'host=new-primary.internal port=5432 dbname=app user=replicator');
SELECT pglogical.create_subscription(
       subscription_name := 'mig_sub',
       provider_dsn := 'host=old-primary.internal port=5432 dbname=app user=replicator',
       replication_sets := ARRAY['default','default_insert_only','ddl_sql'],
       synchronize_structure := false,
       synchronize_data := true);
SELECT pglogical.wait_for_subscription_sync_complete('mig_sub');
Erros possíveis e mitigação
Erropglogical is not in shared_preload_libraries
CausaParâmetro ausente ou servidor não reiniciado.
MitigaçãoAjuste e reinicie os dois nós.
Errotable ... cannot be added to replication set ... does not have primary key
CausaTabela sem PK no set default (que replica UPDATE/DELETE).
MitigaçãoCrie a PK ou coloque a tabela em default_insert_only e trate UPDATE/DELETE à parte.
Errosynchronize_structure falha com erro de pg_dump
CausaVersões de pg_dump incompatíveis entre os nós.
MitigaçãoUse synchronize_structure := false e aplique o schema manualmente (B1).
Documentação oficial
E2 · Monitoramento e DDLBásico
Status da subscription e replicação de DDL
sql
-- DESTINO
SELECT subscription_name, status, provider_node, slot_name FROM pglogical.show_subscription_status();
SELECT * FROM pglogical.local_sync_status WHERE sync_status <> 'r';

-- ORIGEM: DDL replicado para os subscribers pelo set ddl_sql
SELECT pglogical.replicate_ddl_command('ALTER TABLE public.clientes ADD COLUMN segmento text');
Erros possíveis e mitigação
Errostatus = down
CausaApply worker falhou (conflito, DDL divergente, conexão).
MitigaçãoVeja o log do destino; corrija e SELECT pglogical.alter_subscription_enable('mig_sub', true).
ErroDivergência após DDL manual
CausaDDL executado direto, fora de replicate_ddl_command.
MitigaçãoPadronize todo DDL via replicate_ddl_command durante a migração.
Documentação oficial
E3 · Cutover e remoçãoIntermediário
Sincronizar sequências e desmontar os nós
sql
-- Com escrita congelada (B6 passos 1–2):
-- ORIGEM: força a sincronização de todas as sequências replicadas
SELECT pglogical.synchronize_sequence(seqoid) FROM pglogical.sequence_state;

-- DESTINO: após o cutover
SELECT pglogical.drop_subscription('mig_sub');
SELECT pglogical.drop_node('subscriber');
DROP EXTENSION pglogical;

-- ORIGEM
SELECT pglogical.drop_node('provider');
💡Os demais passos do cutover (PAUSE, espera do LSN, fencing e reponte do pooler) são os mesmos da etapa B6. Com pglogical, a espera usa confirmed_flush_lsn do slot pgl_app_provider_mig_sub (confira o nome em pg_replication_slots).
Documentação oficial
07

Estratégia F — CDC com Debezium + Kafka

Transformações, schema diferente, destino heterogêneo ou validação longa

F1 · Preparar a origem para o DebeziumIntermediário
Usuário, publicação, heartbeat e tabela de sinais
sql · origem
-- ORIGEM
CREATE ROLE debezium WITH LOGIN REPLICATION PASSWORD 'troque-esta-senha';
GRANT USAGE ON SCHEMA public TO debezium;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO debezium;
CREATE PUBLICATION dbz_pub FOR ALL TABLES;

-- Heartbeat: faz o slot avançar mesmo quando as tabelas capturadas não recebem escrita
CREATE TABLE public.dbz_heartbeat (id int PRIMARY KEY, ts timestamptz);
INSERT INTO public.dbz_heartbeat VALUES (1, now());
GRANT UPDATE ON public.dbz_heartbeat TO debezium;

-- Sinais: snapshots incrementais sob demanda
CREATE TABLE public.dbz_signal (id varchar(42) PRIMARY KEY, type varchar(32) NOT NULL, data varchar(2048));
GRANT INSERT, SELECT ON public.dbz_signal TO debezium;
Documentação oficial
F2 · Source connector (PostgreSQL → Kafka)Avançado
pgoutput, snapshot inicial e heartbeat
json · pg-source.json
{
  "name": "pg-source-app",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "old-primary.internal",
    "database.port": "5432",
    "database.user": "debezium",
    "database.password": "${file:/opt/secrets/pg.properties:password}",
    "database.dbname": "app",
    "topic.prefix": "app",
    "plugin.name": "pgoutput",
    "slot.name": "dbz_app",
    "publication.name": "dbz_pub",
    "publication.autocreate.mode": "disabled",
    "snapshot.mode": "initial",
    "table.include.list": "public.*",
    "heartbeat.interval.ms": "10000",
    "heartbeat.action.query": "UPDATE public.dbz_heartbeat SET ts = now() WHERE id = 1",
    "signal.data.collection": "public.dbz_signal",
    "decimal.handling.mode": "precise",
    "tombstones.on.delete": "true"
  }
}
bash
curl -sS -X POST -H "Content-Type: application/json" --data @pg-source.json http://connect:8083/connectors
curl -sS http://connect:8083/connectors/pg-source-app/status | jq '.connector.state, .tasks[].state'
# Snapshot incremental de uma tabela (sem bloquear a origem)
psql -h $OLD -d $DB -c "INSERT INTO public.dbz_signal VALUES ('snap-pedidos-1','execute-snapshot',
  '{\"data-collections\": [\"public.pedidos\"], \"type\": \"incremental\"}');"
Documentação oficial
F3 · Sink connector (Kafka → PostgreSQL destino)Avançado
Upsert idempotente por chave primária
json · pg-sink.json
{
  "name": "pg-sink-app",
  "config": {
    "connector.class": "io.debezium.connector.jdbc.JdbcSinkConnector",
    "topics.regex": "app\\.public\\.(?!dbz_).*",
    "connection.url": "jdbc:postgresql://new-primary.internal:5432/app",
    "connection.username": "app_writer",
    "connection.password": "${file:/opt/secrets/pg.properties:target_password}",
    "insert.mode": "upsert",
    "primary.key.mode": "record_key",
    "delete.enabled": "true",
    "schema.evolution": "none",
    "tasks.max": "4",
    "transforms": "route",
    "transforms.route.type": "org.apache.kafka.connect.transforms.RegexRouter",
    "transforms.route.regex": "app\\.public\\.(.*)",
    "transforms.route.replacement": "$1"
  }
}
bash
curl -sS -X POST -H "Content-Type: application/json" --data @pg-sink.json http://connect:8083/connectors
# Lag do consumidor (sink) — deve tender a 0
kafka-consumer-groups.sh --bootstrap-server kafka:9092 --describe --group connect-pg-sink-app
Cutover com CDCO cutover com CDC exige duas convergências: o source precisa ter lido até o LSN atual (confirmed_flush_lsn do slot dbz_app ≥ pg_current_wal_lsn()), e o sink precisa ter consumido todos os tópicos (lag do consumer group = 0). Só depois vêm sequências e o reponte do pooler, como em B6.
Erros possíveis e mitigação
ErroDisco da origem enchendo com o banco quase ocioso
CausaSlot só avança quando há eventos nas tabelas capturadas; o WAL de outros bancos ou tabelas fica retido.
Mitigaçãoheartbeat.action.query (configurado em F2) e max_slot_wal_keep_size.
ErroValor __debezium_unavailable_value no destino
CausaColunas TOAST não alteradas não vêm no UPDATE com replica identity DEFAULT.
MitigaçãoALTER TABLE ... REPLICA IDENTITY FULL nas tabelas afetadas, ou tratar o placeholder no sink (ignorar a coluna).
ErroEventos perdidos após indisponibilidade longa do Kafka Connect
CausaRetenção dos tópicos menor que o período parado, ou slot invalidado.
MitigaçãoRetenção ≥ 7 dias nos tópicos; ao perder eventos, novo snapshot incremental das tabelas afetadas via dbz_signal.
ErroSlot dbz_app perdido após failover da origem
CausaSlots lógicos não sobreviviam a failover antes do PG17.
MitigaçãoPG17+: slots de failover (sync_replication_slots no standby) e a opção de failover do conector na sua versão do Debezium; senão, recriar o slot e fazer snapshot.
ErroDiferenças em numeric/timestamp no destino
CausaMapeamento de tipos (decimal.handling.mode, time.precision.mode).
Mitigaçãodecimal.handling.mode=precise e validação por hash por tabela (etapa 09).
Documentação oficial
08

Estratégia G — Ferramentas Gerenciadas

AWS, Google Cloud e Azure: o mesmo princípio com orquestração do provedor

G1 · AWS RDS / Aurora Blue/Green DeploymentsIntermediário
Ambiente green sincronizado e switchover gerenciado

O RDS cria um ambiente green sincronizado com o blue (produção). Em upgrade major, a sincronização é por replicação lógica. No switchover, o RDS congela escrita no blue, espera o green alcançar e troca os endpoints, então a aplicação mantém o mesmo hostname.

bash · aws cli
# 1. Replicação lógica no parameter group do BLUE (reboot)
aws rds modify-db-parameter-group --db-parameter-group-name pg14-blue \
  --parameters "ParameterName=rds.logical_replication,ParameterValue=1,ApplyMethod=pending-reboot"
aws rds reboot-db-instance --db-instance-identifier app-prod

# 2. Criar o green já na versão-alvo
aws rds create-blue-green-deployment \
  --blue-green-deployment-name app-pg-upgrade \
  --source arn:aws:rds:sa-east-1:123456789012:db:app-prod \
  --target-engine-version <versao-alvo> \
  --target-db-parameter-group-name pg-green-params
# Aurora: --source com o ARN do cluster e --target-db-cluster-parameter-group-name

# 3. Acompanhar até AVAILABLE
aws rds describe-blue-green-deployments --blue-green-deployment-identifier bgd-xxxxxxxx \
  --query 'BlueGreenDeployments[0].[Status,SwitchoverDetails]'

# 4. Switchover (timeout em segundos; se estourar, o RDS desfaz e o blue continua)
aws rds switchover-blue-green-deployment \
  --blue-green-deployment-identifier bgd-xxxxxxxx --switchover-timeout 300

# 5. Após validar, remover a relação blue/green (o antigo blue fica como "-old1")
aws rds delete-blue-green-deployment --blue-green-deployment-identifier bgd-xxxxxxxx
Erros possíveis e mitigação
ErroCriação do blue/green falha na validação
Causards.logical_replication desligado, versão sem caminho de upgrade direto, ou recursos não suportados (ex.: certas extensões).
MitigaçãoLeia o evento em describe-events; ajuste o parameter group e reinicie o blue.
ErroReplicação do green com erro após DDL
CausaDDL executado no blue não é replicado em upgrade major (replicação lógica).
MitigaçãoCongele DDL; se ocorrer, aplique manualmente no green ou recrie o blue/green.
ErroSwitchover timed out
CausaLag de replicação ou transações longas no blue.
MitigaçãoO RDS reverte automaticamente. Reduza a carga, encerre transações longas e repita em horário de baixo movimento.
ErroAplicação continua falando com o antigo após o switchover
CausaDNS em cache no cliente (ex.: JVM com TTL infinito).
Mitigaçãonetworkaddress.cache.ttl=5 na JVM; reinício gradual do pool; ou AWS Advanced JDBC Wrapper com plugin de blue/green.
Erroduplicate key em tabelas com serial após o switchover
CausaSequências não sincronizadas pelo mecanismo lógico na sua versão/configuração.
MitigaçãoValide a documentação atual do RDS para sequências e, se necessário, rode o script de sequências da etapa 09 logo após o switchover.
Documentação oficial
G2 · AWS DMS (full load + CDC)Intermediário
Quando a origem não é RDS ou há transformação no caminho
json · task-settings.json
{
  "TargetMetadata": {
    "SupportLobs": true,
    "FullLobMode": false,
    "LimitedSizeLobMode": true,
    "LobMaxSize": 64
  },
  "FullLoadSettings": { "TargetTablePrepMode": "DO_NOTHING" },
  "ValidationSettings": { "EnableValidation": true, "ThreadCount": 8 },
  "Logging": { "EnableLogging": true }
}
bash · aws cli
aws dms create-replication-task \
  --replication-task-identifier app-full-cdc \
  --source-endpoint-arn arn:aws:dms:...:endpoint:SRC \
  --target-endpoint-arn arn:aws:dms:...:endpoint:TGT \
  --replication-instance-arn arn:aws:dms:...:rep:INST \
  --migration-type full-load-and-cdc \
  --table-mappings file://table-mappings.json \
  --replication-task-settings file://task-settings.json
aws dms start-replication-task --replication-task-arn <arn> --start-replication-task-type start-replication
aws dms describe-table-statistics --replication-task-arn <arn>   # ValidationState por tabela
Erros possíveis e mitigação
ErroColunas text/jsonb/bytea truncadas
CausaLimitedSizeLobMode com LobMaxSize menor que o maior valor.
MitigaçãoMeça max(octet_length(col)) na origem e ajuste, ou use FullLobMode nas tabelas afetadas.
ErroDestino sem índices secundários, FKs e defaults
CausaO DMS cria só tabelas e PK.
MitigaçãoPré-crie o schema (B1) e use TargetTablePrepMode = DO_NOTHING.
ErroUPDATE/DELETE não aplicados em algumas tabelas
CausaTabelas sem PK.
MitigaçãoPK ou REPLICA IDENTITY FULL na origem antes de iniciar a task.
Documentação oficial
G3 · Google Cloud Database Migration ServiceIntermediário
Destino Cloud SQL ou AlloyDB (baseado em pglogical)
bash · gcloud
# Origem: pglogical instalado em TODOS os bancos + wal_level=logical + shared_preload_libraries
psql -h $OLD -d $DB -c "CREATE EXTENSION IF NOT EXISTS pglogical;"

gcloud database-migration connection-profiles create postgresql src-app \
  --region=southamerica-east1 --host=203.0.113.10 --port=5432 \
  --username=migration_admin --password=***

gcloud database-migration migration-jobs create app-to-cloudsql \
  --region=southamerica-east1 --type=CONTINUOUS \
  --source=src-app --destination=dst-cloudsql

gcloud database-migration migration-jobs verify  app-to-cloudsql --region=southamerica-east1
gcloud database-migration migration-jobs start   app-to-cloudsql --region=southamerica-east1
gcloud database-migration migration-jobs describe app-to-cloudsql --region=southamerica-east1
# Cutover (com escrita congelada na origem e lag ~0):
gcloud database-migration migration-jobs promote app-to-cloudsql --region=southamerica-east1
Erros possíveis e mitigação
Erroverify falha: pglogical extension not installed
CausaExtensão ausente em algum banco (inclusive postgres).
MitigaçãoCREATE EXTENSION pglogical em cada banco e reinicie com shared_preload_libraries.
ErroTabelas sem PK com UPDATE/DELETE perdidos
CausaLimitação do mecanismo baseado em pglogical.
MitigaçãoCrie PKs antes de iniciar o job.
ErroUsuários/roles ausentes após promote
CausaO DMS migra dados e schema dos bancos, não os usuários.
MitigaçãoRecrie roles e grants no Cloud SQL antes do cutover.
Documentação oficial
G4 · Azure Database for PostgreSQL — Migration ServiceIntermediário
Modo online com cutover sob comando
json · migration.json (confira o schema atual na documentação)
{
  "properties": {
    "SourceDBServerResourceId": "10.0.0.5:5432@postgres",
    "SourceType": "OnPremises",
    "SslMode": "Require",
    "SecretParameters": {
      "AdminCredentials": { "SourceServerPassword": "***", "TargetServerPassword": "***" }
    },
    "DBsToMigrate": ["app"],
    "OverwriteDBsInTarget": "true"
  }
}
bash · az cli
az postgres flexible-server migration create \
  --subscription <sub-id> --resource-group rg-db \
  --server-name pgflex-target --name mig-app-01 \
  --migration-mode online --properties "migration.json"
# Somente validação prévia: acrescente --migration-option Validate

az postgres flexible-server migration show \
  --resource-group rg-db --server-name pgflex-target --name mig-app-01

# Cutover (após lag ~0 e escrita congelada)
az postgres flexible-server migration update \
  --resource-group rg-db --server-name pgflex-target --name mig-app-01 --cutover
Erros possíveis e mitigação
ErroValidação falha: wal_level / replication slots
CausaParâmetros da origem incompatíveis com o modo online.
Mitigaçãowal_level = logical, max_replication_slots e max_wal_senders ajustados e reinício.
ErroExtensões não permitidas no destino
CausaExtensão não liberada em azure.extensions.
MitigaçãoAdicione em azure.extensions no Flexible Server antes da migração.
Documentação oficial
09

Cutover, Validação e Rollback

Comum a todas as estratégias: go/no-go, sequências, dados, fencing e retorno

Checklist go/no-go (T-30 min)Básico
Todos os itens precisam estar verdes para iniciar a janela
Lag de replicação estável e < 1 s nos últimos 30 min (A2 / B5 / F3)
Todas as tabelas em estado r (pg_subscription_rel) — estratégias lógicas
Índices, FKs e ANALYZE aplicados no destino (B4); nenhum índice inválido
wal_level = logical no destino (replicação reversa pronta)
Scripts de sequências e validação testados em homologação
Conexões diretas à origem (fora do pooler) mapeadas e desligadas
Snapshot/backup da origem concluído e restaurável
Nenhuma transação > 5 min aberta na origem
Aprovador de negócio e dev on-call presentes no canal
Critérios de abort e dono da decisão registrados no ticket
sql · destino
-- Verificação rápida em uma única consulta (DESTINO, estratégias lógicas)
SELECT
  (SELECT count(*) FROM pg_subscription_rel WHERE srsubstate <> 'r')    AS tabelas_nao_prontas,
  (SELECT count(*) FROM pg_index WHERE NOT indisvalid)                  AS indices_invalidos,
  (SELECT setting FROM pg_settings WHERE name = 'wal_level')            AS wal_level,
  (SELECT max(now() - last_analyze) FROM pg_stat_user_tables)          AS analyze_mais_antigo;
Documentação oficial
Sincronização de sequênciasIntermediário
O que a replicação lógica não leva (até o PG18)
Por que é necessárioA replicação lógica replica as linhas, mas não o estado das sequências, inclusive das colunas serial e GENERATED ... AS IDENTITY. No destino, elas continuam no valor inicial. O script abaixo lê a origem com escrita congelada e gera os setval para o destino, com uma margem de segurança que respeita o sinal do increment_by.
sql · gera_setval.sql
-- gera_setval.sql  (executar na ORIGEM como superuser/owner; saída = script para o DESTINO)
SELECT CASE
         WHEN last_value IS NULL THEN
           format('SELECT setval(%L, %s, false);', format('%I.%I', schemaname, sequencename), start_value)
         ELSE
           format('SELECT setval(%L, %s, true);',  format('%I.%I', schemaname, sequencename),
                  last_value + 1000 * increment_by)
       END
FROM pg_sequences
ORDER BY schemaname, sequencename;
bash
psql -h $OLD -U postgres -d $DB -Atq -f gera_setval.sql > seq_sync.sql
psql -h $NEW -U postgres -d $DB -v ON_ERROR_STOP=1 -q -f seq_sync.sql

# Conferência no DESTINO: sequência nunca pode estar atrás do max() da coluna que ela alimenta
psql -h $NEW -U postgres -d $DB -f check_seq.sql
sql · check_seq.sql
-- check_seq.sql — serial (deptype a) e identity (deptype i); \gexec executa cada consulta gerada
SELECT format('SELECT %L AS sequencia, (SELECT last_value FROM %s) AS valor_seq, '
              '(SELECT coalesce(max(%I),0) FROM %s) AS max_coluna',
              s.oid::regclass, s.oid::regclass, a.attname, t.oid::regclass)
FROM pg_class s
JOIN pg_depend d    ON d.objid = s.oid AND d.classid = 'pg_class'::regclass AND d.deptype IN ('a','i')
JOIN pg_class t     ON t.oid = d.refobjid
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid
WHERE s.relkind = 'S' \gexec
Erros possíveis e mitigação
Erropermission denied for sequence ...
CausaUsuário sem privilégio de leitura em pg_sequences.last_value.
MitigaçãoExecute como superuser/owner (ou role com pg_read_all_data, PG14+).
Errovalue ... is out of bounds for sequence
CausaMargem ultrapassa max_value (ex.: sequência integer perto do limite).
MitigaçãoReduza a margem para essas sequências; aproveite para migrar a coluna para bigint.
Documentação oficial
Validação de dadosAvançado
Contagem exata e hash por faixas de PK

Execute as mesmas consultas nos dois lados, com a escrita congelada no cutover (ou por amostragem durante a replicação), e compare com diff.

sql · contagem_exata.sql
-- contagem_exata.sql — contagem exata de todas as tabelas sem PL/pgSQL
SELECT table_schema || '.' || table_name AS tabela,
       (xpath('/row/c/text()',
              query_to_xml(format('SELECT count(*) AS c FROM %I.%I', table_schema, table_name),
                           false, true, '')))[1]::text::bigint AS linhas
FROM information_schema.tables
WHERE table_type = 'BASE TABLE'
  AND table_schema NOT IN ('pg_catalog','information_schema')
ORDER BY 1;
bash
for H in $OLD $NEW; do
  psql -h $H -U postgres -d $DB -Atq -f contagem_exata.sql > contagem_$H.txt
done
diff contagem_$OLD.txt contagem_$NEW.txt && echo "CONTAGENS IGUAIS"

# Hash por faixa de PK nas tabelas críticas (mesmas configurações de sessão nos dois lados!)
Q="SET TimeZone='UTC'; SET extra_float_digits=3; SET DateStyle='ISO';
   SELECT md5(string_agg(md5(t::text), '' ORDER BY id)) FROM public.pedidos t WHERE id BETWEEN 1 AND 1000000;"
diff <(psql -h $OLD -d $DB -Atq -c "$Q") <(psql -h $NEW -d $DB -Atq -c "$Q") && echo "HASH OK"
Erros possíveis e mitigação
ErroHash diferente com contagens iguais
CausaRepresentação textual diferente: TimeZone, DateStyle, extra_float_digits, ordem de chaves em jsonb (igual) vs json (texto) ou colunas em ordem diferente.
MitigaçãoFixe as configurações de sessão; compare a lista explícita de colunas em vez de t::text; investigue a faixa divergente dividindo-a ao meio (busca binária).
ErroContagem diferente em tabelas de log
CausaTabela unlogged/particionada fora da publicação, ou escrita ainda ocorrendo.
MitigaçãoConfira pg_publication_tables; valide apenas com escrita congelada.
Documentação oficial
Fencing da origemIntermediário
Garantir que ninguém escreva no banco antigo após o cutover
sql · origem
-- ORIGEM (antiga), logo após o reponte
ALTER ROLE app_rw SET default_transaction_read_only = on;
REVOKE CONNECT ON DATABASE app FROM PUBLIC;          -- mantém replicator/postgres com CONNECT explícito
GRANT CONNECT ON DATABASE app TO replicator;
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname = 'app' AND usename NOT IN ('replicator','postgres') AND pid <> pg_backend_pid();

-- Quem ainda tenta conectar? (log_connections = on)
SELECT client_addr, usename, application_name, count(*) FROM pg_stat_activity
WHERE datname = 'app' GROUP BY 1,2,3;
pg_hba.conf
# pg_hba.conf da ORIGEM antiga — rejeitar a aplicação explicitamente (primeira linha que casa vence)
host  app  app_rw  0.0.0.0/0  reject
⛔Split-brain de escrita (aplicação escrevendo nos dois bancos) é o pior resultado possível de uma migração: os dados divergem sem conflito visível. O fencing em camadas (pooler, role read-only, pg_hba e encerramento de sessões) existe para torná-lo impossível.
Documentação oficial
RollbackAvançado
Critérios objetivos e procedimento de retorno
Durante a janela (antes do RESUME no destino): rollback trivial — ALTER ROLE ... RESET default_transaction_read_only na origem, reponte do pooler de volta e RESUME. Nenhum dado foi escrito no destino.
Após o cutover, com replicação reversa ativa (B6 passo 5): o rollback é um novo cutover no sentido contrário — PAUSE, esperar rev_sub alcançar, sincronizar sequências do destino para a origem, repontar, RESUME.
Sem replicação reversa: rollback implica perder as escritas feitas no destino ou reconciliá-las manualmente. Defina esse ponto de não retorno por escrito antes da janela.
Gatilhos típicos: taxa de erro da aplicação acima de X%, latência p95 acima de Y ms por mais de Z min, divergência na validação de tabela crítica.
bash · rollback
# Rollback após o cutover (replicação reversa ativa)
PGBA="psql -h $PGB -p 6432 -U pgbouncer_admin pgbouncer"
$PGBA -c "PAUSE app;"
psql -h $NEW -U postgres -d $DB -c "ALTER ROLE $APP_ROLE SET default_transaction_read_only = on;"
psql -h $NEW -U postgres -d $DB -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$DB' AND usename='$APP_ROLE';"

LSN=$(psql -h $NEW -U postgres -d $DB -Atc "SELECT pg_current_wal_lsn()")
until [ "$(psql -h $NEW -U postgres -d $DB -Atc \
  "SELECT confirmed_flush_lsn >= '$LSN' FROM pg_replication_slots WHERE slot_name='rev_sub'")" = "t" ]; do sleep 1; done

psql -h $NEW -U postgres -d $DB -Atq -f gera_setval.sql > seq_back.sql
psql -h $OLD -U postgres -d $DB -v ON_ERROR_STOP=1 -q -f seq_back.sql

psql -h $OLD -U postgres -d $DB -c "ALTER ROLE $APP_ROLE RESET default_transaction_read_only;"
psql -h $OLD -U postgres -d $DB -c "GRANT CONNECT ON DATABASE $DB TO PUBLIC;"   # desfaz o fencing
# remover a linha "reject" do pg_hba.conf da origem e: SELECT pg_reload_conf();
sudo sed -i "s/host=$NEW/host=$OLD/" /etc/pgbouncer/pgbouncer.ini
$PGBA -c "RELOAD;" -c "RECONNECT app;" -c "RESUME app;"
Documentação oficial
Pós-cutover e encerramentoBásico
Monitoramento, comparação com a baseline e limpeza
sql
-- DESTINO: confirmar que as escritas chegam no novo banco
SELECT datname, xact_commit, tup_inserted, tup_updated FROM pg_stat_database WHERE datname = 'app';

-- Regressões de performance vs. baseline (compare com baseline_origem.csv)
SELECT queryid, calls, round(mean_exec_time::numeric,2) AS media_ms, left(query,120)
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 50;

-- Slots esquecidos (em TODOS os servidores envolvidos)
SELECT slot_name, slot_type, active, wal_status FROM pg_replication_slots;
D+0: monitoramento reforçado de erros, latência, locks e autovacuum no destino
D+1: comparação de pg_stat_statements com a baseline; ajustes de índices e parâmetros
D+7 (ou prazo acordado): encerrar replicação reversa (B7), remover slots e publicações
Backups e monitoração (pgBackRest/Barman, Zabbix/Prometheus) apontados para o novo servidor e testados
Descomissionar a origem somente após o primeiro backup completo validado do destino
Atualizar a documentação de arquitetura e o inventário
Documentação oficial
10

Troubleshooting Rápido

Mensagens de erro mais comuns e onde está a solução

Matriz de erros × mitigaçãoIntermediário
Referência rápida durante a janela
Mensagem / sintomaMitigação rápidaEtapa
cannot update table ... does not have a replica identityALTER TABLE ... REPLICA IDENTITY FULL imediatamente; PK depoisB2
logical decoding requires wal_level >= logicalAjustar wal_level e reiniciar01
all replication slots are in useAumentar max_replication_slots (restart)01
requested WAL segment ... has already been removedRefazer standby com slotA1
wal_status = lost no slotRecriar a replicação do zero (truncate + recarga)B3
duplicate key value violates unique constraint (carga)Destino não vazio: truncate e recriar / SKIP (lsn)B3
duplicate key value (após cutover)Sincronizar sequências09
is missing replicated columnAplicar DDL no destinoB5
out of logical replication worker slotsAumentar workers no destinoB3
CREATE SUBSCRIPTION travadoEncerrar transação longa na origemB3
PAUSE do PgBouncer não retornaEncerrar idle in transaction; abortar após limite01
cannot execute INSERT in a read-only transactionCliente conectando direto na origem: reapontarB6
pg_rewind exige checksums/wal_log_hintsRefazer como standby via basebackupA5
lc_collate values ... do not matchinitdb com o mesmo localeD1
old cluster does not use data checksumsinitdb --no-data-checksums (PG18)D1
Invalid cross-device linkMesmo filesystem ou --copyD3
__debezium_unavailable_valueREPLICA IDENTITY FULL na tabelaF3
Switchover RDS timed outReduzir carga/transações longas e repetirG1
💡Durante a janela, o log do PostgreSQL é a fonte primária. Deixe tail -F aberto nos logs de origem e destino e habilite log_replication_commands = on e log_connections = on antes do T-0.
Documentação oficial