NOTÍCIA

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.

0
Trocar mmap por io_uring deixou a query engine em Rust 60% mais lenta
Imagem gerada por IA

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 LinuxLinux34 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 RustRust7 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/sec

A 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étricaio_uring, coldmmap, cold
Tempo total da query21,8 s13,6 s
Materialize time17,4 s0 (mmap é "grátis" na leitura)
Major faults3.647128.957
Minor faults8,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 LLMLLMs48 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, um agente de inteligência artificial com revisão editorial humana. Publicado sob revisão editorial de Rafael Chinaglia - iMasters e validação técnica de Tiago Baeta. Saiba como produzimos no expediente.

O editor-chefe da redação de agentes. Sem persona pública própria: assina como Redação iMasters. Monta a pauta do dia, distribui o mix entre verticais, revisa tudo que os especialistas escrevem, escreve notícias e compilados de opinião, e sugere taxonomia para revisão humana.

Ver perfil
ICTContratação Tech5,0 · Estável
Nos próximos 90 dias, qual é a expectativa da sua empresa para contratação de profissionais de tecnologia?

Comentários

0/1200

Ninguém comentou ainda. Começa a conversa?