Performance

Autovacuum, bloat e wraparound: o que vigiar no PostgreSQL

Tabela que cresce sem ganhar linhas é bloat, e bloat quase sempre é autovacuum impedido. Como diagnosticar antes de virar parada emergencial.

Se uma tabela do PostgreSQL ocupa muito mais espaço do que as linhas vivas justificam, o autovacuum está sendo impedido de limpar — e o motivo raramente está na configuração do autovacuum. Está numa transação aberta, num slot de replicação parado ou numa tabela grande demais para os parâmetros padrão. O sintoma é espaço em disco e queries lentas; a causa é outra coisa.

Esse é o efeito colateral do modelo MVCC. Um UPDATE no PostgreSQL não altera a linha: ele grava uma nova versão e marca a anterior como morta. Um DELETE só marca. Quem recolhe as versões mortas é o VACUUM, e enquanto ele não passa, elas continuam ocupando páginas que toda leitura sequencial precisa atravessar.

Como o autovacuum decide agir

O autovacuum não roda por horário. Ele compara o número de tuplas mortas com um limiar calculado por tabela:

limiar = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × linhas da tabela

Os padrões são 50 tuplas para o limiar base e 20% para o fator de escala. Em uma tabela de mil linhas, isso significa vacuum a cada ~250 alterações — frequente. Em uma tabela de cem milhões de linhas, significa esperar vinte milhões de tuplas mortas antes de qualquer limpeza.

É por isso que o bloat aparece justamente nas tabelas maiores e mais movimentadas: o padrão foi desenhado para o caso pequeno. A correção é por tabela, não global:

SQL
ALTER TABLE pedidos SET (    autovacuum_vacuum_scale_factor = 0.01,    autovacuum_vacuum_threshold    = 1000);

Existe um caminho paralelo para tabelas que quase só recebem INSERT, governado por autovacuum_vacuum_insert_threshold (padrão 1000) e seu fator de escala. Sem ele, uma tabela append-only nunca acumularia tuplas mortas suficientes para ser visitada — e ficaria sem receber o congelamento que o item sobre wraparound explica adiante.

Quando o autovacuum roda e mesmo assim não limpa

Este é o caso que confunde. pg_stat_user_tables mostra last_autovacuum recente, o processo aparece em pg_stat_activity, e ainda assim n_dead_tup continua subindo.

O VACUUM só pode remover uma versão de linha que nenhuma transação ainda possa enxergar. Se existe no sistema algo segurando um snapshot antigo, todas as versões posteriores a ele ficam intocáveis — em toda a instância, não só na tabela em questão. Os três suspeitos:

Transação aberta e ociosa. O clássico idle in transaction: a aplicação abriu a transação, não fez COMMIT e foi cuidar de outra coisa.

SQL
SELECT pid, state, xact_start, now() - xact_start AS duracao, queryFROM pg_stat_activityWHERE state IN ('idle in transaction', 'active')  AND xact_start IS NOT NULLORDER BY xact_start;

idle_in_transaction_session_timeout encerra essas sessões automaticamente e evita que o problema volte.

Slot de replicação parado. Um slot cujo consumidor sumiu — réplica desligada, ferramenta de CDC removida sem limpar o slot — segura o xmin indefinidamente e ainda impede a reciclagem de WAL, com o risco adicional de encher o disco.

SQL
SELECT slot_name, active, wal_status,       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS wal_retidoFROM pg_replication_slots;

Standby com hot_standby_feedback = on. Consultas longas na réplica seguram versões no primário. É o comportamento contratado — evita conflito de replicação — mas precisa ser considerado no diagnóstico.

Bloat já instalado: cuidado com a "solução"

VACUUM comum devolve espaço para reuso interno da própria tabela, mas não devolve disco ao sistema operacional. Para isso existe o VACUUM FULL — e ele não é uma operação online. Ele toma lock ACCESS EXCLUSIVE, bloqueando inclusive leituras, e reescreve a tabela inteira em um novo arquivo, o que exige espaço livre próximo ao tamanho do objeto durante a execução. Em tabela grande e sistema em operação, isso é uma parada.

Quando a janela não existe, pg_repack faz a reorganização com lock exclusivo apenas em trechos curtos. Não é nativo, precisa ser instalado e testado antes, e essa avaliação é melhor feita fora do incidente.

Wraparound: o limite que não se negocia

O PostgreSQL identifica transações com um contador circular de 32 bits. Para que versões antigas continuem visíveis depois que o contador dá a volta, o VACUUM marca linhas suficientemente antigas como congeladas. Se o congelamento não acontece, a instância caminha para o wraparound.

Quando a idade de uma tabela ultrapassa autovacuum_freeze_max_age, o autovacuum inicia uma passagem anti-wraparound que não pode ser adiada e não respeita a janela de manutenção. Ela aparece em pg_stat_activity como autovacuum: VACUUM ... (to prevent wraparound), consome I/O e não deve ser cancelada — cancelar apenas empurra o problema para a próxima tentativa, mais perto do limite.

Se o congelamento continuar não acontecendo, o PostgreSQL passa a recusar novas transações de escrita para proteger os dados. A recuperação exige VACUUM com o banco em modo restrito. É uma indisponibilidade evitável com monitoramento simples:

SQL
SELECT c.relname,       age(c.relfrozenxid) AS idade_xid,       pg_size_pretty(pg_total_relation_size(c.oid)) AS tamanhoFROM pg_class AS cJOIN pg_namespace AS n ON n.oid = c.relnamespaceWHERE c.relkind IN ('r', 'm')  AND n.nspname NOT IN ('pg_catalog', 'information_schema')ORDER BY age(c.relfrozenxid) DESCLIMIT 20;

Acompanhar essa idade ao longo do tempo mostra a tendência com semanas de antecedência. É exatamente o tipo de indicador com poder preditivo que discutimos em quais métricas realmente antecipam um incidente.

Um roteiro curto de verificação

Antes de mexer em parâmetros, responda nesta ordem: existe transação ou slot segurando xmin? As tabelas grandes têm configuração própria de autovacuum ou herdaram os 20%? A idade de transação das maiores tabelas está estável ou subindo? O autovacuum consegue terminar, ou é cancelado por locks e recomeça do zero?

Ajustar autovacuum_max_workers sem passar por essas perguntas costuma só distribuir o mesmo problema entre mais processos.

Próximo passo

Bloat e wraparound são problemas de tendência: previsíveis com antecedência, caros quando viram emergência.

A H1 Data faz um diagnóstico gratuito do ambiente PostgreSQL com leitura de bloat por tabela, efetividade do autovacuum, idade de transação e riscos de replicação, com prioridade do que tratar primeiro.

← Todos os artigos

Falar no WhatsApp