blob: 39109e8681b7359a4518bb8d55ca0226e4621047 [file] [view]
# Async File Cache Write Microbenchmark
## Scope
`async_file_cache_write_microbench` measures the asynchronous cache-write components added below
`CachedRemoteFileReader`. It uses a real filesystem-backed `BlockFileCache` and production
`InflightWriteBufferIndex`, admission control, bounded locked FIFO queue, worker pool, `get_or_set`,
`append`, and `finalize` implementations. Its deterministic in-memory remote reader removes S3 and
network latency from the comparison.
This complements the existing `file_cache_microbench`:
- The existing S3 read mode is useful for a complete remote-read environment, but its results also
contain object-store and network latency.
- The existing direct `get_or_set` mode measures the lower-level cache lookup path, but does not
exercise asynchronous buffer publication, queue admission, background workers, or persistence.
- The asynchronous benchmark connects those production components and verifies the final cache
state, while keeping the source read deterministic.
The measured reader flow is:
```text
SyntheticRemoteFileReader
-> CachedRemoteFileReader
-> inflight-buffer lookup
-> BlockFileCache probe
-> foreground remote fill
-> InflightWriteBufferIndex publication
-> AsyncCacheWriteManager admission and locked FIFO queue
-> worker get_or_set, append, and finalize
-> inflight entry cleanup
```
## Benchmark groups
The benchmark groups are:
- `reader`
- Starts every case with an empty cache.
- Issues concurrent, non-overlapping cold reads through `CachedRemoteFileReader`.
- Compares forced synchronous writes with asynchronous writes using the same source data and
requested ranges.
- Validates returned bytes and verifies that the complete aligned block range is readable from
the final cache state.
- Separates caller-visible foreground time from the time needed to drain accepted asynchronous
writes.
- `manager`
- Submits unique real buffers directly to `AsyncCacheWriteManager` from concurrent producers.
- Measures the drop-oldest queue with 1, 4, and 16 workers by default, including allocation,
inflight publication, queue admission, worker consumption, `get_or_set`, `append`, `finalize`,
and completion cleanup.
- Verifies every persisted task in the final `BlockFileCache`.
Accepted-but-not-persisted tasks must equal `evicted`.
- Includes a deliberately bounded saturated case. `accepted`, `rejected`, and `evicted`
distinguish rejection when no queued victim exists from replacement of the oldest queued task.
- `index`
- Measures `InflightWriteBufferIndex::lookup` independently from disk writes.
- `sharded_miss` spreads absent keys across shards.
- `sharded_hit` spreads pre-populated keys across shards.
- `hot_key_hit` sends all producers to one key to expose the upper bound of lock contention.
The integrated reader and manager groups cover index insertion and conditional removal. The index
group intentionally measures only lookup contention so it does not duplicate those flows.
## Disk baseline
Run the source-tree wrapper instead of invoking the binary directly when comparing machines. With
the default `RUN_FIO=auto`, the wrapper checks whether `fio` is installed and, if available, runs
these baselines on the same filesystem before starting the cache benchmark:
- `seqwrite_qd1`: direct 1 MiB sequential writes at queue depth 1, showing single-stream device
throughput and latency.
- `randwrite_qd16`: direct 1 MiB random writes at queue depth 16, showing concurrent device
throughput and latency for a workload closer to writes spread across cache files.
The fio files are created in a unique directory next to `--cache_path`, use `--unlink=1`, and are
not placed in `/tmp`. Direct I/O avoids filling the page cache immediately before the cache
benchmark. These fio results describe the storage environment; they are not numerically equivalent
to cache `persisted_mib_per_sec`, because production cache files use buffered writes without an
`fsync` or `fdatasync` in the measured completion path.
Environment controls for the wrapper are:
| Variable | Default | Meaning |
| --- | --- | --- |
| `RUN_FIO` | `auto` | Run when fio exists. Use `1` to require fio or `0` to skip it. |
| `FIO_SIZE` | `1G` | Address space used by each direct-I/O fio case. |
| `FIO_RUNTIME` | `5` | Measured seconds for each fio case, after a one-second ramp. |
## Defaults and repetitions
The defaults represent small reads that cause full 1 MiB cache-block writes:
| Setting | Default | Purpose |
| --- | ---: | --- |
| `block_size` | 1 MiB | Cache alignment and persisted bytes per reader miss |
| `request_size` | 64 KiB | Bytes returned by each caller read |
| `reader_operations` | 128 | Cold blocks in each synchronous or asynchronous reader case |
| `manager_task_size` | 1 MiB | Payload in each direct manager task |
| `manager_operations` | 256 | Attempts in each manager case |
| `producer_threads` | 16 | Concurrent readers or submitters |
| `reader_workers` | 16 | Workers in the asynchronous reader comparison |
| `worker_counts` | 1, 4, 16 | Worker scaling points in the manager group |
| `backpressure_pending_bytes` | 67108864 | Pending-buffer byte limit in the saturated manager case |
| `index_operations_per_thread` | 100,000 | Lookups performed by each index producer |
| `index_key_count` | 4,096 | Keys in each sharded index case |
| `repetitions` | 5 | Repeated executions of every selected case |
Each repetition clears and drains the real cache before every reader or manager case. The process
prints the one-based `repetition` on every `RESULT` line. Use the median across repetitions as the
primary comparison and retain the minimum-to-maximum range to expose scheduler, filesystem, page
cache, and background writeback noise. The first repetition is intentionally retained instead of
being silently treated as warm-up.
For formal comparisons, use the same Release build, host, cache filesystem, arguments, and idle
machine state. Five repetitions are the default for a quick comparison; increase
`--repetitions` when the min-to-max spread is large.
## Build and run
Performance numbers should come from a Release build. Build the standalone benchmark with:
```bash
./build.sh --be --file-cache-microbench -j100
```
The wrapper remains in the benchmark source directory and is not installed into the Doris output.
Run all groups, the fio baseline, and five repetitions with:
```bash
./be/src/io/cache/benchmark/run-async-file-cache-write-microbench.sh \
--benchmark_mode=all \
--cache_path=./output/async_file_cache_write_microbench \
--producer_threads=16 \
--worker_counts=1,4,16 \
--repetitions=5 2>&1 | tee ./output/async_file_cache_write_microbench.log
```
> `--cache_path` is benchmark-owned and must not exist or must be an empty directory. The benchmark
> rejects a non-empty path instead of clearing it, and removes only the directory it accepted on
> exit unless `--keep_cache` is set. Always use a dedicated path under `output/`; never point it at
> an existing cache or data directory.
Invoke `be/output/lib/async_file_cache_write_microbench` directly only when the fio baseline is not
wanted. Use `--help` to adjust operation counts, request and task sizes, queue limit, worker counts,
repetitions, and cache retention.
## Result fields
Each measured case emits one machine-readable `RESULT` line:
- Identity and shape: `benchmark`, `variant`, `repetition`, `producers`, `workers`, and
`operations`.
- Admission and correctness: `accepted`, `rejected`, `evicted`, and `persisted`. A case fails
instead of printing a successful result if returned data is wrong or if drop-oldest admission
does not satisfy `accepted - persisted = evicted`.
- Timing: `foreground_seconds` ends when producer or reader calls return; `drain_seconds` is the
remaining background completion time; `total_seconds` includes both.
- Rates: `foreground_ops_per_sec` measures caller or submitter completion.
- Queue contention: `queue_lock_wait_p99_us` and `queue_lock_hold_p99_us` expose the manager's
rolling P99 FIFO mutex acquisition and critical-section time.
- Memory: `peak_buffer_bytes` samples tracked async-write buffer capacity alongside peak pending,
queued, and inflight counts.
`persisted_mib_per_sec` divides verified bytes by total time and does not claim durable-media
completion.
- Foreground latency: `avg_us`, `p50_us`, `p95_us`, `p99_us`, and `max_us`.
- Queue shape: `peak_pending`, `peak_queued`, and `peak_inflight` are sampled high-water marks.
`pending` includes queued and active accepted tasks. `inflight` can briefly exceed `pending`
because a producer publishes its buffer before admission and conditionally removes it after a
rejection.
- Index-only rates: `elapsed_seconds` and `operations_per_sec` replace write-specific rate fields.
Use reader `foreground_ops_per_sec` and latency to quantify caller benefit from asynchronous
writes. Use manager `persisted_mib_per_sec`, `drain_seconds`, and peak gauges together to determine
whether workers or admission are limiting progress. Use the saturated case's rejection and eviction
ratios to validate bounded overload behavior, and compare sharded versus hot-key index results to
quantify lock-contention sensitivity.
## Out of scope
This benchmark does not measure S3 or network latency, complete scanner/query throughput,
cache-hit read throughput, restart recovery, multiple-cache-disk balancing, or durable `fsync`
throughput. It is also not a replacement for BE unit and regression tests: it validates data and
final cache coverage to reject invalid performance samples, but its primary purpose is controlled
performance comparison.