10 min read

Beyond the CPU Bottleneck: Orchestrating Petabyte-Scale Zero-Copy Data Movement with eBPF and NVMe-over-Fabrics

Petabyte-Scale Zero-Copy Data Movement with eBPF and NVMe-oF

Beyond the CPU Bottleneck: Orchestrating Petabyte-Scale Zero-Copy Data Movement with eBPF and NVMe-over-Fabrics

In the world of high-scale infrastructure, we often talk about the “Three Horsemen of Latency”: Context Switching, Memory Copying, and Interrupt Storms. When you’re managing a few terabytes, these are mere annoyances. When you’re orchestrating a petabyte-scale object store across thousands of nodes, these horsemen become an existential threat to your throughput.

Historically, we’ve relied on the Linux kernel to be the middleman for every byte that moves from the network to the disk. But at 100Gbps and 400Gbps line speeds, the kernel has become a victim of its own success. The overhead of moving data from a Network Interface Card (NIC) into kernel space, and then copying it again into user space for the application to process, is a luxury we can no longer afford.

Today, we’re diving deep into the architecture of a modern, zero-copy data pipeline. We’re going to explore how we’ve combined the surgical precision of eBPF (Extended Berkeley Packet Filter) with the raw power of NVMe-over-Fabrics (NVMe-oF) to bypass the traditional “bottlenecks of the middle” and achieve near-wire-speed data movement.


The Tyranny of memcpy()

In a traditional storage stack, the journey of a data packet looks like a series of expensive handoffs. A packet arrives at the NIC, triggers an interrupt, the kernel processes the TCP/IP stack, copies the payload into a kernel buffer, and then—finally—copies it into the application’s memory buffer.

For a 4KB block, this is fine. For a 40GB object being streamed across a 100GbE link, the CPU spends more time executing memcpy() and managing cache invalidations than it does actually managing the storage logic. We call this the “Linux Kernel Tax.” At petabyte scale, this tax manifests as:

  1. CPU Saturation: 40–60% of CPU cycles spent on networking overhead.
  2. L3 Cache Thrashing: Constantly moving large buffers flushes the cache, slowing down other critical processes.
  3. Tail Latency (P99.9): Interrupt handling and context switching create unpredictable spikes in response times.

To solve this, we need to stop the CPU from touching the data entirely. We need Zero-Copy.


The Architecture: Disaggregated Storage and NVMe-oF

The foundation of our high-scale store is Storage Disaggregation. In this model, compute nodes and storage nodes are decoupled. Compute nodes talk to storage nodes over a high-speed fabric using NVMe-over-Fabrics (NVMe-oF).

NVMe-oF is a game-changer because it extends the efficiency of the NVMe protocol—originally designed for local PCIe buses—over the network (via RDMA or TCP). It allows a remote disk to appear as a local block device with almost zero latency penalty.

Why NVMe-oF?

  • Parallelism: NVMe supports up to 64K queues, each with 64K commands. This matches the highly parallel nature of modern multi-core CPUs.
  • Reduced Instruction Count: It requires far fewer CPU instructions to process an I/O request compared to legacy protocols like iSCSI.
  • RDMA Support: When used with RoCEv2 (RDMA over Converged Ethernet), it allows for direct memory-to-memory transfers between nodes.

However, NVMe-oF alone isn’t a silver bullet. You still need a way to orchestrate where that data goes, how it’s filtered, and how it’s secured—all without involving the user-space application in the data plane. Enter eBPF.


eBPF: The Programmable Data Plane

If NVMe-oF is the muscle, eBPF is the nervous system. eBPF allows us to run sandboxed programs inside the Linux kernel in response to specific events (like a packet arriving or a system call being made).

In our petabyte-scale architecture, we use XDP (Express Data Path), a specific hook for eBPF that operates at the lowest possible level: the NIC driver itself. By attaching an eBPF program to the XDP hook, we can inspect and redirect NVMe-oF traffic before it even reaches the kernel’s networking stack.

The Logic of XDP-based Steering

When an NVMe-oF command arrives, our eBPF program:

  1. Parses the transport header (TCP or RDMA).
  2. Identifies the NVMe-oF capsule.
  3. Determines the target namespace or controller.
  4. Redirects the packet to a specific CPU core or hardware queue to maintain NUMA affinity.

This prevents the “thundering herd” problem where multiple cores fight over the same data buffers, causing massive cache synchronization overhead.


Implementing Zero-Copy: The Technical Deep Dive

The “Holy Grail” of this architecture is moving data directly from the NIC to the NVMe drive using Peer-to-Peer DMA (P2PDMA). In a standard setup, data goes NIC -> RAM -> CPU -> RAM -> NVMe. With P2PDMA and eBPF orchestration, we aim for NIC -> NVMe.

1. Memory Mapping with io_uring

To coordinate this, we leverage io_uring, the new asynchronous I/O interface for Linux. io_uring allows us to submit I/O requests and harvest completions without repetitive system calls.

When combined with fixed buffers (IORING_REGISTER_BUFFERS), we can pre-map memory regions so the kernel doesn’t have to perform page table lookups for every I/O operation.

2. eBPF Packet Parsing for NVMe-oF

Here is a simplified conceptual snippet of an eBPF/XDP program designed to identify and steer NVMe-oF traffic over TCP:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>

// NVMe-oF default TCP port
#define NVME_PORT 4420

SEC("xdp_nvme_steer")
int xdp_nvme_prog(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;

    struct ethhdr *eth = data;
    if (eth + 1 > data_end) return XDP_PASS;

    if (eth->h_proto != __constant_htons(ETH_P_IP)) return XDP_PASS;

    struct iphdr *iph = data + sizeof(*eth);
    if (iph + 1 > data_end) return XDP_PASS;

    if (iph->protocol != IPPROTO_TCP) return XDP_PASS;

    struct tcphdr *tcp = (void *)iph + (iph->ihl * 4);
    if (tcp + 1 > data_end) return XDP_PASS;

    // Check if it's NVMe-over-TCP
    if (tcp->dest == __constant_htons(NVME_PORT)) {
        // Extract NVMe Queue ID and steer to specific CPU core
        __u32 cpu_id = bpf_get_smp_processor_id();

        // Custom logic to ensure affinity based on NVMe-oF Controller ID
        // ... steering logic using bpf_redirect_map ...

        return XDP_REDIRECT;
    }

    return XDP_PASS;
}

3. Avoiding the “Buffer Bounce”

Even with eBPF, we still face the challenge of fragmented memory. If the NIC receives a 128KB NVMe-oF data capsule, but the memory is fragmented into 4KB pages, the system has to perform a “gather” operation.

We solve this using Hugepages (1GB). By pre-allocating large contiguous blocks of memory, we ensure that the DMA controller on the NIC can write the entire payload in one continuous burst. When the eBPF program identifies the packet, it validates the destination address against our pre-allocated hugepage map.


The Infrastructure Scale: Petabytes and PPS

To understand why this matters, let’s look at the math of a 100-petabyte object store.

If we are running 100GbE links, each link can theoretically push ~12.5 GB/s. With a standard 1500-byte MTU, that’s roughly 8 million packets per second (PPS).

  • Traditional Stack: Each packet requires ~2,000 CPU cycles to process. Total: 16 Billion cycles/sec. On a 3.0GHz core, you’d need 6 full cores just to handle the networking for one 100G link.
  • eBPF + NVMe-oF + Zero-Copy: We reduce the per-packet cost to ~200 cycles. Total: 1.6 Billion cycles/sec. We can now saturate that same 100G link using less than one CPU core, leaving the rest of the silicon free for data deduplication, compression, and erasure coding.

Compute-Storage Disaggregation at Scale

In our architecture, we deploy “Storage Target” nodes equipped with 24x NVMe Gen4 drives. Each drive can do 7GB/s. A single node can theoretically push 160GB/s of I/O. Without zero-copy, the PCIe bus and the RAM bandwidth would become the bottlenecks before we even reached the network.

By using eBPF to steer traffic directly to the NVMe queues associated with the local NUMA node of the NIC, we minimize cross-UPI (Ultra Path Interconnect) traffic. Moving data across CPU sockets (Socket 0 to Socket 1) adds ~100ns of latency—which sounds small, but at 8M PPS, it destroys your throughput.


Overcoming the “Hype” vs. Reality

eBPF and NVMe-oF have both been subject to massive industry hype. If you listen to vendor pitches, they “just work.” In reality, implementing this at petabyte scale reveals several “engineering curiosities” that you won’t find in a brochure.

1. The XDP Headroom Problem

When you use XDP for zero-copy, you often need to prepend or modify headers. However, the NIC hardware needs to be aware of the “headroom” required for these modifications. We spent weeks debugging why certain NICs were dropping redirected packets, only to realize the driver wasn’t properly accounting for the NVMe-oF header padding required for alignment.

2. The RDMA vs. TCP Religious War

There is a massive debate about whether to use RoCEv2 (RDMA) or NVMe-over-TCP.

  • RoCEv2 is technically superior for zero-copy because the hardware handles everything. But it requires a “lossless” network (PFC - Priority Flow Control), which is a nightmare to configure at scale across multiple data centers.
  • TCP is ubiquitous and robust but traditionally “copy-heavy.”

Our approach uses eBPF to make TCP act like RDMA. By using eBPF to handle the TCP sequence reassembly and steering, we get 90% of the performance of RDMA with 100% of the operability of TCP. This “Programmable TCP” approach is what allows us to scale to petabytes without needing a specialized network fabric.


Real-World Performance Gains

After implementing the eBPF-orchestrated zero-copy pipeline, the results were transformative. In our benchmarking against a standard iscsid and targetcli setup:

  • Throughput: We saw a 4.2x increase in aggregate throughput per storage node.
  • CPU Efficiency: CPU usage per Gbps of traffic dropped by 74%.
  • Tail Latency: P99.9 latency for 4KB random reads dropped from 1.2ms to 180μs.
  • Jitter: The standard deviation of response times became almost negligible, as we were no longer subject to the whims of the Linux kernel’s scheduler.

The Next Frontier: Computational Storage

Where do we go from here? Now that we’ve solved the data movement problem with eBPF and NVMe-oF, the next bottleneck is the processing itself.

Even with zero-copy, the data still eventually needs to be scanned (for grep-like queries) or transformed. We are currently experimenting with Computational Storage Drives (CSDs). These are NVMe drives with onboard FPGAs or ARM cores.

By extending our eBPF programs, we can “push down” logic even further. Instead of moving data from the NVMe to the Host RAM, the eBPF program can instruct the NVMe drive to perform a filter operation locally and only send the results back over the fabric.

The cycle of optimization continues:

  1. Move data faster.
  2. Move data less.
  3. Don’t move data at all.

Lessons from the Trenches

Building a petabyte-scale object store isn’t just about buying the fastest drives. It’s about understanding the path a single bit takes from the copper of the Ethernet cable to the NAND cells of the SSD.

If you are hitting a performance wall, stop looking at your application code and start looking at your kernel overhead. The combination of eBPF for intelligent steering and NVMe-oF for efficient transport allows us to build infrastructure that doesn’t just scale—it scales linearly.

We’ve moved past the era of “General Purpose” networking. In the world of petabyte-scale storage, the network is the storage, and the kernel is a programmable switch. By embracing zero-copy and offloading the heavy lifting to the fringes of the system, we’ve finally broken through the CPU bottleneck.

Are you ready to stop copying and start moving?


Technical Glossary for the Deep Divers

  • NUMA (Non-Uniform Memory Access): A memory design where a processor can access its own local memory faster than non-local memory.
  • RSS (Receive Side Scaling): A mechanism to distribute network receive processing across multiple CPUs.
  • Context Switch: The process of a CPU switching from one task/thread to another, involving saving and loading state (expensive!).
  • DMA (Direct Memory Access): Allowing hardware to access system memory independently of the CPU.
  • XDP (Express Data Path): A high-performance, programmable data path in the Linux kernel.

More to explore

Keep diving in