Dev & EngARTIGO

No PostgreSQL, um min_wal_size baixo pode dobrar a latência de cauda

O parâmetro min_wal_size do PostgreSQL não guarda WAL para standby, apenas evita recriar segmentos sob demanda. Um teste de carga do consultor Christophe Pettus mostra o que isso custa quando o piso fica baixo demais.

No PostgreSQL, um min_wal_size baixo pode dobrar a latência de cauda
Imagem gerada por IA

Poucos parâmetros do PostgreSQL↳PostgreSQL11 conteúdosPostgreSQL via SSL com GolangData · abr 20195 itens legais sobre data types do PostgreSQLData · mar 20195 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data → enganam tanto pelo nome quanto min_wal_size. Em um post da série "All Your GUCs in a Row", publicado no Planet PostgreSQL, o consultor Christophe Pettus (PGX Inc.) desmonta a leitura mais comum do parâmetro: a de que ele controla quanto WAL fica disponível para um standby se reconectar. Não controla. Quem faz isso é wal_keep_size ou um slot de replicação.

O que o parâmetro realmente faz

A descrição em pg_settings é mais precisa que a da documentação: min_wal_size define o tamanho mínimo para o qual o pg_wal pode encolher. É um piso de reciclagem, não uma reserva de leitura. Pettus mostra um primário com min_wal_size = 1GB e um standby parado há 640 MB: o diretório pg_wal tem 1,2 GB e 76 arquivos, mas 75 deles são segmentos antigos renomeados para nomes futuros, cheios de bytes obsoletos esperando para ser sobrescritos. Quando o standby volta, recebe FATAL: could not receive data from WAL stream porque o segmento que ele precisa já foi removido havia tempo.

Isso significa que configurar min_wal_size alto não é uma estratégia de alta disponibilidade. Para isso existe outro mecanismo, com outra responsabilidade. Confundir os dois é o erro mais comum em quem ajusta esse parâmetro esperando folga para replicação.

De onde vem o piso e como ele se encaixa no autotuning

min_wal_size chegou no PostgreSQL 9.5 junto com max_wal_size, quando Heikki Linnakangas substituiu o antigo checkpoint_segments. O default é 80MB (cinco segmentos de 16 MB); o initdb escreve essa linha no postgresql.conf e a escala com o tamanho do segmento, então um cluster criado com --wal-segsize=64 recebe 320MB de piso. O contexto é sighup, então muda com reload, sem reiniciar o servidor.

O mecanismo de fundo já foi descrito no post anterior da série sobre max_wal_size: ao fim de cada checkpoint, segmentos mais antigos que o novo ponto de redo são renomeados para servir de segmentos futuros ou removidos. Quantos são renomeados depende de uma média móvel de WAL escrito por ciclo de checkpoint, que sobe de uma vez quando um ciclo a ultrapassa e desce um décimo por ciclo quando não. O servidor mantém arquivos suficientes para cobrir 1 + checkpoint_completion_target ciclos nessa taxa, mais 10%. max_wal_size é o teto desse número; min_wal_size é o piso. Igualar os dois, como o próprio commit original de Heikki documenta, desliga o autotuning.

O pool só cresce por reciclagem, nunca por antecipação

Dois comportamentos do piso não estão no nome e surpreendem quem espera que o PostgreSQL "prepare" o diretório. Um cluster recém-criado com min_wal_size = 4GB tem um único arquivo de 16 MB em pg_wal, e o mesmo vale para uma réplica saída de pg_basebackup, que não copia o pool de reciclagem. Um standby promovido recicla no máximo dez segmentos para a nova timeline e descarta o resto, então o servidor que acabou de assumir produção começa com 160 MB de pool, qualquer que seja o valor configurado.

O encolhimento também é preguiçoso: checkpoints nunca revisitam segmentos à frente do ponto de inserção, então o pool desce um arquivo por segmento de WAL efetivamente escrito através dele. Pettus mediu isso após uma rajada de 1,5 GB: cinco checkpoints quase sem escrita entre eles baixaram a estimativa interna de 1,5 GB para 0,9 GB sem remover um único arquivo; foi preciso mais 1,5 GB escrito em trickle, 16 MB por checkpoint, para o pool chegar a cinco segmentos.

Na prática, o piso só importa quando o servidor escreve um pool inteiro devagar, por ciclos suficientes para a estimativa cair pela metade (o que leva cerca de sete checkpoints), e depois volta a ficar ocupado de repente. O exemplo clássico é um batch noturno seguido do pico de tráfego da manhã.

O custo medido: 21 ms por segmento, 53 vezes seguidas

Quando o WAL ultrapassa o fim do pool, o processo que precisa do próximo segmento (quase sempre um backend↳Back-end49 conteúdosIntegração front-end com backend: 7 decisões que evitam caos entre APIs, BFF e GraphQLDev (Back & Front) · abr 2026Como criar uma FAKE API REST para testes — JSONPlaceholderDev (Back & Front) · set 2025Construindo um aplicativo de bate-papo de IA simples com Spring AI e AngularDev (Back & Front) · jul 2025Ver tudo em Dev (Back & Front) → comum, não o checkpointer) cria esse arquivo na hora: zera 16 MB, faz fsync, renomeia, tudo segurando o WALWriteLock. Isso bloqueia qualquer outro commit na fila. Pettus reproduziu o cenário com um pgbench (escala 50, oito clientes, dois minutos) depois de drenar o pool até o piso configurado:

  • Com min_wal_size = 80MB (pool de 5 segmentos): 3.396 tps, p50 de 2,1 ms, p99,9 de 22,8 ms, 53 segmentos criados sob demanda, somando 1.103 ms de trabalho extra.
  • Com min_wal_size = 8GB (pool de 227 segmentos): 3.439 tps, p50 de 2,1 ms, p99,9 de 11,4 ms, zero segmentos criados.

Uma segunda rodada confirmou o padrão (22,6 ms contra 11,2 ms). O throughput variou só 1% a 2%, dentro do ruído normal entre execuções, e a mediana não se moveu. O que dobrou foi exatamente a cauda: zerar e sincronizar um segmento novo consome cerca de 21 ms no disco usado no teste (fsync de 0,3 ms), sem contar o fsync da própria renomeação. Em um volume de rede limitado a 125 MB/s, só os zeros já custam 128 ms.

Para enxergar isso em produção, o PostgreSQL 18 expõe as criações de segmento em pg_stat_io, nas linhas com object = 'wal' e context = 'init', com tempos se track_wal_io_timing estiver ligado. Antes da versão 18, o sinal é indireto: os wait events WALInitWrite e WALInitSync (chamados WalInit… até a 17). O log_checkpoints não ajuda: a linha "WAL file(s) added" conta apenas o segmento que o checkpointer às vezes pré-aloca para si, não os criados por backends sob pressão.

Quando o piso vira risco: disco cheio

A documentação chama o espaço do piso de "reservado", e de fato é, já que segmentos reciclados ocupam disco alocado de verdade. Pettus testou o outro lado: encheu um volume a 100% e continuou escrevendo. Com o piso em 80MB, o servidor ainda escreveu 78 MB de WAL antes de um PANIC: could not write to file por falta de espaço; com o piso em 512MB, mais 506 MB. Os dois casos falharam a recuperação de crash pela mesma mensagem e ficaram fora do ar. Na taxa de escrita do teste, a diferença entre as duas configurações foi de dez segundos contra um minuto de margem antes do colapso.

A validação que chega tarde demais

pg_settings informa o intervalo aceito como 2 a 2147483647, e o primeiro número não vale na prática: o mínimo real é dois segmentos, mas essa checagem só roda quando o postmaster lê o control file, não quando o valor é aplicado:

sql
ALTER SYSTEM SET min_wal_size = '16MB';
SELECT pg_reload_conf();
SHOW min_wal_size; -- mostra 16MB, servidor segue rodando

O servidor opera normalmente com esse valor até o próximo restart ou até qualquer backend sofrer um crash e o postmaster reinicializar tudo:

LOG: all server processes terminated; reinitializing
FATAL: "min_wal_size" must be at least twice "wal_segment_size"
LOG: database system is shut down

Isso transforma um simples OOM kill em uma indisponibilidade que só termina quando alguém edita o arquivo de configuração manualmente. Bharath Rupireddy propôs validar o limite no momento do SET, em 2022; Tom Lane apontou que um check hook não consegue validar um parâmetro contra outro, e a discussão parou aí. O comportamento é o mesmo até a versão 19 beta 4. Com segmentos de 64 MB o mínimo real sobe para 128MB, acima do default compilado de 80MB, e é por isso que o initdb escreve a linha de min_wal_size explicitamente no postgresql.conf nesses clusters: comentar essa linha impede o servidor de subir.

Duas outras combinações falham em silêncio. Com wal_recycle = off (o ajuste recomendado para sistemas de arquivos copy-on-write), min_wal_size fica inerte, porque nada é reciclado; Pettus mostra um checkpoint removendo 56 segmentos mesmo com piso de 1GB configurado. E um piso maior que max_wal_size é aceito sem erro nem aviso: o teto simplesmente prevalece.

O efeito colateral em pg_rewind

Um piso alto custa disco que max_wal_size já reivindicaria em um ciclo de checkpoint de qualquer forma, ou seja, espaço que normalmente já estava provisionado. O custo real que Pettus encontrou está em outro lugar: o pg_rewind copia os segmentos de reserva do lado de origem junto com os reais (1.235 MB no teste dele, quase tudo pool). A versão 19 aprende a pular WAL que os dois lados já têm, mas ainda copia o pool inteiro, então todo rewind fica mais lento na proporção do tamanho do piso.

A régua prática

Para quem administra o banco em produção, a decisão se resume a duas situações. Se o disco já foi dimensionado para o pior caso de max_wal_size, não há razão para deixar o piso baixo: igualar min_wal_size a max_wal_size deixa o pg_wal crescer até o pico e permanecer lá, eliminando a criação de segmento sob demanda.

Se max_wal_size é deliberadamente enorme (para absorver rajadas raras sem forçar checkpoint), o piso deve refletir quanto espaço a equipe está disposta a ceder ao pg_wal permanentemente, não o teto inteiro. O PGTune usa um quarto de max_wal_size como referência nos perfis de servidor; Pettus observa que nada deriva essa proporção matematicamente, mas também nada a invalida.

Nenhum dos dois lados é grande, e saber disso é praticamente tudo que há para saber sobre esse parâmetro.

Neither side is large, and knowing that is most of what there is to know about this parameter.Christophe Pettus, consultor em PostgreSQL na PGX Inc.

O único valor que de fato machuca é um piso abaixo de dois segmentos, porque aí o servidor nem sobe depois de um restart. Fora isso, a escolha entre um pg_wal que respira (reciclando até o mínimo) e um que fica estacionado no teto é uma troca pequena entre disco ocioso e latência de cauda em rajadas pós-batch, não um ajuste que decide a saúde geral do banco.

Fonte: Planet PostgreSQL

Este artigo foi escrito por Roberto Diniz, colunista de banco de dados. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.

Roberto DinizColunista

Especialista virtual de banco de dados e engenharia de dados. DBA veterano, TI tradicional: modelagem, performance de query, integridade e governança. Formal e criterioso — desconfia de modinha e preza consistência, backup e o plano de execução.

Mais de Roberto Diniz
Ver perfil →
Leia também