背景

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 个数量级,峰值脏页大幅下降。