Tracing distribuído no hardware: conectando OpenTelemetry às filas do NVMe

      Lucas Ferreira 10 min de leitura
      Tracing distribuído no hardware: conectando OpenTelemetry às filas do NVMe

      Descubra como usar eBPF e OpenTelemetry para correlacionar a latência de cauda da sua aplicação diretamente com as filas de submissão de discos NVMe.

      Compartilhar:

      A ilusão das médias está destruindo a sua capacidade de entender o que realmente acontece na sua infraestrutura. Quando o seu banco de dados começa a gritar sobre lentidão e os alertas disparam, a primeira reação de muitos engenheiros é abrir o terminal e rodar um iostat. O painel mostra uma latência média de disco de 2 milissegundos. Tudo parece verde. Tudo parece normal.

      Mas o seu usuário final está experimentando timeouts de 5 segundos. Como isso é possível?

      A resposta curta é que métricas agregadas mentem. Elas suavizam os picos, escondem a latência de cauda e apagam o contexto. Para entender verdadeiramente o comportamento de sistemas de armazenamento modernos baseados em flash, precisamos parar de olhar para médias de um segundo e começar a rastrear requisições individuais. Precisamos levar o tracing distribuído para onde ele raramente vai: direto para o hardware, conectando o OpenTelemetry às filas do protocolo NVMe.

      Resumo em 30 segundos

      • O iostat e as métricas agregadas mentem: eles escondem a latência de cauda que destrói a experiência do usuário.
      • O protocolo NVMe possui filas de submissão e conclusão; o tempo gasto entre elas é a verdadeira latência do hardware.
      • Usar eBPF para capturar eventos do block layer e anexá-los a Trace IDs do OpenTelemetry permite ver o invisível no armazenamento.

      A ilusão do iostat e o mistério da latência de cauda

      Discos de estado sólido modernos, especialmente os baseados no protocolo NVMe (Non-Volatile Memory Express), operam na escala dos microssegundos. Eles são incrivelmente rápidos, mas também são sistemas distribuídos complexos por si só. Um SSD corporativo possui múltiplos núcleos de processamento, sua própria memória RAM e executa tarefas de fundo pesadas, como a coleta de lixo (Garbage Collection) e o nivelamento de desgaste (Wear Leveling) das células NAND.

      Quando uma dessas tarefas de fundo colide com a sua requisição de leitura crítica do banco de dados, ocorre um pico de latência. Essa requisição específica pode levar 100 milissegundos em vez de 100 microssegundos. Isso é a latência de cauda.

      Gráfico de dispersão mostrando como a média esconde os picos reais de latência de I/O. Figura: Gráfico de dispersão mostrando como a média esconde os picos reais de latência de I/O.

      Se você tem mil requisições rápidas e uma requisição terrivelmente lenta no mesmo segundo, a média agregada do iostat vai diluir esse pico. O painel continuará verde, mas a requisição que falhou era exatamente a transação de pagamento do seu maior cliente. Métricas sem alta cardinalidade são inúteis para debugar problemas reais de I/O.

      O abismo entre o kernel e as filas do protocolo NVMe

      Para entender onde o tempo está sendo gasto, precisamos olhar para a arquitetura do NVMe. Diferente do antigo protocolo AHCI usado pelos discos SATA, o NVMe foi desenhado para paralelismo massivo. Ele utiliza pares de filas alocadas na memória do host: a Fila de Submissão (Submission Queue - SQ) e a Fila de Conclusão (Completion Queue - CQ).

      O fluxo funciona assim: a CPU coloca um comando de leitura ou escrita na SQ e toca uma "campainha" (doorbell) no registro da controladora PCIe. O SSD busca o comando via DMA (Direct Memory Access), executa o trabalho nas memórias flash, coloca o resultado na CQ e envia uma interrupção para a CPU avisando que terminou.

      O grande problema de observabilidade atual é o abismo entre a sua aplicação e essas filas. A sua aplicação sabe quando pediu o dado. Mas assim que a requisição desce pelo VFS (Virtual File System) e entra no block layer do kernel Linux, o contexto da aplicação (o Trace ID) é perdido.

      ⚠️ Perigo: Confiar apenas no tempo de resposta medido pela aplicação significa que você não sabe se a lentidão ocorreu no escalonador de I/O do Linux, no enfileiramento do barramento PCIe ou no silício do próprio SSD.

      Por que métricas agregadas de disco escondem gargalos de I/O

      Quando você perde o Trace ID da requisição original, você perde a capacidade de fazer perguntas complexas aos seus dados. Se o disco está lento, você precisa saber exatamente quais requisições estão sofrendo.

      Métricas tradicionais de infraestrutura agrupam tudo por dispositivo (exemplo: /dev/nvme0n1). Elas não conseguem responder perguntas vitais como: "Quais IDs de clientes estão sofrendo com latência de disco superior a 50ms neste exato momento?".

      Para responder a isso, precisamos de eventos estruturados e de alta cardinalidade. Precisamos que cada operação de I/O no disco emita um span de tracing contendo o ID da requisição original, o namespace do NVMe, o tamanho do bloco e o tempo exato gasto nas filas de hardware.

      Monitoramento tradicional vs Tracing de hardware

      Para deixar claro o salto geracional que estamos discutindo, observe como a abordagem muda quando paramos de contar coisas e começamos a rastrear eventos.

      Característica Monitoramento Tradicional (iostat, node_exporter) Tracing de Hardware (eBPF + OpenTelemetry)
      Abordagem Agregação temporal (médias por segundo). Eventos individuais (spans por requisição).
      Granularidade Dispositivo inteiro (/dev/nvme0n1). Bloco, Namespace, Fila de Submissão (SQ).
      Contexto da Aplicação Nenhum. O disco é uma caixa preta. Total. O I/O é vinculado ao Trace ID do usuário.
      Visibilidade de Cauda Cega. Picos são diluídos na média. Clara. Cada outlier pode ser inspecionado.
      Foco de Resolução "O disco parece ocupado." "A query X saturou a fila 4 do barramento PCIe."

      Injetando trace IDs do OpenTelemetry no block layer usando eBPF

      A ponte mágica que nos permite cruzar o abismo entre o espaço do usuário (onde vive a sua aplicação) e o espaço do kernel (onde vive o driver NVMe) chama-se eBPF (Extended Berkeley Packet Filter). O eBPF permite executar programas seguros e em sandbox dentro do kernel Linux sem precisar alterar o código-fonte do sistema operacional.

      Podemos escrever um programa eBPF que se anexa aos tracepoints do block layer, especificamente em eventos como block_rq_issue (quando o comando é enviado ao hardware) e block_rq_complete (quando o hardware responde).

      Fluxo de injeção de Trace IDs no block layer do Linux via eBPF. Figura: Fluxo de injeção de Trace IDs no block layer do Linux via eBPF.

      O truque de mestre é capturar o contexto da thread no espaço do usuário no momento em que a chamada de sistema (syscall) de I/O é feita. Extraímos o Trace ID do OpenTelemetry da memória da aplicação e o mapeamos para a estrutura de requisição de bloco (struct request) no kernel.

      Quando o evento block_rq_complete dispara, nosso programa eBPF calcula a diferença de tempo em nanossegundos, empacota isso junto com o Trace ID e envia para o nosso backend de observabilidade como um span filho. Agora, o seu trace distribuído não termina no banco de dados. Ele desce até o metal.

      Consultando spans de hardware para provar a saturação do barramento PCIe

      Com os dados fluindo corretamente, a investigação de incidentes muda de patamar. Você não precisa mais adivinhar se o problema é rede, CPU ou disco. Você pode provar com dados.

      Imagine que você recebe um alerta de latência no seu serviço de checkout. Você abre a ferramenta de observabilidade e agrupa os traces lentos. Você percebe que todos eles compartilham um span filho extremamente longo vindo da camada de armazenamento.

      💡 Dica Pro: Adicione atributos de alta cardinalidade aos seus spans de hardware gerados via eBPF, como o ID da controladora NVMe, o número da fila (Queue ID) e o tamanho do payload em bytes. Isso permite fatiar os dados de formas impossíveis com métricas padrão.

      Ao inspecionar os atributos desse span de hardware, você descobre que o tempo entre a submissão na SQ e a resposta na CQ foi de 80 milissegundos. Você também nota que todas as requisições lentas estão caindo na mesma fila de hardware (Queue ID 3) e que o tamanho do payload é massivo.

      Você acabou de provar que não há problema no código da aplicação ou no banco de dados. O barramento PCIe está saturado por operações de leitura sequencial gigantescas concorrendo com as suas pequenas leituras transacionais. A solução não é otimizar a query, mas sim isolar os workloads em namespaces NVMe diferentes ou ajustar as políticas de QoS (Quality of Service) do próprio SSD.

      O fim da adivinhação no armazenamento

      A era de tratar a infraestrutura de armazenamento como uma caixa preta que emite apenas médias de utilização acabou. Sistemas modernos exigem observabilidade de ponta a ponta, do clique no navegador até a célula de memória flash NAND no datacenter.

      Ao conectar o OpenTelemetry às entranhas do protocolo NVMe usando eBPF, paramos de adivinhar e começamos a ver. A latência de cauda deixa de ser um fantasma assustador e se torna apenas mais um problema de engenharia que pode ser isolado, compreendido e resolvido. Pare de olhar para painéis verdes que escondem usuários frustrados. Exija alta cardinalidade até o nível do silício.

      Visualização de um trace em cascata chegando até a camada física do SSD. Figura: Visualização de um trace em cascata chegando até a camada física do SSD.


      Referências & Leitura Complementar

      • NVM Express Base Specification: Documentação oficial da SNIA detalhando o funcionamento das Submission e Completion Queues.

      • Linux Kernel Documentation (eBPF): Guias arquiteturais sobre como instrumentar tracepoints no block layer e drivers de dispositivos.

      • OpenTelemetry Tracing API: Especificações sobre propagação de contexto e injeção de Trace IDs em sistemas distribuídos.


      É possível enviar spans do OpenTelemetry diretamente para o firmware do NVMe? Não diretamente. O hardware não entende o que é um Trace ID. O que fazemos é usar eBPF no nível do kernel (escutando o block layer e o driver NVMe) para capturar o tempo exato de enfileiramento e conclusão. O eBPF atua como um tradutor, associando esses eventos de baixo nível ao Trace ID da requisição da aplicação que gerou o I/O no espaço do usuário.
      Qual é a diferença exata entre as filas de submissão (SQ) e conclusão (CQ) no NVMe? A fila de submissão (SQ) é o local na memória onde o host (a sua CPU) coloca os comandos de I/O para a controladora NVMe executar. A fila de conclusão (CQ) é onde a controladora posta os resultados após processar fisicamente esses comandos nas memórias flash. Monitorar a latência exata entre a entrada na SQ e a saída na CQ revela o tempo real de processamento no silício, livre de interferências do sistema operacional.
      Por que o iostat não é suficiente para diagnosticar problemas de I/O modernos? Ferramentas tradicionais como o iostat trabalham com médias agregadas em intervalos de segundos. Elas são cegas por design. Elas não conseguem capturar micro-explosões de latência (tail latency) que afetam requisições individuais. Se uma query leva 100ms por causa de um engasgo no SSD, mas o resto do segundo teve I/Os de 1ms, a média esconderá o problema. Apenas o tracing baseado em eventos de alta cardinalidade consegue revelar a verdade.
      #OpenTelemetry #NVMe #eBPF #Observabilidade #Storage #Latência de cauda
      Lucas Ferreira
      Assinatura Técnica

      Lucas Ferreira

      Engenheiro de Observabilidade

      "Transformo o caos de logs, métricas e traces em clareza operacional. Minha missão é eliminar pontos cegos e garantir que nada permaneça invisível na infraestrutura."