Trocar mmap por io_uring deixou a query engine em Rust 60% mais lenta
A Conviva relatou que migrar a leitura de arquivos Arrow IPC de mmap para io_uring com O_DIRECT piorou a latência antes de melhorar. O caso serve de alerta contra o hype.

Existe um consenso implícito rondando os fóruns de engenharia de alta performance: mmap é conveniente mas escala mal, e io_uring é o futuro do I/O no Linux↳Linux34 conteúdosKali Linux em um Servidor VPS: como, quando e por que usar?DevSecOps · dez 2024Construindo um Windows Service ou Linux Daemon com Worker Service & .NET Core – Parte 2Dev (Back & Front) · jul 2020Criando uma WebApi utilizando .NET, Linux e VSCodeDev (Back & Front) · ago 2019Ver tudo em DevSecOps →. A Conviva, empresa que analisa trilhões de eventos por dia para diagnosticar experiência de usuário, resolveu fazer a troca na sua engine de queries escrita em Rust↳Rust7 conteúdosDesmistificando Rust: a linguagem segura e rápida que você precisa conhecerDev (Back & Front) · out 2024Como criar seu primeiro Programa em Rust com Solana PlaygroundDev (Back & Front) · mai 2026Rust no ranking: a segurança de memória perdeu a guerra corporativa?Marketing Tech · abr 2026Ver tudo em Dev (Back & Front) →. O resultado, contado pelo engenheiro Evan Chan, foi o oposto do prometido: a versão com io_uring ficou cerca de 60% mais lenta que o baseline de mmap.
O relato é útil justamente porque contraria o roteiro feliz que domina os blogs. Para quem constrói sistemas de dados no Brasil, e cada vez mais times aqui operam engines de análise sobre NVMe local, é um lembrete de que substituir uma primitiva de I/O por outra sem redesenhar a arquitetura em volta costuma sair pela culatra.
O que estava rodando antes
A engine da Conviva é montada sobre DataFusion, Arrow, Rust, Rayon e Tokio. Eventos são transformados, codificados num formato proprietário majoritariamente numérico e guardados na nuvem. Para consulta, os arquivos são copiados para NVMe local e lidos como grandes arquivos Arrow IPC de 3 a 5 GB.
A escolha do Arrow IPC tinha lógica: o layout em memória e em disco é idêntico, então o custo de decode é mínimo, e mmap entrega leitura zero-copy nativamente suportada pelo arrow-rust. Uma query típica toca 6 colunas em 8 arquivos de batch (um batch por arquivo), cerca de 1,6 GB por batch, algo como 13 GB de dados por dia.
Em carga leve, funcionava bem, respondendo em segundos. O problema apareceu sob concorrência pesada.
O sintoma em produção
Algum aumento de latência sob carga é esperado, mais queries disputando a mesma CPU. Mas o p95 e o p99 dispararam muito além do que o crescimento linear preveria. Os sintomas, segundo o relato:
- O page cache do SO encolheu: cada pod consumia mais memória como alocação privada e menos como cache compartilhado.
- Um número enorme de page faults.
- O p95 saltou de cerca de 30s para mais de 150s sob concorrência real.
- Adicionar pods piorava, não melhorava.
O diagnóstico apontou para thrashing do page cache do mmap sob pressão de memória. Um teste controlado tornou isso explícito: 1 pod venceu 4 pods na mesma máquina, sendo 41% mais rápido no pico e mais de 20% no p95 para queries de 14 dias. O perf record mostrou 100% de contenção de lock no nível do kernel. Os quatro pods não brigavam por CPU, brigavam pelo page cache, que é estado compartilhado implícito: um cache, uma hierarquia de locks, uma política de eviction para todos os processos do host.
Os números de pidstat durante uma execução estressante ilustram a tempestade de faults:
23:27:09 1,255,709 minor faults/sec
23:27:41 2,124,327 minor faults/sec
23:27:46 2,354,383 minor faults/secA mais de 2 milhões de minor faults por segundo, cada um tocando uma cache line via atômicos, o L1/L2 é destruído, algo letal para uma aplicação que depende de grandes lookup tables residentes em cache. Os context switches passaram de 14 mil/s numa execução com cache quente para mais de 2 milhões/s, 150x mais.
O que io_uring prometia resolver
O teto de hardware media o tamanho do prêmio. Um fio com engine io_uring, 4 processos, iodepth 32, blocos de 4 MiB e O_DIRECT atingiu 20,2 GiB/s com os 32 NVMe a 99,75% de utilização. O mmap, no pico, entregava 3,44 GB/s, cerca de 16% do que o hardware podia fazer.
O plano era clássico: bypassar o page cache com O_DIRECT, submeter leituras via io_uring, coordenar com Tokio e decodificar Arrow inline. A implementação usou compio, um wrapper Rust-nativo de io_uring. A primeira versão disparava uma future por coluna, todas as 40 colunas (8 batches × 5 colunas) submetidas concorrentemente.
O choque de realidade no Linux
O teste inicial no macOS (sem io_uring real) já não animava, mas foi no Linux que a conta apareceu:
| Métrica | io_uring, cold | mmap, cold |
|---|---|---|
| Tempo total da query | 21,8 s | 13,6 s |
| Materialize time | 17,4 s | 0 (mmap é "grátis" na leitura) |
| Major faults | 3.647 | 128.957 |
| Minor faults | 8,6 milhões | ~1 milhão |
O io_uring resolveu exatamente o que deveria: os major faults caíram 35x, de 128.957 para 3.647. O kernel parou de thrashear em page-ins físicos. Mas os minor faults subiram 8x, e o tempo total foi de 13,6s para 21,8s. Uma classe de fault foi trocada por outra, e o negócio saiu no prejuízo.
Onde os minor faults se escondiam
Depois de ligar O_DIRECT (runtime caiu de 21,8s para ~19s, ganho real mas modesto), o perf apontou para o Arrow. As amostras concentravam em Buffer::from_slice_ref, a forma padrão do arrow-rs de construir um Buffer a partir de um slice de bytes: aloca memória nova e faz memcpy.
Cada página de destino de 4 KiB que o memcpy toca precisa que o kernel a zere e mapeie, um minor fault por página. Os 8 milhões de minor faults sobre ~13 GB de leituras batem quase exatamente com essa conta. Ou seja: a camada Arrow estava forçando o kernel a refazer o trabalho de gerência de memória que a equipe achava ter bypassado ao adotar io_uring. Construir o Buffer direto dos bytes que o io_uring já possuía baixou o runtime para ~16s, ainda pior que o mmap.
A comunidade não perdoou (e apontou o ponto técnico)
O thread no Hacker News reagiu em duas frentes. A primeira, ao formato: vários leitores acusaram o texto de ser gerado por LLM↳LLMs48 conteúdosConsiderações básicas de hardware para modelos de linguagem em código aberto: Memória, Desempenho e ViabilidadeMarketing Tech · out 2025Modelos de linguagem sob ataque: o lado obscuro da IA generativaDevSecOps · mai 2025Criando um LLM – modelo de linguagem de grande escala – do zero com TransformersAI · abr 2024Ver tudo em AI →. Como resumiu muragekibicho: "Seeing a section titles "The Production Symptom" is a Claudism in itself."
A segunda frente foi mais interessante para quem constrói. samus apontou que o título talvez estivesse mirando na primitiva errada:
"It would have been more precise to say O_DIRECT instead of io_uring. The point of io_uring is to avoid syscall overhead. What they were after was actually managing a page cache on their own, and it turned out to be more complicated than they thought."
samus
E jandrewrogers foi ao cerne arquitetural: "mmap and io_uring require fundamentally different software architectures in a performance context. You shouldn’t swap them out." Segundo ele, io_uring com O_DIRECT existe para você desenhar seu próprio scheduler em espaço de usuário; se você delega o scheduling a um runtime, perde a maior parte da vantagem, e a performance pode até piorar. Foi mais ou menos o que aconteceu.
Vale registrar a ressalva de laserbeam: existe um Part 2, linkado no fim do artigo, em que a equipe torna o io_uring duas vezes mais rápido que o mmap. Ou seja, o título é o gancho de uma série, não a conclusão final.
O que fica para quem constrói no Brasil
O recado não é "io_uring é ruim". É que a primitiva de I/O não é uma peça plug-and-play. Trocar mmap por io_uring mantendo o resto da arquitetura (um único thread de materialização fazendo cinco trabalhos, decode do Arrow com cópia default, prefetch disparando 40 SQEs de uma vez) transfere o gargalo em vez de eliminá-lo.
Para times brasileiros que rodam DataFusion, Arrow ou engines analíticas próprias sobre NVMe local, três lições concretas emergem do relato: o custo real muitas vezes não está no disco (a I/O de NVMe respondeu por só 6,9% do tempo off-CPU medido), o page cache compartilhado do host vira estado global disputado quando você empilha pods, e a cópia default do arrow-rs pode reintroduzir exatamente os faults que você tentou evitar. O restante da história (o momento em que perceberam que 40 SQEs concorrentes eram o problema) fica para a Parte 2, que Chan promete detalhar na P99 CONF, online em 21 e 22 de outubro de 2026.
Fontes: Hacker News · Reações no Hacker News
Este artigo foi escrito por Redação iMasters. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.
ByteDance explica falha de recuperação em contexto longo nos modelos DeepSeek
Pesquisa do time Seed da ByteDance identifica a causa técnica da recuperação inconsistente em contextos longos nos modelos DeepSeek, com variações de até 40 pontos percentuais conforme a posição da informação no cache comprimido.









