docs/components/read_flow/10_prefetching_and_async_io.md
Files: file/file_prefetch_buffer.h, file/file_prefetch_buffer.cc, table/block_based/block_prefetcher.h, table/block_based/block_prefetcher.cc, table/block_based/block_based_table_iterator.h, table/block_based/block_based_table_iterator.cc, env/io_posix.cc, env/io_posix.h, include/rocksdb/file_system.h, include/rocksdb/options.h, include/rocksdb/table.h
RocksDB's read-path I/O optimization has four layers:
| Layer | Component | Role |
|---|---|---|
| Configuration | ReadOptions, BlockBasedTableOptions | Set readahead size, async mode, adaptive behavior |
| Iterator integration | BlockBasedTableIterator | Detects sequential access, triggers prefetch, two-pass async seek |
| Prefetch orchestration | BlockPrefetcher | Decides when/how to create FilePrefetchBuffer |
| Buffer management + I/O | FilePrefetchBuffer | Multi-buffer prefetch, sync/async reads, readahead tuning |
These options control auto-readahead behavior at the SST level (see BlockBasedTableOptions in include/rocksdb/table.h):
| Option | Default | Purpose |
|---|---|---|
initial_auto_readahead_size | 8 KB | Starting readahead size for auto prefetching |
max_auto_readahead_size | 256 KB | Maximum readahead size (doubles from initial up to this cap) |
num_file_reads_for_auto_readahead | 2 | Number of sequential reads before auto prefetch enables |
| Option | Default | Effect |
|---|---|---|
readahead_size | 0 | If non-zero, overrides auto-readahead with a fixed readahead size |
async_io | false | Enable async prefetching via ReadAsync() and Poll() |
adaptive_readahead | false | Carry readahead state across files within the same level |
auto_readahead_size | true | Auto-tune readahead based on block cache, upper bound, and prefix |
optimize_multiget_for_io | true | Async reads across SST levels in MultiGet |
BlockPrefetcher (see BlockPrefetcher in table/block_based/block_prefetcher.h) is owned by BlockBasedTableIterator and decides when to create a FilePrefetchBuffer. The PrefetchIfNeeded() method handles three cases:
Compaction reads: Uses compaction_readahead_size (from DBOptions). First tries the FS-level Prefetch() syscall (Linux readahead()). If unsupported, falls back to internal FilePrefetchBuffer with a fixed readahead size.
Explicit user readahead (ReadOptions::readahead_size > 0): Immediately creates FilePrefetchBuffer with the specified fixed size.
Implicit auto readahead (the most common path):
Step 1: Track sequential access via IsBlockSequential() and num_file_reads_ counter
Step 2: Wait for num_file_reads_for_auto_readahead sequential reads (default: 2) before enabling
Step 3: First try FS-level Prefetch() (POSIX readahead() syscall to populate page cache)
Step 4: If FS Prefetch() is unsupported, create a FilePrefetchBuffer internally
Step 5: Readahead doubles exponentially: readahead_size = min(max_auto_readahead_size, readahead_size * 2)
When async_io is enabled, num_buffers is set to 2 for double-buffering.
FilePrefetchBuffer (see FilePrefetchBuffer in file/file_prefetch_buffer.h) manages a deque of BufferInfo objects (bufs_ for active buffers, free_bufs_ for reuse, overlap_buf_ for data spanning two buffers).
Synchronous mode (num_buffers == 1):
TryReadFromCache() checks if requested data is in the bufferPrefetchInternal() reads current data + readahead bytes in one synchronous pread()Asynchronous mode (num_buffers > 1, typically 2 -- double-buffering):
ReadAsync()PrefetchAsync() submits an async read and returns Status::TryAgainTryReadFromCache() call, PollIfNeeded() completes the async read, then serves dataPrefetchAsyncCallback()ReadaheadParams (see ReadaheadParams in file/file_prefetch_buffer.h) controls the prefetch buffer:
| Field | Purpose |
|---|---|
initial_readahead_size | Starting readahead size |
max_readahead_size | Cap for exponential growth |
implicit_auto_readahead | Whether RocksDB-initiated (not user-configured) |
num_file_reads | Current sequential read count |
num_file_reads_for_auto_readahead | Threshold to activate prefetch |
num_buffers | 1 = synchronous, 2 = async double-buffer |
When iterating over SST files, auto-readahead activates after detecting sequential access:
Step 1: The first num_file_reads_for_auto_readahead reads (default: 2) are served without prefetching
Step 2: On the next sequential read, readahead enables with initial_auto_readahead_size (default: 8KB)
Step 3: Readahead doubles on each subsequent sequential read, up to max_auto_readahead_size (default: 256KB)
Step 4: When the iterator moves to a new file, readahead size resets (unless adaptive_readahead is true)
When ReadOptions::adaptive_readahead is true:
GetReadaheadState() saves readahead_size and num_file_reads from the FilePrefetchBuffer when leaving a fileSetReadaheadState() restores them when entering the next SST file at the same levelinitial_auto_readahead_size at file boundaries, benefiting long scans spanning multiple SST filesWhen ReadOptions::auto_readahead_size is true (default) and block cache is enabled:
A readaheadsize_cb callback bound to BlockBasedTableIterator::BlockCacheLookupForReadAheadSize() is passed to BlockPrefetcher. This callback:
iterate_upper_bound is not prefetchedprefix_same_as_start is true, trims readahead at the prefix boundaryDecreaseReadAheadIfEligible() reduces readahead size when prefetched blocks were already in cache (wasted prefetch).
Async I/O requires the file system to implement three methods (see FSRandomAccessFile in include/rocksdb/file_system.h):
| Method | Purpose |
|---|---|
ReadAsync() | Submit non-blocking read, returns an io_handle for later polling |
Poll() | Check/wait for async reads to complete, invoke callbacks |
AbortIO() | Cancel outstanding async reads |
The file system must also set FSSupportedOps::kAsyncIO in SupportedOps(). The default implementation falls back to synchronous Read() with immediate callback.
On Linux, PosixRandomAccessFile uses per-thread io_uring instances:
ReadAsync (see PosixRandomAccessFile in env/io_posix.cc):
Posix_IOHandle storing callback, offset, length, and io_uring pointerio_uring_get_sqe()readv operation: io_uring_prep_readv(sqe, fd, &iov, 1, offset)io_uring_submit() -- returns immediatelyPoll() laterMultiRead (synchronous scatter/gather, see PosixRandomAccessFile in env/io_posix.cc):
io_uring_submit_and_wait() for combined submit+waitkIoUringDepth = 256pread() if io_uring init failsio_uring setup flags:
IORING_SETUP_SINGLE_ISSUER: Only the submitting thread accesses the ringIORING_SETUP_DEFER_TASKRUN: Task work runs in submitter's contextWhen ReadOptions::async_io is true, BlockBasedTableIterator::SeekImpl() uses a two-pass pattern:
Pass 1: Calls AsyncInitDataBlock(is_first_pass=true):
FilePrefetchBuffer with num_buffers=2 (double-buffering)NewDataBlockIterator() with async_read=trueFilePrefetchBuffer submits async read via io_uring and returns TryAgainasync_read_in_progress_ = true and returns to callerPass 2: The caller calls Seek() again, which detects async_read_in_progress_:
SeekSecondPass() -> AsyncInitDataBlock(is_first_pass=false)TryReadFromCache() calls PollIfNeeded() to complete the async readDuring sequential Next() calls, the double-buffer scheme means the next data block is prefetched asynchronously while the current block is being iterated.
RetrieveMultipleBlocks() in block_based_table_reader_sync_and_async.h optimizes disk reads for MultiGet:
Step 1: Identify data blocks needed for all keys in the batch
Step 2: Sort blocks by file offset
Step 3: Merge adjacent or near-adjacent block reads into single I/O requests
Step 4: Issue merged reads via MultiRead() (scatter/gather I/O using io_uring on Linux)
This reduces the number of system calls from O(keys) to O(distinct_read_regions).
| Scenario | Without Prefetch | With Prefetch |
|---|---|---|
| Sequential scan (HDD) | Random IOPS limited | Near-sequential throughput (use db_bench to measure) |
| Sequential scan (SSD) | Random read latency per block | Near-sequential throughput |
| MultiGet (10 keys, 3 levels) | 10+ read syscalls | 3-5 coalesced reads |
| Prefix scan (short) | May over-prefetch | Trimmed to prefix boundary |
Note: Over-aggressive prefetching can waste I/O bandwidth and pollute OS page cache. The auto-tuning features (auto_readahead_size, adaptive_readahead) help balance prefetch benefit against waste.