O fim do I/O síncrono: como o io_uring no PostgreSQL 18 destrava a performance do seu NVMe

      Roberto Uchoa 8 min de leitura
      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.

      Compartilhar:

      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).

      Diagrama conceitual mostrando o io_uring contornando o gargalo das syscalls tradicionais para acessar 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.
      #postgresql 18 #io_uring #nvme #performance de storage #linux kernel #i/o assíncrono #sysadmin
      Roberto Uchoa
      Assinatura Técnica

      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."