Banco de Dados em Produção
Connection Pooling: PgBouncer
POR QUE CONNECTION POOLING É OBRIGATÓRIO:
Sem pooler:
App (100 workers) → 100 conexões → PostgreSQL
Cada conexão PostgreSQL = 1 processo fork (~10MB RAM)
100 conexões × 10MB = 1GB só para manter conexões
PostgreSQL padrão: max_connections = 100
Com PgBouncer:
App (100 workers) → 100 conexões → PgBouncer → 20 conexões → PostgreSQL
PgBouncer multiplica: reutiliza conexões ociosas entre workers
Overhead do PgBouncer: ~2KB por conexão (vs ~10MB do PostgreSQL)
MODOS DE POOLING:
- session: conexão dedicada por sessão (menos eficiente, mais compatível)
- transaction: conexão compartilhada, alocada por transação (recomendado)
- statement: conexão compartilhada por statement (não suporta transações multi-statement)
; pgbouncer.ini
[databases]
myapp = host=127.0.0.1 port=5432 dbname=myapp
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
; Modo transaction: conexão devolvida ao pool após cada transação
pool_mode = transaction
; Pool sizing:
default_pool_size = 20 ; Conexões por par user/database
min_pool_size = 5 ; Manter 5 conexões sempre abertas
reserve_pool_size = 5 ; Pool reserva para picos
reserve_pool_timeout = 3 ; Segundos antes de usar reserva
max_client_conn = 400 ; Máximo de clientes simultâneos
max_db_connections = 50 ; Máximo de conexões reais ao PostgreSQL
; Timeouts:
server_idle_timeout = 600 ; Fechar conexões ociosas após 10min
query_timeout = 30 ; Timeout de query (segundos)
client_idle_timeout = 0 ; 0 = sem timeout para clientes ociosos
; Logging:
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
stats_period = 60 ; Estatísticas a cada 60 segundos
; IMPORTANTE para transaction mode:
; - NÃO usar SET statements (ex: SET timezone)
; - NÃO usar LISTEN/NOTIFY
; - NÃO usar prepared statements nomeados
; - NÃO usar cursores WITH HOLD
; Alternativa: usar session mode para essas operações
# Fórmula para calcular pool_size ideal:
# pool_size = (num_cores * 2) + num_disks
# Para SSD: pool_size = num_cores * 2 + 1
# Exemplo: 4 cores, SSD → pool_size = 9
# Começar conservador e aumentar monitorando pg_stat_activity
# Monitorar o PgBouncer:
psql -h localhost -p 6432 -U pgbouncer pgbouncer
SHOW POOLS;
# database | user | cl_active | cl_waiting | sv_active | sv_idle
# myapp | app | 45 | 0 | 12 | 8
# cl_waiting > 0 frequentemente → aumentar pool_size
SHOW STATS;
# Mostra: total_requests, avg_query_time, bytes_sent, etc.
SHOW CLIENTS;
# Lista todos os clientes conectados ao PgBouncer
Migrations em Produção: Zero-Downtime
REGRA FUNDAMENTAL:
Em produção com deploy rolling (múltiplas versões rodando simultaneamente),
migrations DEVEM ser backwards compatible.
A versão ANTIGA do código precisa funcionar com o schema NOVO.
A versão NOVA do código precisa funcionar com o schema ANTIGO.
PADRÃO EXPAND-CONTRACT:
Exemplo: renomear coluna "name" para "full_name"
ERRADO (causa downtime):
ALTER TABLE users RENAME COLUMN name TO full_name;
→ Versão antiga do código quebra imediatamente: SELECT name FROM users
CORRETO (expand-contract em 4 deploys):
Deploy 1 — EXPAND: adicionar nova coluna
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
-- Trigger para sincronizar dados durante transição:
CREATE OR REPLACE FUNCTION sync_name_to_full_name()
RETURNS TRIGGER AS $$
BEGIN
NEW.full_name = COALESCE(NEW.full_name, NEW.name);
RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_sync_name
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION sync_name_to_full_name();
-- Backfill dados existentes:
UPDATE users SET full_name = name WHERE full_name IS NULL;
Deploy 2 — MIGRATE: código lê de full_name, escreve em ambas
SELECT full_name FROM users; -- Lê da nova
UPDATE users SET name = $1, full_name = $1 WHERE id = $2; -- Escreve em ambas
Deploy 3 — CONTRACT: código usa apenas full_name
SELECT full_name FROM users;
UPDATE users SET full_name = $1 WHERE id = $2;
Deploy 4 — CLEANUP: remover coluna antiga e trigger
DROP TRIGGER trg_sync_name ON users;
DROP FUNCTION sync_name_to_full_name();
ALTER TABLE users DROP COLUMN name;
-- OPERAÇÕES SEGURAS vs PERIGOSAS em PostgreSQL:
-- SEGURO (não bloqueia reads/writes):
ALTER TABLE users ADD COLUMN bio TEXT; -- Sem DEFAULT
ALTER TABLE users ADD COLUMN active BOOLEAN DEFAULT true; -- PG 11+ (não reescreve tabela)
CREATE INDEX CONCURRENTLY idx_users_email ON users(email); -- Não bloqueia writes
DROP INDEX CONCURRENTLY idx_old_index;
-- PERIGOSO (bloqueia a tabela — ACCESS EXCLUSIVE LOCK):
ALTER TABLE users ADD COLUMN score INT DEFAULT 0; -- PG < 11 (reescreve tabela inteira!)
ALTER TABLE users ALTER COLUMN name SET NOT NULL; -- Full table scan + lock
ALTER TABLE users ALTER COLUMN name TYPE TEXT; -- Reescreve tabela se mudar tipo
CREATE INDEX idx_users_email ON users(email); -- Sem CONCURRENTLY = bloqueia writes
-- NOT NULL seguro (PG 12+):
ALTER TABLE users ADD CONSTRAINT users_name_not_null
CHECK (name IS NOT NULL) NOT VALID; -- Não verifica dados existentes
ALTER TABLE users VALIDATE CONSTRAINT users_name_not_null; -- Valida sem bloquear
-- Timeout para DDL (não ficar travado esperando lock):
SET lock_timeout = '5s';
-- Se não conseguir o lock em 5s, aborta em vez de bloquear a fila
-- FERRAMENTAS DE MIGRATION SEGURAS:
-- pgroll — migrations reversíveis com expand-contract automático
-- pg-osc — online schema change (como pt-osc do MySQL)
-- reshape — migrations com zero downtime para PostgreSQL
-- squawk — linter de migrations PostgreSQL (detecta operações perigosas)
Backups: pg_dump, pg_basebackup e PITR
# 3 NÍVEIS DE BACKUP:
# 1. LOGICAL BACKUP — pg_dump (SQL ou custom format)
pg_dump -Fc -Z 9 -j 4 -d myapp -f /backups/myapp_$(date +%Y%m%d).dump
# -Fc: custom format (comprimido, permite restore seletivo)
# -Z 9: compressão máxima
# -j 4: 4 jobs paralelos (tabelas em paralelo)
# Restore:
pg_restore -j 4 -d myapp /backups/myapp_20240115.dump
# Restore seletivo (apenas uma tabela):
pg_restore -j 4 -d myapp -t orders /backups/myapp_20240115.dump
# Limitações do pg_dump:
# - Snapshot no momento do início (pode perder dados entre dump e crash)
# - Tempo proporcional ao tamanho do banco
# - Lock leve mas pode afetar performance (ACCESS SHARE)
# - NÃO serve para bancos > 100GB (muito lento)
# 2. PHYSICAL BACKUP — pg_basebackup (cópia dos arquivos de dados)
pg_basebackup -D /backups/base -Ft -z -P -X stream
# -Ft: formato tar
# -z: comprimido
# -P: progresso
# -X stream: inclui WAL necessário durante o backup
# Mais rápido que pg_dump para bancos grandes
# Mas restaura o banco INTEIRO (não seletivo)
# 3. WAL ARCHIVING + PITR (Point-In-Time Recovery)
# O método mais robusto — combinação de base backup + WAL contínuo
# postgresql.conf:
# wal_level = replica # Mínimo para archiving
# archive_mode = on # Habilitar archiving
# archive_command = 'cp %p /wal_archive/%f' # Comando para copiar WAL
# Produção: usar pgBackRest, WAL-G ou barman em vez de cp
# PITR — restaurar para um ponto específico no tempo:
# 1. Restaurar base backup
# 2. Replay WAL até o momento desejado
# recovery.conf (ou postgresql.auto.conf no PG 12+):
# restore_command = 'cp /wal_archive/%f %p'
# recovery_target_time = '2024-01-15 14:30:00'
# recovery_target_action = 'promote'
# FERRAMENTAS PROFISSIONAIS DE BACKUP:
# pgBackRest: backup incremental, paralelo, encryption, S3/GCS support
# WAL-G: backup para cloud (S3, GCS, Azure), compressão, encryption
# Barman: backup manager completo (2ndQuadrant/EDB)
# pgBackRest exemplo:
pgbackrest --stanza=myapp --type=full backup
pgbackrest --stanza=myapp --type=diff backup # Diferencial
pgbackrest --stanza=myapp --type=incr backup # Incremental
# Restore com PITR:
pgbackrest --stanza=myapp --type=time \
--target="2024-01-15 14:30:00" restore
# REGRA 3-2-1 DE BACKUPS:
# 3 cópias dos dados
# 2 tipos diferentes de mídia (disco local + cloud)
# 1 cópia offsite (outra região/provedor)
# TESTE DE RESTORE (o mais importante!):
# Backup que nunca foi testado NÃO é backup.
# Automatizar teste semanal: restaurar em ambiente isolado e validar dados.
Replicação PostgreSQL
# STREAMING REPLICATION (física) — réplica bit-a-bit:
# Primary envia WAL records em tempo real para standby(s)
# No primary (postgresql.conf):
# wal_level = replica
# max_wal_senders = 3 # Máximo de réplicas
# wal_keep_size = 1GB # WAL retido para réplicas que atrasam
# Criar usuário de replicação:
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'secret';
# No standby:
pg_basebackup -h primary -U replicator -D /var/lib/postgresql/data -P -X stream
# standby.signal (criar arquivo vazio):
touch /var/lib/postgresql/data/standby.signal
# postgresql.auto.conf no standby:
# primary_conninfo = 'host=primary port=5432 user=replicator password=secret'
# Monitorar replication lag:
SELECT client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replication_lag_bytes
FROM pg_stat_replication;
# SYNCHRONOUS vs ASYNCHRONOUS:
# Async (padrão): melhor performance, possível perda de dados no failover
# Sync: zero data loss, mas latência de commit aumenta (espera confirmação)
# synchronous_commit = remote_apply # Mais seguro
# synchronous_standby_names = 'standby1'
# LOGICAL REPLICATION (PG 10+) — replicação por tabela/operação:
# Permite replicar tabelas específicas (não o banco inteiro)
# Permite replicação entre versões diferentes do PostgreSQL
# Ideal para: upgrade com zero downtime, data warehouse, multi-DC
# No publisher:
CREATE PUBLICATION my_pub FOR TABLE orders, users;
# No subscriber:
CREATE SUBSCRIPTION my_sub
CONNECTION 'host=primary port=5432 dbname=myapp user=replicator'
PUBLICATION my_pub;
# READ REPLICAS — escalar leituras:
# Aplicação envia writes para primary, reads para réplicas
# Cuidado com replication lag: leitura pode retornar dado desatualizado
# Padrão: ler do primary logo após write (read-your-own-writes)
Alta Disponibilidade: Patroni e Failover
# PATRONI — HA (High Availability) para PostgreSQL:
# Gerencia failover automático usando consensus (etcd, ZooKeeper, Consul)
# Arquitetura:
# ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
# │ Node 1 │ │ Node 2 │ │ Node 3 │
# │ PostgreSQL │ │ PostgreSQL │ │ PostgreSQL │
# │ (PRIMARY) │ │ (STANDBY) │ │ (STANDBY) │
# │ Patroni agent │ │ Patroni agent │ │ Patroni agent │
# └────────┬────────┘ └────────┬────────┘ └────────┬────────┘
# └─────────────┬──────┴──────────────┬─────┘
# ┌─────┴─────┐ ┌─────┴─────┐
# │ etcd │ │ HAProxy/ │
# │ cluster │ │ PgBouncer │
# └───────────┘ └───────────┘
# patroni.yml:
scope: myapp-cluster
name: node1
restapi:
listen: 0.0.0.0:8008
connect_address: node1:8008
etcd3:
hosts: etcd1:2379,etcd2:2379,etcd3:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576 # 1MB — máximo lag para failover
postgresql:
use_pg_rewind: true
parameters:
max_connections: 200
shared_buffers: 4GB
wal_level: replica
max_wal_senders: 5
synchronous_commit: 'on'
synchronous_standby_names: '*'
initdb:
- encoding: UTF8
- data-checksums # Detectar corrupção silenciosa
postgresql:
listen: 0.0.0.0:5432
connect_address: node1:5432
data_dir: /var/lib/postgresql/data
authentication:
replication:
username: replicator
password: secret
superuser:
username: postgres
password: secret
# Comandos Patroni:
patronictl -c /etc/patroni.yml list
# +--------+--------+----------+--------+---------+----+-----------+
# | Member | Host | Role | State | TL | Lag| Tags |
# +--------+--------+----------+--------+---------+----+-----------+
# | node1 | node1 | Leader | running| 3 | | |
# | node2 | node2 | Replica | running| 3 | 0 | |
# | node3 | node3 | Replica | running| 3 | 0 | |
# +--------+--------+----------+--------+---------+----+-----------+
# Failover manual:
patronictl -c /etc/patroni.yml switchover --master node1 --candidate node2
# Reinicializar configuração:
patronictl -c /etc/patroni.yml reload myapp-cluster
Performance Tuning do PostgreSQL
# PARÂMETROS ESSENCIAIS (postgresql.conf):
# shared_buffers: cache de páginas do PostgreSQL em memória
# Regra: 25% da RAM total (não mais que 8GB em geral)
shared_buffers = 4GB # Para servidor com 16GB RAM
# effective_cache_size: estimativa de memória disponível para cache de disco
# Regra: 50-75% da RAM total (inclui OS page cache)
effective_cache_size = 12GB
# work_mem: memória por operação de sort/hash (POR QUERY, não global!)
# Cuidado: 100 queries simultâneas × 64MB = 6.4GB!
# Regra: RAM / (max_connections * 2) para workloads OLTP
work_mem = 32MB
# maintenance_work_mem: memória para VACUUM, CREATE INDEX, ALTER TABLE
maintenance_work_mem = 1GB
# wal_buffers: buffer para WAL antes de flush
wal_buffers = 64MB
# checkpoint_completion_target: spread checkpoints para reduzir I/O spikes
checkpoint_completion_target = 0.9
# random_page_cost: custo relativo de I/O aleatório (para planner)
# SSD: 1.1 (quase igual a sequential I/O)
# HDD: 4.0 (padrão, I/O aleatório é muito mais caro)
random_page_cost = 1.1
# effective_io_concurrency: operações de I/O em paralelo
# SSD: 200 | HDD: 2
effective_io_concurrency = 200
# max_parallel_workers_per_gather: paralelismo de queries
max_parallel_workers_per_gather = 4
# ENABLE pg_stat_statements (obrigatório em produção):
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
-- pg_stat_statements — encontrar queries lentas:
SELECT
calls,
round(total_exec_time::numeric / 1000, 2) AS total_time_sec,
round(mean_exec_time::numeric, 2) AS avg_time_ms,
round(stddev_exec_time::numeric, 2) AS stddev_ms,
rows,
query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
-- Otimize as queries do TOP 20 — representam a maioria do tempo
-- Identificar queries que mais consomem I/O:
SELECT
calls,
shared_blks_hit + shared_blks_read AS total_blocks,
round(100.0 * shared_blks_hit / NULLIF(shared_blks_hit + shared_blks_read, 0), 2) AS cache_hit_pct,
query
FROM pg_stat_statements
ORDER BY shared_blks_read DESC
LIMIT 20;
-- cache_hit_pct < 95% → índice faltando ou work_mem pequeno
-- EXPLAIN ANALYZE — entender o plano de execução:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.id, u.name, o.total
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.created_at > '2024-01-01'
ORDER BY o.total DESC
LIMIT 10;
-- Pontos de atenção no EXPLAIN:
-- Seq Scan em tabela grande → índice faltando
-- Nested Loop com tabela grande → considerar Hash Join
-- Sort com "Sort Method: external merge" → work_mem pequeno
-- Buffers: shared read alto → dados não estão em cache
Vacuum e Autovacuum
POR QUE VACUUM EXISTE:
PostgreSQL usa MVCC (Multi-Version Concurrency Control).
UPDATE não modifica a linha — cria uma nova e marca a antiga como "morta".
DELETE marca a linha como morta mas não remove fisicamente.
VACUUM remove as linhas mortas e libera espaço para reutilização.
SEM VACUUM:
- Tabela cresce indefinidamente (table bloat)
- Índices ficam ineficientes (index bloat)
- Risco de transaction ID wraparound (corrupção de dados!)
AUTOVACUUM — processo automático que roda VACUUM:
# Configuração do autovacuum (postgresql.conf):
autovacuum = on # NUNCA desativar em produção!
autovacuum_max_workers = 5 # Workers simultâneos
autovacuum_naptime = 30s # Intervalo entre verificações
# Quando autovacuum inicia para uma tabela:
# dead_tuples > autovacuum_vacuum_threshold + (autovacuum_vacuum_scale_factor × table_rows)
# Padrão: 50 + (0.2 × table_rows) — 20% das linhas precisam estar mortas
# Para tabelas grandes (>1M linhas), ajustar por tabela:
autovacuum_vacuum_scale_factor = 0.01 # 1% em vez de 20%
autovacuum_vacuum_threshold = 1000
# Tuning por tabela (tabelas muito atualizadas):
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_vacuum_threshold = 500,
autovacuum_analyze_scale_factor = 0.005
);
# VACUUM FULL vs VACUUM regular:
# VACUUM: marca espaço como reutilizável (não reduz tamanho do arquivo)
# VACUUM FULL: reescreve a tabela inteira (reduz tamanho, mas ACCESS EXCLUSIVE LOCK!)
# Em produção: NUNCA usar VACUUM FULL em horário de pico
# Alternativa: pg_repack (reescrita online sem lock exclusivo)
-- Monitorar bloat e vacuum:
-- Tabelas com mais dead tuples:
SELECT
schemaname || '.' || relname AS table,
n_live_tup,
n_dead_tup,
round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
last_vacuum,
last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;
-- Progresso do VACUUM em execução:
SELECT * FROM pg_stat_progress_vacuum;
-- Transaction ID age (risco de wraparound):
SELECT
datname,
age(datfrozenxid) AS xid_age,
round(100.0 * age(datfrozenxid) / 2147483647, 2) AS pct_to_wraparound
FROM pg_database
ORDER BY age(datfrozenxid) DESC;
-- Se pct_to_wraparound > 50% → problema sério iminente!
-- PostgreSQL vai entrar em modo single-user para forçar VACUUM FREEZE
-- Estimar bloat de tabela (aproximado):
SELECT
schemaname || '.' || tablename AS table,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS total_size,
pg_size_pretty(pg_table_size(schemaname || '.' || tablename)) AS table_size,
pg_size_pretty(pg_indexes_size(schemaname || '.' || tablename)) AS indexes_size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC;
Monitoramento de Banco em Produção
-- QUERIES ATIVAS (o que está rodando agora?):
SELECT
pid,
now() - query_start AS duration,
state,
wait_event_type,
wait_event,
left(query, 100) AS query
FROM pg_stat_activity
WHERE state != 'idle'
AND pid != pg_backend_pid()
ORDER BY duration DESC;
-- LOCKS (quem está bloqueando quem?):
SELECT
blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query,
now() - blocked.query_start AS blocked_duration
FROM pg_stat_activity blocked
JOIN pg_locks bl ON bl.pid = blocked.pid
JOIN pg_locks lock_on ON lock_on.locktype = bl.locktype
AND lock_on.database IS NOT DISTINCT FROM bl.database
AND lock_on.relation IS NOT DISTINCT FROM bl.relation
AND lock_on.pid != bl.pid
JOIN pg_stat_activity blocking ON blocking.pid = lock_on.pid
WHERE NOT bl.granted;
-- CACHE HIT RATIO (deve ser > 99% em produção):
SELECT
sum(heap_blks_hit) AS cache_hits,
sum(heap_blks_read) AS disk_reads,
round(100.0 * sum(heap_blks_hit) / NULLIF(sum(heap_blks_hit) + sum(heap_blks_read), 0), 2) AS ratio
FROM pg_statio_user_tables;
-- Se < 99%: aumentar shared_buffers ou a instância não tem RAM suficiente
-- INDEX USAGE (índices não utilizados desperdiçam espaço e I/O):
SELECT
schemaname || '.' || relname AS table,
indexrelname AS index,
pg_size_pretty(pg_relation_size(indexrelid)) AS size,
idx_scan AS scans,
idx_tup_read AS tuples_read
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND indexrelname NOT LIKE '%_pkey'
ORDER BY pg_relation_size(indexrelid) DESC;
-- Índices com 0 scans → candidatos para remoção
-- CONNECTION USAGE:
SELECT
count(*) AS total,
count(*) FILTER (WHERE state = 'active') AS active,
count(*) FILTER (WHERE state = 'idle') AS idle,
count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_transaction,
max_conn.setting::int AS max_connections
FROM pg_stat_activity, pg_settings max_conn
WHERE max_conn.name = 'max_connections'
GROUP BY max_conn.setting;
-- idle in transaction: perigo! Transação aberta sem fazer nada (segura locks)
Disaster Recovery: RTO e RPO
RTO (Recovery Time Objective) — quanto tempo pode ficar fora do ar?
RPO (Recovery Point Objective) — quantos dados pode perder?
ESTRATÉGIAS POR NÍVEL:
RPO: horas | RTO: horas → pg_dump diário + restore manual
RPO: minutos | RTO: 30min → WAL archiving + PITR
RPO: segundos | RTO: minutos → Streaming replication + failover manual
RPO: zero | RTO: segundos → Synchronous replication + Patroni (auto-failover)
RPO: zero | RTO: zero* → Multi-region active-active (complexidade extrema)
PLANO DE DR (Disaster Recovery):
1. DEFINIR RTO/RPO com o negócio (não é decisão técnica)
2. IMPLEMENTAR backups automáticos com verificação
3. TESTAR restore regularmente (simulação mensal)
4. DOCUMENTAR runbook passo a passo
5. PRATICAR failover (game days)
RUNBOOK DE RESTORE (exemplo):
1. Identificar o ponto no tempo alvo (antes do incidente)
2. Parar aplicação (evitar novas escritas)
3. Provisionar novo servidor PostgreSQL
4. Restaurar base backup mais recente
5. Aplicar WAL até o ponto alvo (PITR)
6. Validar dados (queries de verificação)
7. Redirecionar aplicação para novo servidor
8. Monitorar métricas intensivamente por 24h
Upgrade do PostgreSQL
# DOIS MÉTODOS DE UPGRADE:
# 1. pg_upgrade (in-place, mais rápido):
# Converte data files diretamente. Downtime necessário.
pg_upgrade \
--old-datadir=/var/lib/postgresql/15/data \
--new-datadir=/var/lib/postgresql/16/data \
--old-bindir=/usr/lib/postgresql/15/bin \
--new-bindir=/usr/lib/postgresql/16/bin \
--link # Hard links em vez de copiar (muito mais rápido)
# Passos:
# 1. Instalar nova versão do PostgreSQL
# 2. Parar o cluster antigo
# 3. Executar pg_upgrade
# 4. Iniciar o cluster novo
# 5. Rodar ANALYZE para atualizar estatísticas
# 6. Remover cluster antigo
# Tempo de downtime: minutos (com --link) a horas (sem --link)
# 2. LOGICAL REPLICATION (zero-downtime upgrade):
# Mais complexo, mas sem downtime.
# Passos:
# 1. Provisionar novo cluster com PG 16
# 2. Criar schema no PG 16 (pg_dump --schema-only)
# 3. Configurar logical replication PG 15 → PG 16
# 4. Esperar sincronização completa
# 5. Validar dados no PG 16
# 6. Redirecionar aplicação para PG 16 (switch no DNS/PgBouncer)
# 7. Derrubar replicação e desativar PG 15
# Validação pós-upgrade:
# - Comparar contagem de linhas em tabelas críticas
# - Executar queries de negócio e comparar resultados
# - Verificar pg_stat_statements para regressão de performance
# - Monitorar cache hit ratio (vai começar frio após upgrade)
Referencias e Fontes