O custo oculto dos schedulers de I/O: como locks no blk-mq estrangulam a performance do NVMe
Descubra por que schedulers como mq-deadline e bfq destroem a performance de SSDs NVMe. Entenda a arquitetura blk-mq, o impacto dos spinlocks na CPU e como otimizar o kernel Linux para latência de microssegundos.
Você investe milhares de dólares em arrays de armazenamento baseados em NVMe PCIe Gen4 ou Gen5, capazes de entregar mais de um milhão de IOPS por unidade. O datasheet promete latências na casa dos microssegundos. Porém, quando a sua aplicação de banco de dados atinge o pico de tráfego, o monitoramento dispara alertas de latência de cauda na casa dos milissegundos.
Ao abrir o terminal e rodar um simples top ou mpstat, você nota que a CPU está cravada em 100% de uso, com a maior parte desse tempo classificada como iowait ou system. O hardware não está gargalando. O barramento PCIe tem banda de sobra. O verdadeiro culpado está escondido no espaço de núcleo do seu sistema operacional: o scheduler de I/O.
Resumo em 30 segundos
- Dispositivos NVMe possuem múltiplas filas de hardware nativas, tornando o enfileiramento por software redundante e custoso.
- Schedulers tradicionais como
mq-deadlineebfqintroduzem locks de CPU que destroem a performance em altas taxas de IOPS.- Alterar a política do disco para
nonefaz o bypass do scheduler, reduzindo a latência de cauda e liberando ciclos preciosos de CPU.
A anatomia da latência de cauda e o pesadelo do iowait
Para entender o desperdício de CPU, precisamos olhar para o caminho que um bloco de dados percorre. Quando uma aplicação faz uma chamada de sistema (syscall) para ler ou escrever no disco, essa requisição desce pela pilha de armazenamento do kernel. Em cenários de alta concorrência, milhares de threads tentam acessar o dispositivo simultaneamente.
Se o kernel tenta organizar, fundir ou priorizar essas requisições em software antes de enviá-las ao hardware, ele precisa usar mecanismos de sincronização, como spinlocks. Um spinlock força o núcleo da CPU a ficar em um loop infinito verificando se um recurso foi liberado. Isso gera um consumo massivo de ciclos de processamento que não realizam trabalho útil.
O resultado direto dessa contenção de locks é o aumento do iowait. Embora muitos pensem que o iowait significa apenas que o disco é lento, em arquiteturas modernas ele frequentemente indica que a CPU está bloqueada esperando a resolução de filas de software superlotadas. A latência de cauda (o percentil 99 ou 99.9 das requisições) explode porque algumas operações ficam presas aguardando a liberação desses locks no kernel.
A colisão arquitetural no blk-mq
Historicamente, o Linux utilizava uma única fila de requisições para discos. Isso funcionava bem para discos rígidos (HDDs), onde a agulha mecânica era o gargalo absoluto. Com a chegada dos SSDs rápidos, essa fila única se tornou um desastre de performance. Para resolver isso, a comunidade introduziu o framework blk-mq (Block Multi-Queue).
O blk-mq mapeia múltiplas filas de submissão de software (geralmente uma por núcleo lógico de CPU) para múltiplas filas de hardware no controlador do dispositivo. O protocolo NVMe foi desenhado exatamente para isso, suportando até 64 mil filas de submissão (Submission Queues - SQ) e de completamento (Completion Queues - CQ), cada uma com até 64 mil comandos.
Figura: Arquitetura do blk-mq mostrando o afunilamento causado pelo scheduler de I/O antes de atingir as filas de hardware do NVMe.
O problema ocorre quando ativamos um scheduler de I/O complexo dentro da camada blk-mq. Em vez de a requisição fluir diretamente da fila de software para a fila de hardware do NVMe, ela é interceptada. O kernel tenta aplicar heurísticas de ordenação, o que exige coordenação entre os núcleos da CPU, quebrando o paralelismo elegante que o blk-mq e o NVMe tentam construir.
A ilusão de trocar mq-deadline por bfq
Muitos administradores de sistemas, ao notarem problemas de performance, tentam otimizar o ambiente trocando o scheduler padrão. O mq-deadline tenta garantir um tempo máximo de espera para cada requisição, evitando a inanição (starvation) de leituras. Já o bfq (Budget Fair Queueing) foca em distribuir a banda de I/O de forma justa entre diferentes processos.
Trocar o mq-deadline pelo bfq em um array NVMe é um erro clássico de engenharia de performance. O bfq é extremamente pesado em termos computacionais. Ele foi desenhado para garantir responsividade em sistemas com discos lentos ou SSDs SATA, onde a latência do barramento justifica o custo de CPU para organizar a fila.
Em um dispositivo NVMe capaz de responder em 20 microssegundos, o tempo que a CPU gasta executando o algoritmo do bfq pode ser maior do que o tempo que o disco levaria para simplesmente gravar o dado. Você não está resolvendo o gargalo, está apenas movendo ele do barramento PCIe para a sua CPU.
Comparativo de Schedulers de I/O
| Scheduler | Foco Principal | Overhead de CPU | Ideal para |
|---|---|---|---|
| bfq | Justiça entre processos e responsividade | Muito Alto | HDDs, SSDs SATA lentos, Desktops |
| mq-deadline | Prevenção de inanição (starvation) | Médio | SSDs SATA Enterprise, SANs legadas |
| none | Bypass total, submissão direta | Quase Zero | NVMe PCIe, Arrays All-Flash modernos |
O bypass do scheduler com a política none
A solução para extrair o máximo de IOPS do seu hardware de armazenamento é surpreendentemente simples: não faça agendamento em software. Ao configurar a política do scheduler para none, você instrui o kernel Linux a fazer o bypass completo das heurísticas de ordenação.
Com a política none, quando uma thread de aplicação submete um I/O, a requisição entra na fila de software do blk-mq e é imediatamente despachada para o ring buffer da fila de submissão de hardware do controlador NVMe. Não há tentativas de fusão de blocos (merging) complexas. Não há spinlocks pesados de reordenação.
Figura: Comparativo visual do caminho de dados: a complexidade do mq-deadline versus a submissão direta com a política none.
O controlador de hardware do NVMe possui processadores ARM internos altamente otimizados para lidar com a ordem de execução dos comandos. Deixe o silício dedicado fazer o trabalho para o qual foi projetado. Ao remover o kernel do caminho, a latência despenca e os ciclos de CPU são devolvidos para a sua aplicação de banco de dados ou hypervisor.
💡 Dica Pro: Para testar o impacto imediatamente sem reiniciar o servidor, você pode alterar o scheduler em tempo de execução injetando o valor no sysfs:
echo none > /sys/block/nvme0n1/queue/scheduler. Monitore a queda no uso de CPU em seguida.
Comprovando a redução de latência com eBPF e fio
Em engenharia de performance, não confiamos em suposições. Precisamos de dados empíricos. Para provar o custo dos schedulers, utilizamos geradores de carga sintética como o fio (Flexible I/O Tester) combinados com ferramentas de observabilidade baseadas em eBPF (Extended Berkeley Packet Filter).
Ao rodar um teste de leitura randômica de 4KB com alta profundidade de fila (iodepth=128) usando o mq-deadline, podemos usar scripts do bcc-tools, como o biolatency, para capturar o histograma de latência. Em seguida, usamos o profile ou criamos um Flame Graph para mapear o tempo de CPU.
Figura: Representação conceitual de um Flame Graph evidenciando o tempo de CPU gasto em funções de lock do scheduler de I/O.
Com o mq-deadline ativo, o Flame Graph mostrará torres largas e vermelhas em funções do kernel como blk_mq_sched_insert_request e rotinas de spin_lock. Isso é a prova visual do desperdício. Ao repetir o mesmo teste do fio com o scheduler none, essas torres desaparecem do Flame Graph. O throughput total (IOPS) sobe significativamente e a latência de cauda no percentil 99.99% sofre uma redução drástica.
O veredito para a próxima geração de storage
A evolução do armazenamento não vai desacelerar. Com a adoção em massa do PCIe Gen5 e a iminente chegada de dispositivos baseados em CXL (Compute Express Link), a largura de banda e a latência do hardware atingirão patamares onde qualquer instrução extra no kernel será fatal para a performance. O CXL, por ser uma interconexão coerente de cache, borrará ainda mais a linha entre memória RAM e armazenamento persistente.
Nesse cenário de hiper-velocidade, o software é o gargalo definitivo. Manter configurações legadas de I/O em infraestruturas modernas é o equivalente a colocar pneus de trator em um carro de Fórmula 1. A regra de ouro para engenharia de performance em storage NVMe moderno é clara: confie no hardware, elimine os locks e tire o sistema operacional do caminho dos seus dados.
Referências & Leitura Complementar
NVM Express Base Specification 2.0: Documentação oficial do consórcio NVMe detalhando o funcionamento das Submission e Completion Queues.
Linux Kernel Documentation (Block Layer): Detalhamento técnico da arquitetura multi-queue (blk-mq) e o mapeamento de filas de software para hardware.
SNIA (Storage Networking Industry Association): Artigos técnicos sobre latência de cauda em arrays All-Flash e o impacto do overhead de sistema operacional.
BPF Performance Tools (Brendan Gregg): Metodologias de análise de latência de I/O utilizando eBPF para rastreamento de chamadas de bloco no kernel Linux.
Por que o Linux ainda usa schedulers de I/O se o NVMe é tão rápido?
Schedulers como mq-deadline e bfq são fundamentais para discos rotacionais (HDDs) ou SSDs SATA legados, onde fundir e reordenar requisições evita latência mecânica ou saturação de barramento. No NVMe, o controlador de hardware já gerencia múltiplas filas de forma muito mais eficiente, tornando o software redundante.Como verificar qual scheduler de I/O meu disco NVMe está usando?
Você pode inspecionar o sysfs lendo o arquivo `/sys/block/nvme0n1/queue/scheduler`. O valor que estiver entre colchetes (por exemplo, `[none] mq-deadline`) indica o scheduler ativo no momento para aquele block device.O que é o blk-mq no kernel Linux?
O blk-mq (Block Multi-Queue) é um framework de camada de bloco do kernel Linux introduzido para substituir a antiga fila única. Ele mapeia múltiplas filas de submissão de software (por núcleo de CPU) em múltiplas filas de hardware do dispositivo, permitindo que storages modernos alcancem milhões de IOPS sem contenção severa de locks.
André Linhares
Engenheiro de Performance (Kernel/IO)
"Vivo no kernel space caçando latência com eBPF. Para mim, context switches excessivos são inimigos pessoais e cada ciclo de CPU desperdiçado é uma ofensa técnica."