Assinar
Voltar
Backend

PostgreSQL 18 quase um ano depois: o que o I/O assíncrono realmente entregou

O PG18 chegou com o I/O assíncrono como manchete. Um ano de produção depois, dá para trocar a manchete por números: onde o ganho é real, onde é zero, e o que mais vale adotar no mesmo dia.

{}

O PostgreSQL 18 chegou em setembro de 2025 com a manchete mais barulhenta dos últimos anos: I/O assíncrono. Quase um ano de produção depois, dá para trocar a manchete por números. E os números contam uma história mais interessante do que o release note.

Resumo honesto: em alguns workloads o AIO cortou horas de janela de manutenção. Em outros, o ganho foi zero. Saber em qual grupo você está — antes de mexer em postgresql.conf — é o que separa uma migração tranquila de uma tarde perdida.

O problema que o I/O assíncrono resolve

Até o PostgreSQL 17, cada processo backend lia do disco de um jeito muito simples e muito antigo: pedia um bloco ao sistema operacional e parava até o bloco chegar. Uma leitura, uma espera. Outra leitura, outra espera.

Imagine um garçom que só anota o pedido da mesa 2 depois que a cozinha entrega o prato da mesa 1. Em um restaurante vazio, ninguém percebe. Em um sequential scan de 200 GB, o processo passa a maior parte da vida bloqueado esperando o disco — e o núcleo de CPU que poderia estar processando fica ocioso.

O Postgres sempre teve um paliativo para isso: o posix_fadvise, que basicamente cutuca o kernel dizendo “vou precisar desses blocos, vá buscando”. Funciona, mas é um aviso, não um pedido. Você não controla quando volta, não sabe se voltou, e não dá para medir.

O PG18 substituiu o paliativo por infraestrutura de verdade: o backend submete várias requisições de leitura, continua trabalhando, e coleta os resultados quando ficam prontos. O garçom passou a anotar as cinco mesas antes de ir à cozinha.

Os três parâmetros que importam

A configuração central é o io_method, que define como as leituras são despachadas. São três valores:

  • sync. O comportamento antigo. Existe como escape se algo der errado.
  • worker. Processos de background dedicados executam as leituras em nome dos backends. É o padrão do PG18 e funciona em qualquer sistema — macOS, FreeBSD, contêiner com seccomp restrito, servidor gerenciado.
  • io_uring. A interface nativa de I/O assíncrono do Linux. Menos cópia, menos troca de contexto, menos CPU por operação. Exige kernel recente e permissão explícita — em muitos ambientes gerenciados e sandboxes o io_uring está bloqueado por política de segurança.

A regra prática: se você não controla o kernel da máquina onde o Postgres roda, worker é o seu backend de AIO — queira você ou não.

Os outros dois parâmetros controlam o tamanho do lote. O io_combine_limit define quantos blocos adjacentes o Postgres funde em uma única requisição ao disco, e o io_max_combine_limit é o teto rígido desse valor. Fundir leituras é o que transforma 128 idas ao disco em uma só — e é onde mora boa parte do ganho real.

No PG18 o pool de workers é estático: io_workers vale 3 por padrão, com teto de 32, e só muda com restart. Essa rigidez foi o incômodo mais citado no primeiro ano — em carga de pico faltam workers, e fora do pico eles ficam parados. O PostgreSQL 19 troca o dial único por um pool que se auto-ajusta, com io_min_workers, io_max_workers, io_worker_idle_timeout e io_worker_launch_interval. Se você está planejando a adoção agora, vale saber que essa parte ainda vai mudar.

Onde o ganho aparece de verdade

Aqui está a parte que o marketing omite. O AIO acelera leitura de dados frios em volume. Traduzindo para o que você tem em produção:

  • VACUUM em tabelas grandes. É o caso de sucesso mais consistente — relatos de janelas caindo de 8 para 5 horas na mesma tabela e no mesmo hardware.
  • Sequential scans sobre dados que não estão em cache. Relatórios, ETL, agregação em tabela histórica. Ganho substancial.
  • Bitmap heap scans. Melhora modesta, mas mensurável.
  • OLTP indexado. Praticamente nada. Se sua carga é “busca por chave primária, devolve uma linha, tudo cabe em shared_buffers“, o AIO não tem trabalho para fazer.

Faz sentido quando você olha pelo mecanismo em vez do benchmark: o AIO só ajuda enquanto houver espera de disco para esconder. Se o dado já está na memória, não há espera — e otimizar espera zero continua dando zero.

Como medir em vez de adivinhar

O PG18 trouxe a visão pg_aios, que expõe as operações de I/O assíncrono em voo, e ampliou a pg_stat_io com as colunas read_bytes, write_bytes e extend_bytes, além de linhas para o I/O de WAL. Antes de ligar qualquer coisa, tire uma foto:

-- volume real de leitura por tipo de backend e contexto
SELECT backend_type, context, object,
       reads, read_bytes, read_time
FROM pg_stat_io
WHERE reads > 0
ORDER BY read_bytes DESC;

Se read_time for irrelevante perto do tempo total das suas consultas, você acabou de descobrir que o AIO não é o seu gargalo — e economizou uma semana.

O resto do PG18 que vale adotar no mesmo dia

O I/O assíncrono roubou a cena, mas algumas mudanças menores mudam código do dia a dia.

uuidv7() nativo. UUID versão 7 embute o timestamp nos bits mais significativos, então valores gerados em sequência ficam ordenados. Isso importa porque o índice B-tree de uma chave UUIDv4 sofre com inserção aleatória: cada novo registro cai em uma página diferente, fragmentando o índice e derrubando a taxa de acerto do cache. Com UUIDv7, as inserções voltam a ser quase sequenciais — você mantém a chave globalmente única sem pagar o preço no índice. Existe também uuidv4() como alias explícito.

Skip scan em índices B-tree multicoluna. Antes, um índice em (tenant_id, criado_em) era praticamente inútil para uma consulta que filtrava só por criado_em — o planner exigia a primeira coluna. Agora ele consegue “pular” pelos valores distintos da coluna inicial e usar o índice mesmo assim. Na prática, índices que você criaria em duplicata deixam de ser necessários.

RETURNING old.* e new.*. Em UPDATE, DELETE e MERGE você agora devolve o estado anterior e o novo na mesma instrução. Auditoria e outbox deixam de precisar de um SELECT extra dentro da transação:

UPDATE pedidos
   SET status = 'enviado'
 WHERE id = $1
RETURNING old.status AS status_anterior,
          new.status AS status_atual;

Colunas geradas virtuais. O padrão do GENERATED ALWAYS AS passou a ser calcular na leitura, não na escrita. Menos disco ocupado, escrita mais barata. Se você dependia do comportamento antigo, o STORED continua lá.

pg_upgrade --swap e estatísticas preservadas. O --swap troca os diretórios em vez de copiar ou linkar arquivos — é o modo mais rápido de subir de versão. E o --preserve-optimizer-statistics, agora padrão, resolve a dor clássica do upgrade: o cluster subir sem estatísticas e o planner tomar decisões terríveis até o primeiro ANALYZE completo.

O que quebra na atualização

Três mudanças incompatíveis merecem atenção antes do pg_upgrade:

  • Checksums de dados agora vêm ligados por padrão no initdb. É o comportamento correto, mas custa um pouco de CPU. Para desligar, --no-data-checksums.
  • VACUUM e ANALYZE passam a processar as partições filhas de uma tabela particionada. Se o seu script noturno assumia o comportamento antigo, use ONLY — ou aceite uma janela bem maior do que a esperada.
  • Autenticação MD5 está formalmente depreciada, com aviso em CREATE ROLE e ALTER ROLE. Migre para SCRAM. O aviso silencia com md5_password_warnings=off, mas silenciar não é migrar.

O plano de adoção em quatro passos

Se eu fosse subir um cluster para o 18 amanhã, faria nesta ordem:

  1. Medir antes. Colete pg_stat_io por uma semana em produção. Sem essa linha de base, você não tem como afirmar que o AIO ajudou.
  2. Subir com io_method = worker. É o padrão, funciona em qualquer lugar e já entrega a maior parte do ganho.
  3. Atacar o VACUUM primeiro. É onde o retorno é mais previsível e mais fácil de defender internamente.
  4. Testar io_uring só se você controla o kernel, mediu que o gargalo é CPU de I/O, e tem ambiente de homologação para comparar. Caso contrário, o ganho marginal não paga o risco operacional.

A lição maior do primeiro ano do PG18 não é sobre I/O assíncrono. É sobre resistir à tentação de ligar o recurso da manchete antes de saber onde o seu tempo está indo. O AIO é uma peça de engenharia excelente — e completamente irrelevante para metade dos bancos que vão atualizar.

Perguntas frequentes

Preciso mudar alguma coisa para usar o I/O assíncrono?

Não. O io_method já vem em worker no PG18, então o AIO está ativo desde o primeiro boot. O ajuste fino é opcional.

io_uring é sempre melhor que worker?

Em consumo de CPU por operação, sim. Em disponibilidade, não: exige kernel Linux recente e permissão que muitos ambientes gerenciados não concedem. A escolha na prática costuma ser feita pelo ambiente, não por você.

Vale esperar o PostgreSQL 19 por causa do pool dinâmico de workers?

Não. O PG18 já entrega o ganho principal, e o pool dinâmico é refinamento operacional. Adiar um upgrade inteiro por causa de quatro GUCs é otimizar a coisa errada.

UUIDv7 resolve o problema de fragmentação de índice de vez?

Resolve a parte causada pela aleatoriedade da chave. Ele não esconde a origem do valor: como o timestamp fica visível nos bits iniciais, um UUIDv7 exposto publicamente revela quando o registro foi criado. Se isso for sensível no seu domínio, mantenha o v4 na fronteira externa.

Fontes

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *