O fim do I/O síncrono: como o io_uring no PostgreSQL 18 destrava a performance do seu NVMe
Cansado de ver seu NVMe de última geração engasgar no banco de dados? Entenda como a adoção do io_uring no PostgreSQL 18 elimina o overhead de syscalls e resolve o problema de I/O de uma vez por todas.
Você convenceu a diretoria a assinar um cheque gordo para comprar drives NVMe enterprise de última geração. O servidor chega, você espeta os discos no barramento PCIe, sobe o banco de dados e vai monitorar o utilitário iostat. O resultado é deprimente. O disco está praticamente dormindo, operando a uma fração da sua capacidade teórica, enquanto a CPU do servidor está engasgada em estado de espera por I/O.
O problema nunca foi o seu hardware de armazenamento. O problema é que o software está tentando conversar com um disco do século 21 usando uma arquitetura de comunicação desenhada na época em que os discos rígidos faziam barulho de máquina de lavar. O gargalo mudou de lugar e agora ele reside no próprio kernel do sistema operacional.
Resumo em 30 segundos
- O I/O síncrono tradicional destrói a performance de discos NVMe devido ao excesso de trocas de contexto (context switches) na CPU.
- A interface io_uring resolve isso usando anéis de memória compartilhada entre a aplicação e o kernel, eliminando a sobrecarga de chamadas de sistema.
- A adoção nativa dessa arquitetura no PostgreSQL permite finalmente saturar o barramento PCIe, entregando os IOPS pelos quais você pagou.
A anatomia do gargalo no kernel Linux
Para entender o desastre da performance, precisamos olhar para a mecânica básica de leitura e escrita. No modelo tradicional de I/O síncrono, quando o PostgreSQL precisa ler uma página de dados do disco, ele emite uma chamada de sistema (syscall). A CPU precisa parar o que está fazendo no espaço de usuário, trocar para o modo kernel, solicitar o dado ao driver de armazenamento e colocar a thread para dormir.
Quando o disco finalmente responde, a CPU é interrompida novamente, acorda a thread e copia o dado do kernel para a memória da aplicação. Esse processo de troca de contexto leva alguns microssegundos.
Na era dos discos magnéticos (HDDs), onde o braço mecânico demorava milissegundos para encontrar o dado, esse tempo de CPU era irrelevante. O disco era tão lento que o kernel tinha tempo de sobra para gerenciar a burocracia.
Hoje, um drive NVMe moderno conectado via PCIe Gen4 ou Gen5 responde em poucos microssegundos. A latência da mídia de armazenamento flash tornou-se menor do que a latência do próprio sistema operacional para processar a interrupção. O resultado é um banho de sangue de ciclos de CPU desperdiçados apenas gerenciando a fila de espera, impedindo que a controladora do disco receba comandos rápido o suficiente.
⚠️ Perigo: Tentar resolver esse problema aumentando o número de conexões ou threads no banco de dados apenas piora a situação. Você não está aumentando o fluxo de dados para o storage, está apenas multiplicando o número de colisões e trocas de contexto na CPU.
A engenharia do io_uring e o fim das syscalls
A comunidade Linux tentou resolver o I/O assíncrono no passado com o libaio, mas a implementação era cheia de restrições, funcionava mal com I/O em buffer e o código era um pesadelo de manter. Foi então que Jens Axboe, um dos principais mantenedores do subsistema de blocos do Linux, criou o io_uring.
A genialidade do io_uring está na simplicidade brutal de evitar o kernel sempre que possível. Ele cria duas estruturas de dados em anel na memória: a Fila de Submissão (Submission Queue ou SQ) e a Fila de Conclusão (Completion Queue ou CQ).
Esses anéis são mapeados em uma área de memória compartilhada entre o espaço de usuário (onde roda o banco de dados) e o espaço do kernel (que fala com o NVMe).
Figura: Diagrama conceitual mostrando o io_uring contornando o gargalo das syscalls tradicionais para acessar o NVMe
Quando o banco de dados quer ler ou escrever no disco, ele simplesmente coloca a requisição na Fila de Submissão. O kernel, rodando de forma independente, consome essa fila, envia os comandos para a controladora NVMe e, quando o disco termina, coloca o resultado na Fila de Conclusão. A aplicação apenas lê o resultado.
Se configurado no modo de polling, isso significa zero chamadas de sistema. Zero trocas de contexto. A CPU apenas empilha comandos na memória na velocidade da luz, permitindo que a controladora de armazenamento finalmente trabalhe no seu limite.
Comparativo de interfaces de armazenamento
Para ilustrar o abismo técnico entre as abordagens, veja como as diferentes interfaces lidam com a comunicação entre o banco de dados e o seu storage:
| Característica | I/O Síncrono (read/write) | libaio (Legado) | io_uring (Moderno) |
|---|---|---|---|
| Sobrecarga de CPU | Altíssima (1 syscall por I/O) | Média | Quase nula (Memória compartilhada) |
| Trocas de Contexto | Constantes e bloqueantes | Reduzidas, mas presentes | Zero (com SQ polling ativado) |
| Suporte a Buffered I/O | Sim | Terrível (frequentemente bloqueia) | Excelente e nativo |
| Saturação de NVMe | Impossível em altas cargas | Difícil e instável | Total (atinge o limite do PCIe) |
Saturando o barramento PCIe na prática
O ecossistema de banco de dados demorou para reescrever seus motores de armazenamento para aproveitar essa tecnologia. O PostgreSQL, historicamente dependente do cache de página do sistema operacional e de I/O síncrono, iniciou uma jornada massiva de refatoração de sua arquitetura interna.
Com a implementação de I/O assíncrono real utilizando io_uring, o motor do banco de dados para de ser o gargalo. Em testes de estresse com cargas de trabalho intensivas em leitura aleatória, servidores que antes batiam 100% de uso de CPU entregando 200 mil IOPS, agora conseguem entregar mais de 1 milhão de IOPS com a CPU trabalhando com folga.
Isso muda completamente a matemática do dimensionamento de servidores. Você passa a extrair o valor real dos drives Enterprise & Data Center Standard Form Factor (EDSFF) que comprou. A latência de cauda, aquele atraso irritante que ocorre no percentil 99 das requisições, despenca drasticamente porque as requisições de I/O não ficam mais presas em filas de bloqueio do sistema operacional.
💡 Dica Pro: O io_uring foi introduzido no kernel Linux 5.1, mas você vai querer rodar seu ambiente de banco de dados em kernels 5.15 ou superiores (idealmente a série 6.x). As versões iniciais tinham bugs de segurança e limitações de performance que já foram resolvidos nas ramificações mais recentes.
O veredito pragmático para a sua infraestrutura
A adoção do io_uring não é uma bala de prata mágica que vai consertar consultas SQL mal escritas ou a falta de índices no seu banco de dados. No entanto, se o seu monitoramento indica que o gargalo da sua infraestrutura é o tempo de espera de I/O, enquanto os discos NVMe reportam baixa utilização de banda, essa é a mudança arquitetural que você estava esperando.
Antes de virar a chave no seu ambiente de produção, valide a versão do kernel dos seus servidores e garanta que o sistema de arquivos subjacente (como XFS ou ext4) está montado com as opções corretas para evitar gargalos de metadados. A tecnologia de armazenamento finalmente forçou o software a evoluir. Pare de desperdiçar ciclos de processador com burocracia de sistema operacional e deixe seus discos trabalharem.
Referências & Leitura Complementar
AXBOE, Jens. Efficient IO with io_uring. Kernel.dk, 2019.
NVM Express. NVMe Base Specification. NVMexpress.org.
PostgreSQL Global Development Group. Asynchronous I/O and io_uring support. PostgreSQL Mailing Lists.
O que é io_uring e por que ele é superior ao antigo libaio?
O io_uring é uma interface de I/O assíncrona moderna do kernel Linux que utiliza anéis de submissão e conclusão em memória compartilhada entre o espaço de usuário e o kernel. Isso elimina a necessidade de chamadas de sistema (syscalls) constantes para cada operação de leitura ou escrita, superando as limitações históricas e a complexidade do libaio.Preciso comprar discos NVMe novos para aproveitar o io_uring no PostgreSQL?
Não. O ganho de performance vem justamente de parar de desperdiçar ciclos de CPU com context switches inúteis. O io_uring permite que os discos NVMe que você já possui no seu storage finalmente operem próximos ao seu limite teórico de IOPS, sem que a CPU seja o gargalo.O io_uring funciona em qualquer sistema operacional ou hypervisor?
Não, o io_uring é uma tecnologia nativa e exclusiva do kernel Linux (introduzida na versão 5.1). Se você roda seu banco de dados em FreeBSD ou Windows Server, dependerá das APIs de I/O assíncrono nativas desses sistemas (como kqueue ou IOCP), que o PostgreSQL também vem otimizando.
Roberto Uchoa
Sysadmin Veterano (Anti-Hype)
"Sobrevivente da bolha pontocom e do hype do Kubernetes. Troco qualquer arquitetura de microsserviços 'inovadora' por um script bash que funciona sem falhas há 15 anos. Uptime não é opcional."