背景
RWF_DONTCACHE(DropBehind)是一种 IO 性能机制:写入时把涉及的页缓存页标上 dropbehind,回收时优先丢弃这些页,避免一次性大写把整个 page cache 污染掉。这些页在落盘前是脏的,需要 writeback 写回。IOCB_DONTCACHE 的写路径在 generic_write_sync 里,每次写都调 filemap_flush_range,在写者自己的上下文里内联提交 writeback。
┌──────────────────────────────┐
│ DONTCACHE 写每次同步回写 │
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ 回写提交在写者上下文内联进行 │ 遍历页树、提交 IO 阻塞写者数毫秒
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ 写者尾延迟大幅膨胀 │ p99.9 从约 23ms 涨到 93ms
└──────────────────────────────┘
perf 分析表明,这里的瓶颈不是锁竞争,而是 writeback 提交这件工作本身:遍历 page tree、提交 I/O,把写者阻塞上毫秒级。结果 dontcache 写的 p99.9 延迟从 buffered 的 23ms 膨胀到 93ms。
问题
- DONTCACHE 写每次都内联提交 writeback
- 回写提交(遍历页树、提交 IO)阻塞写者毫秒级
- dontcache 写 p99.9 从 23ms 膨胀到 93ms
- 若改全 BDI flush 又会打扰无关页
方案
核心是把 writeback 提交从写者热路径挪到后台 flusher。
写者不再内联 filemap_flush_range,而是发一个 flusher kick,让脏页在后台被 drain。
旧:写者内联提交回写
┌────────────────────────────┐
│ 每次写都内联回写脏页 │
└─────────────┬──────────────┘
▼
┌────────────────────────────┐
│ 回写工作阻塞写者,延迟膨胀 │
└────────────────────────────┘
新:交给后台回写线程
┌───────────────────────────────────────┐
│ 写者只发 kick,回写转到后台 │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐ 写者不再阻塞;计数器限定回写范围,不
│ 按脏页计数精确回写,bit 合并多次 kick │ 波及无关页
└───────────────────────────────────────┘
为了不波及无关页,flusher 得知道“有多少脏页是 DONTCACHE 的”。这由新增的 per-bdi_writeback 的一个 DONTCACHE 脏页计数器提供:它随 folio 记脏递增、随转净或 dropbehind 被清递减、随 inode 切换 writeback 域迁移。flusher 的 dontcache 回写例程读这个计数器,从最老的脏 inode 回写相应数量的页,既加速了 DONTCACHE 写的 writeback,又把范围限在计数以内、不动其它 buffered 脏数据。
多次 DONTCACHE 写用一个 dontcache 标志位合并成一次 flusher 唤醒,避免每写都唤醒;每次 kick 通过原子清位来消费,并发写可在回写期间重新置位、调度下一轮。计数则聚合 per-CPU batch 来读,确保低于 percpu 批阈值的小写也对 flusher 可见。
收益
作者用 dontcache-bench 实测(T6F_SKL_1920GBF,251 GiB RAM,xfs on NVMe,fio io_uring)。buffered 与 direct I/O 路径不受影响,改进限于 dontcache 路径。
单流吞吐与尾延迟:
| 指标 | Before | After | 变化 |
|---|---|---|---|
| seq-write/dontcache 吞吐 | 298 MB/s | 897 MB/s | +201% |
| rand-write/dontcache 吞吐 | 131 MB/s | 236 MB/s | +80% |
| seq-write/dontcache p99 | 135,266us | 23,986us | -82% |
| seq-write/dontcache p99.9 | 8,925,479us | 28,443us | -99.7% |
多写者(4 job 顺序写):
| 指标 | Before | After | 变化 |
|---|---|---|---|
| dontcache 聚合吞吐 | 2,529 MB/s | 4,532 MB/s | +79% |
| dontcache p99 | 8,553us | 1,002us | -88% |
| dontcache p99.9 | 109,314us | 1,057us | -99% |
多写者吞吐达 buffered 平价(4,532 vs 4,616 MB/s)。
32 文件写(Axboe 测试):
| 指标 | Before | After | 变化 |
|---|---|---|---|
| dontcache 聚合吞吐 | 1,548 MB/s | 3,499 MB/s | +126% |
| dontcache p99 | 10,170us | 602us | -94% |
| 峰值脏页 | 1,837 MB | 213 MB | -88% |
dontcache 达 buffered 的 81%(原仅 35%)。
竞争写者(dontcache vs buffered,各自文件):
| 指标 | Before | After |
|---|---|---|
| buffered writer | 868 MB/s | 433 MB/s |
| dontcache writer | 415 MB/s | 433 MB/s |
| 聚合 | 1,284 MB/s | 866 MB/s |
此前 buffered 以 2:1 饿死 dontcache;per-bdi_writeback 追踪后两者带宽相等,聚合匹配 buffered-vs-buffered 基线(863 MB/s),dontcache 的 p99.9 从 119ms 降到 33ms。
把 writeback 提交移出写者热路径后,dontcache 顺序写吞吐约 3 倍、多写者达 buffered 平价、尾延迟塌缩 1 到 2 个数量级,峰值脏页大幅下降。