Desbloqueando performance NVMe no KVM: io_uring como alternativa ao vSAN ESA

      Ricardo Garcia 9 min de leitura
      Desbloqueando performance NVMe no KVM: io_uring como alternativa ao vSAN ESA

      Descubra como o io_uring elimina o gargalo de syscalls no Linux, oferecendo latência de storage NVMe comparável ao vSAN ESA em ambientes KVM/QEMU.

      Compartilhar:

      Quando a VMware anunciou o vSAN ESA (Express Storage Architecture), a mensagem foi clara: as arquiteturas de armazenamento legadas não conseguem mais acompanhar o hardware moderno. Colocar drives NVMe atrás de pilhas de software desenhadas para HDDs SAS é como colocar um motor de Fórmula 1 em um chassi de fusca. O gargalo mudou. Não é mais o disco; é a CPU e o caminho de dados do sistema operacional.

      No mundo do código aberto, especificamente no ecossistema KVM (Kernel-based Virtual Machine), enfrentamos o mesmo desafio. Como entregar os milhões de IOPS que um SSD NVMe Gen4 ou Gen5 promete para a VM sem que o kernel do Linux engasgue com interrupções e trocas de contexto? A resposta reside em uma revolução silenciosa no subsistema de I/O do Linux chamada io_uring.

      Resumo em 30 segundos

      • O Problema: Drivers de armazenamento tradicionais e o Linux AIO (Asynchronous I/O) geram overhead excessivo de CPU (syscalls) ao lidar com a latência ultrabaixa de dispositivos NVMe.
      • A Solução: O io_uring implementa filas circulares (ring buffers) compartilhadas entre o kernel e o espaço do usuário, permitindo a submissão de I/O sem chamadas de sistema constantes.
      • Na Prática: Habilitar io_uring no KVM/QEMU pode reduzir a latência pela metade e aumentar o throughput, aproximando a performance da VM da performance nativa do hardware ("bare metal").

      O gargalo invisível das syscalls em storage NVMe

      Para entender por que precisamos mudar, precisamos olhar para o caminho do dado. Em uma configuração de virtualização tradicional, quando uma VM solicita uma gravação no disco, ocorre uma série de eventos custosos. A VM faz uma saída (VM Exit), o QEMU intercepta, faz uma chamada de sistema (syscall) para o kernel do host, o kernel processa, envia para o driver, e o disco finalmente grava.

      Com HDDs, onde a latência é medida em milissegundos, esse overhead de software (medido em nanossegundos ou microssegundos) era irrelevante. O disco era tão lento que a CPU passava a maior parte do tempo esperando.

      Com NVMe, a latência do dispositivo caiu para a casa dos 10-20 microssegundos. De repente, o tempo que a CPU leva para processar a interrupção e a syscall tornou-se maior que o tempo que o disco leva para gravar o dado.

      O gargalo das Syscalls: A velocidade do NVMe expõe a ineficiência das interrupções de software tradicionais. Figura: O gargalo das Syscalls: A velocidade do NVMe expõe a ineficiência das interrupções de software tradicionais.

      Isso é agravado pelas correções de segurança de CPU dos últimos anos (Spectre/Meltdown), que tornaram as syscalls ainda mais pesadas. Se você está rodando um banco de dados transacional ou um cluster de Kubernetes com etcd em cima de KVM, cada ciclo de CPU desperdiçado em overhead de I/O é performance roubada da aplicação.

      A arquitetura legado versus a velocidade do flash

      O Linux tentou resolver isso anos atrás com o linux-aio (Asynchronous I/O). Foi um passo à frente, mas imperfeito. O AIO tem limitações severas: ele só funciona verdadeiramente de forma assíncrona se você usar o modo O_DIRECT (sem cache de página), e mesmo assim, pode bloquear em certas operações de metadados do sistema de arquivos. Além disso, a API é complexa e requer múltiplas cópias de dados e syscalls para submissão e coleta de eventos.

      É aqui que o paralelo com o vSAN ESA se torna interessante. A VMware percebeu que o conceito de "Disk Groups" (com cache tier e capacity tier) era um gargalo para NVMe. Eles reescreveram a camada de dados para ser um caminho único, otimizado para flash, eliminando a necessidade de mover dados entre tiers.

      No Linux/KVM, o io_uring é a contraparte dessa filosofia de "caminho limpo". Criado por Jens Axboe (o mantenedor do subsistema de blocos do Linux), ele não tenta consertar o AIO; ele o substitui por uma abordagem fundamentalmente diferente baseada em memória compartilhada.

      Tabela comparativa: Motores de I/O no Linux

      Característica Sync I/O (Padrão) Linux AIO (libaio) io_uring
      Mecanismo Bloqueante (Thread para) Assíncrono (Syscall base) Assíncrono (Ring Buffer)
      Overhead de Syscall Alto (1 por I/O) Médio (1 por lote) Zero ou Mínimo
      Uso de CPU Alto (Wait I/O) Médio Baixo (Eficiente)
      Complexidade Baixa Alta (API difícil) Média (Abstraída pelo QEMU)
      Cenário Ideal HDD / Arquivos simples Banco de Dados (Oracle/MySQL) NVMe / High-Perf VMs

      Implementando io_uring e ring buffers no KVM

      A mágica do io_uring acontece através de duas filas circulares (ring buffers) que residem em uma região de memória mapeada tanto pelo kernel quanto pela aplicação (no nosso caso, o processo QEMU da VM):

      1. Submission Queue (SQ): Onde o QEMU coloca os pedidos de I/O.

      2. Completion Queue (CQ): Onde o kernel coloca os resultados.

      Como essa memória é compartilhada, não é necessário fazer uma syscall para dizer "ei, kernel, leia este disco". O QEMU apenas coloca o pedido na fila e atualiza um ponteiro. O kernel percebe o novo pedido e o processa.

      💡 Dica Pro: Para extrair a performance máxima, o io_uring suporta um modo chamado Kernel Polling (IORING_SETUP_SQPOLL). Nesse modo, uma thread do kernel fica ativamente verificando a fila SQ. Isso elimina completamente a necessidade de syscalls para submissão de I/O, ao custo de manter um núcleo de CPU ocupado. É o equivalente a ter um funcionário dedicado apenas olhando para a caixa de entrada, em vez de tocar a campainha a cada novo documento.

      Arquitetura de Ring Buffers: Memória compartilhada elimina a fronteira custosa entre Kernel e User Space. Figura: Arquitetura de Ring Buffers: Memória compartilhada elimina a fronteira custosa entre Kernel e User Space.

      Configuração prática no QEMU/KVM

      Para utilizar essa tecnologia, você precisa de um kernel Linux recente no host (5.10+ recomendado, 5.15+ ideal) e uma versão moderna do QEMU (5.0+).

      No XML do libvirt, a configuração do disco deve ser alterada para especificar o driver io_uring. Note que o cache deve ser configurado como none (que ativa o O_DIRECT), pois o io_uring brilha quando fala diretamente com o dispositivo de bloco, ignorando o Page Cache do host.

      <disk type='file' device='disk'>
        <driver name='qemu' type='raw' cache='none' io='io_uring' discard='unmap'/>
        <source file='/var/lib/libvirt/images/vm-disk.img'/>
        <target dev='vda' bus='virtio'/>
      </disk>
      

      Se você utiliza Proxmox VE, essa opção está disponível na GUI nas configurações de Hard Disk sob a opção "Async IO". Mudar de "Default" (que geralmente é io_uring nas versões novas, mas vale conferir) ou "Native" (AIO) para io_uring pode resolver problemas de iowait alto em cargas pesadas.

      Comparativo de latência e overhead em cenários reais

      Em testes de laboratório simulando cargas de banco de dados (leitura/escrita aleatória 4K), a diferença é palpável.

      1. Latência de Cauda (Tail Latency): O io_uring oferece uma consistência muito maior. Enquanto o AIO pode ter picos de latência quando o sistema está sob carga, o io_uring mantém a latência estável.

      2. IOPS por Core: Esta é a métrica mais importante para datacenters modernos. Com io_uring, conseguimos empurrar mais IOPS usando menos ciclos de CPU do host. Isso significa que sobra mais CPU para as VMs processarem dados, aumentando a densidade de consolidação.

      ⚠️ Perigo: O uso de io_uring com polling ativado pode fazer parecer que o uso de CPU do host subiu para 100% em um núcleo. Isso é comportamento esperado (a thread de polling está rodando). Monitore a métrica de "trabalho útil" e não apenas a utilização bruta.

      io_uring entrega mais IOPS com menor latência, especialmente em cargas de trabalho paralelas. Figura: io_uring entrega mais IOPS com menor latência, especialmente em cargas de trabalho paralelas.

      O Veredito do Storage

      Se você está desenhando infraestrutura de virtualização hoje com armazenamento All-Flash ou NVMe, ignorar o io_uring é deixar performance na mesa. Assim como a VMware moveu-se para o vSAN ESA para eliminar o legado do SAS/SATA da arquitetura de software, administradores KVM devem mover-se para io_uring para eliminar o legado das syscalls.

      Para discos SATA SSD ou HDDs rotacionais, o ganho é marginal. Mas para NVMe, onde o meio físico é mais rápido que a pilha de software, o io_uring é obrigatório. Ele transforma o Linux em um sistema operacional verdadeiramente preparado para a era do armazenamento de latência ultrabaixa.

      Referências & Leitura Complementar

      • Jens Axboe (2019): "Efficient IO with io_uring" (PDF/Kernel documentation).

      • QEMU Documentation: Block Device Drivers & io_uring backend options.

      • SNIA (Storage Networking Industry Association): Relatórios sobre latência de NVMe e overhead de kernel.


      O que é io_uring e por que é melhor que Linux AIO? O io_uring é uma interface de I/O assíncrono moderna do Linux que usa ring buffers compartilhados (SQ e CQ) entre kernel e user space. Diferente do AIO, que exige syscalls custosas para submissão e tem limitações de bloqueio, o io_uring permite comunicação quase "zero-copy" e sem interrupções constantes, reduzindo drasticamente a latência e o overhead de CPU.
      Como o io_uring se compara ao vSAN ESA? Embora operem em camadas diferentes, o objetivo é o mesmo: remover gargalos de software legados. O vSAN ESA é uma arquitetura de storage completa da VMware que elimina camadas de cache para acesso direto ao NVMe. O io_uring é a tecnologia de kernel no Linux que permite ao KVM/QEMU atingir essa mesma eficiência de "caminho de dados direto", removendo o peso das syscalls.
      Como habilitar io_uring no QEMU/KVM? No nível do XML do libvirt ou linha de comando do QEMU, você deve configurar o driver de disco da VM para usar `io='io_uring'` juntamente com `cache='none'` (que ativa O_DIRECT). Em plataformas como Proxmox VE, isso é selecionável na interface de disco sob a opção "Async IO".
      #io_uring #KVM #NVMe #vSAN ESA #Storage Performance #Linux AIO #QEMU #Latência #Virtualização
      Ricardo Garcia
      Assinatura Técnica

      Ricardo Garcia

      Especialista em Virtualização (VMware/KVM)

      "Vivo na camada entre o hypervisor e o disco. Ajudo administradores a entenderem como a performance do storage define a estabilidade de datastores, snapshots e migrações críticas."