io_uring: more disk throughput with less CPU
At high shuffle throughput, moving bytes is only half the cost. The other half is submitting I/O, waking threads, copying buffers, and handling completions. Riffle's io_uring engine moves that coordination off the request path—and a 2.2 TB TeraSort validation reported higher write throughput with roughly one-third of the CPU load.
Why asynchronous disk I/O matters
Riffle absorbs shuffle writes in memory and flushes them to local disks in the background. Under sustained load, the storage engine has to process many independent buffers while continuing to serve reads. A conventional blocking path ties submission and completion work to more application threads, adding context switches and CPU overhead as concurrency rises.
RFC #554 proposed an
io_uring-based local-file handler for this workload. The goal was not to make
the device itself faster. It was to keep more requests in flight with fewer
threads and less coordination on the hot path.
One engine, two queues
PR #549 introduced the first experimental engine. Each engine shard accepts read and write tasks, places operations into the kernel’s submission queue, and resolves their waiting callers from the completion queue.
The write path uses vectored writes, allowing a composed shuffle payload to be submitted without first flattening every segment into one contiguous application buffer. Reads can use buffered or direct I/O, while the socket path can hand data to a pipe for zero-copy transfer.
The important shift is ownership: a small number of I/O workers drive many operations. PR #570 later reduced the default to two io_uring threads per disk after testing showed that two threads could preserve overall throughput.
What the TeraSort run showed
The RFC records a 2.2 TB TeraSort application with one Riffle server handling all shuffle data. It reports write throughput reaching 5 GB/s, compared with 3.75 GB/s on the previous path, summarized by the project as an approximately 25% improvement. CPU load was reported at roughly one-third of the non-io_uring run.
| 2.2 TB TeraSort phase | With io_uring | Without io_uring | Duration change |
|---|---|---|---|
| Shuffle write | 3.4 min | 4.1 min | −17.1% |
| Shuffle read | 6.3 min | 8.3 min | −24.1% |
| Combined | 9.7 min | 12.4 min | −21.8% |
These phase durations are an end-to-end application result, not an isolated disk benchmark. The RFC separately notes that standalone read throughput did not improve significantly: reads were less intensive and each operation already transferred relatively large blocks. The clearest direct benefit was the write-heavy path and its lower CPU cost.
Zero-copy reads with splice
The local engine was only the first step.
PR #559 added splice-based reads
for the Linux 5.10 deployment target. Instead of copying file data into an
application buffer and then copying it again to the socket, Riffle can move the
data through a pipe while keeping the payload in kernel space.
PR #560 then made the read mode a
client-visible choice across buffered I/O, direct I/O, sendfile, and
splice. PR #568 added the
TOTAL_URING_SPLICE metric so operators can confirm that eligible reads are
actually taking the zero-copy path.
Enabling the engine
io_uring support is compiled explicitly and is available only on Linux. The project requires Linux kernel 5.10 or later and reports validation on Anolis OS 8.
cargo build --release -p riffle-server --features io-uring
Enable the local-file engine in the server configuration:
[localfile_store.io_uring_options]
threads = 2
io_depth = 64
Two threads and an I/O depth of 64 are the current defaults. They should still be validated against the number and type of local disks, CPU topology, and expected spill concurrency.
From experimental to resilient
Asynchronous I/O expands the number of states the storage engine must handle:
partial vectored writes, file lifetime across queued tasks, alignment for
O_DIRECT, and rollback when a later chunk fails. The implementation evolved
through targeted fixes rather than treating the first benchmark as the finish
line.
- PR #553 fixed a buffer-lifetime issue that could cause data inconsistency.
- PR #608 split vectored writes
that exceed Linux’s
UIO_MAXIOVlimit. - PR #620 added rollback when a later write chunk fails after earlier chunks have reached disk.
- PR #623 aligned direct appends
and submitted them through io_uring so
O_DIRECTworks on the new engine.
This reliability work matters as much as the headline throughput. Shuffle data is temporary, but silent corruption or a logically successful partial write can invalidate an entire stage.
The result
io_uring gives Riffle a storage path shaped for high concurrency: composed
writes are submitted in batches, completions are handled by a small worker set,
and eligible reads can reach the network through splice. In the reported
TeraSort run, that translated into more write throughput, lower application
phase time, and much lower CPU load.
The larger lesson is not that every read or disk becomes faster. It is that reducing per-operation coordination leaves more CPU available for the parts of shuffle service that cannot be offloaded.