这不是一篇面向存储小白的博客
在 OpenZFS 的异步写入模型里,社区常用一张分段函数图描述“脏数据百分比”和“active I/O count”的关系,但很多细节(脏数据到底住在哪里?active I/O count 到底指什么?)往往在图中被隐藏。本文结合当前 openzfs/zfs 源码,面向既懂 Linux 又想入门 ZFS 内部的读者,一口气把两个问题说清楚。

1. 脏数据并不是一块独立内存
1.1 何为脏数据
ZFS 通过 DSL pool 追踪“修改但尚未刷盘”的数据量。官方注释指出:每当数据被修改,dp_dirty_total 会增加;当数据被写回磁盘,它会减少;如果超出 zfs_dirty_data_max,新的修改会被阻塞(module/zfs/dsl_pool.c:60)。zfs_dirty_data_max 在 arc_init() 阶段按照物理内存的一定比例(默认 10%,上限 4 GiB 或内存的 25%)计算出来(module/zfs/arc.c:8079)。因此“脏数据”只是一个逻辑上的配额,不是额外分配的一整块内存。
1.2 脏页就藏在 ARC 的 dbuf 里
当 DMU 数据块被修改时,会走 dbuf_dirty():
- 如果这是第一次在该 TXG 脏化它,会从
dbuf_dirty_kmem_cache 拿到一个 dbuf_dirty_record_t 并挂进链表(module/zfs/dbuf.c:2290)。
- 对于普通数据块,会调用
arc_release() 把该 arc_buf_t 从 ARC 的共享状态释放出来,然后直接在这个 ARC buffer 上修改数据,以免影响其他读者(同上)。
- 更重要的是,
dbuf_dirty() 在脏化时调用 dmu_objset_willuse_space(),把该数据块的字节数记入 DSL 的脏数据账本(module/zfs/dbuf.c:2290 与 module/zfs/dmu_objset.c:3022)。
也就是说:脏数据就是 ARC 中那些被标记成 dirty 的缓存块,外加一份严格的计数器。ZFS 没有把 dirty data 复制到另一块 buffer,而是让 ARC 缓存承担“写回缓存”的角色。
1.3 计量路径:从线程到 DSL pool
沿着调用栈可以看到一条清晰的记账链:
应用线程 -> dbuf_dirty() (module/zfs/dbuf.c)
-> dmu_objset_willuse_space() (module/zfs/dmu_objset.c)
-> dsl_dir_willuse_space() (module/zfs/dsl_dir.c)
-> dsl_pool_dirty_space()/dp_dirty_total(module/zfs/dsl_pool.c)
dsl_dir_willuse_space() 把“即将写入的空间”加到 dataset 及其父目录的 dd_space_towrite,同时标记目录为 dirty,以便 TXG 同步时处理(module/zfs/dsl_dir.c:1410)。
dsl_pool_dirty_space() 把字节数记到 dp_dirty_pertxg[] 和 dp_dirty_total,必要时触发新一轮 txg_kick(),确保后台开始同步(module/zfs/dsl_pool.c:990)。
当写回发生时(TXG syncing 阶段的 dbuf_sync_*()),每个 dbuf_dirty_record_t 调用 dsl_pool_undirty_space() 把对应字节数减掉,这就是“脏数据消退”的来源(module/zfs/dbuf.c:4656 与 module/zfs/dsl_pool.c:1006)。
1.4 为什么需要 ARC 预留
由于脏数据就在 ARC 里,ZFS 需要保证 ARC 足够大,才能同时保存缓存和尚未刷盘的数据。dsl_dir_tempreserve_space() 会在事务提交前调用 arc_tempreserve_space(),一旦预估的 dirty footprint 超过 ARC 目标大小就返回 ERESTART,逼迫调用者暂停(module/zfs/dsl_dir.c:1408, module/zfs/arc.c:7134)。因此 dirty limit 既是 I/O 背压,也是 ARC 内存压。
2. Async Write 的 active I/O count 是什么
2.1 I/O 调度器的结构
每个叶子 vdev 都维护一个 vdev_queue。它把 I/O 分成多个优先级队列(同步读、同步写、异步读、异步写、scrub/resilver…),并对每个队列维护“最小/最大在途 I/O 数”。除此之外,还有一个设备级的 zfs_vdev_max_active 上限(module/zfs/vdev_queue.c:60)。
active I/O count 就是 vq->vq_active,表示当前这个 vdev 上发出的、尚未完成的 zio_t 数量。vdev_queue_pending_add()/vdev_queue_pending_remove() 分别在 I/O 下发和完成时增加/减少这个计数,并跟踪每个优先级的 vq_cactive[](module/zfs/vdev_queue.c:537)。换句话说,它记录的是“正在硬件上飞行的 I/O 请求数量”,与线程数无关——即使有更多调度线程,只要 vq_active 达到上限,新的 I/O 也不会被发出去。
2.2 Async write 队列的线性调节
异步写的 max_active 不是常量,而是 vdev_queue_max_async_writes() 的输出:
if dirty < min_bytes: use zfs_vdev_async_write_min_active
if dirty > max_bytes or有同步任务: use zfs_vdev_async_write_max_active
else: 线性插值到 min/max 之间
这段逻辑正是那张分段图的来源(module/zfs/vdev_queue.c:310)。min_bytes、max_bytes 由 zfs_dirty_data_max 乘上 zfs_vdev_async_write_active_{min,max}_dirty_percent 得到。Dirty data 越靠近上限,发出的 async write 越多,直到达到 zfs_vdev_async_write_max_active。如果 dirty data 依旧上涨,说明后台写已经没有跟上,此时 dmu_tx_delay() 会根据 zfs_delay_min_dirty_percent 等参数对新事务加延迟,进一步限流(module/zfs/dmu_tx.c:880)。
2.3 为什么要这样做
- 抑制突发写延迟:TXG 进入
syncing 阶段后会瞬间产生大量 ZIO_PRIORITY_ASYNC_WRITE 的写请求(见 dbuf_sync_lightweight() 中发出的 zio_write(..., ZIO_PRIORITY_ASYNC_WRITE, ...),module/zfs/dbuf.c:4681)。如果不加控制,设备队列会被写 I/O 饱和,前台同步 I/O 延迟会飙升。
- 把 dirty data 信号反馈到调度器:调度器直接读取
dp_dirty_total 来决定 max_active(module/zfs/vdev_queue.c:310),这样即使实际 I/O 线程很多,也会被全局 dirty 限制“勒住”,避免越写越脏。
- 配合更激烈的背压:当 dirty data 高于
zfs_vdev_async_write_active_max_dirty_percent 且仍未下降时,vdev_queue 仍维持 max_active,但 dmu_tx_delay() 开始指数级延迟,直到 dirty data 回到斜坡区域(module/zfs/dmu_tx.c:880)。
因此,纵轴的 active I/O count 表示“在对应叶 vdev 上允许同时在飞的异步写 zio 数量”,而不是线程数或全局 TXG 数。
3. 回答开头的两个问题
- Dirty data 是否是一块独立内存? 不是。它是 ARC 中被脏化的 dbuf,总量由
dp_dirty_total 统计。脏化时只是在 ARC buffer 上复制/修改,并通过 dmu_objset_willuse_space() 等函数把字节数记入 DSL/dir/pool 的计数器;刷盘完成后再减回。zfs_dirty_data_max 只是“允许有多少 ARC buffer 处于脏态”的阈值。
- active I/O count 到底是什么? 它是每个叶 vdev 当前正在执行的
zio_t 数,调度器按照 zfs_vdev_async_write_min/max_active 以及脏数据百分比动态控制该数字。在 dirty data 低于 zfs_vdev_async_write_active_min_dirty_percent 时,只允许 zfs_vdev_async_write_min_active 个 async write 持续刷盘;当 dirty data 上升,允许的并发线性增加直到 zfs_vdev_async_write_max_active。这完全是 I/O 并发度,而非线程计数。
4. 小结与实践建议
- 调大
zfs_dirty_data_max 会直接放宽 ARC 中 dirty buffer 的数量,但要确保内存足够,否则 arc_tempreserve_space() 会频繁重试;过大的 dirty window 也会让 TXG 刷盘耗时更长。
- 对于读写带宽充分大的 NVMe 后端设备,应该调整
zfs_dirty_data_max 为较大的值。
- 如果发现 dirty data 长期高于
zfs_vdev_async_write_active_max_dirty_percent,说明设备写入跟不上:可以通过优化 vdev(更快的 SSD / 增加并发度)、调高 zfs_vdev_async_write_max_active、或者允许更多 ZIL 同步任务来改善,但最根本还是提高后端 I/O 能力。
- 观察
kstat.zfs.misc.vdev_queue 里的 async_write_active、async_write_pending 等指标,可以直接看到 active I/O count 是否达到了上述限制,从而验证调优是否生效。
希望这篇文章能让你在阅读文档或调试 async write 时,不再对那张分段函数图心怀疑惑。