跳转至

dirty 是一张欠条:writeback 的调度哲学

04-1 讲脏槽位时把 dirty 定义成"一致性记账"——哪些页欠磁盘一次回写,系统随时能回答。但记账只讲了半个故事:欠条什么时候还、按什么节奏还,是另一整套设计。这篇接着查账:Linux 的双阈值、InnoDB 的 flush list、fsync 到底买了什么。

1. 账本的两面

dirty 标记同时干两件事:

  1. 正确性记账:崩溃后的恢复逻辑靠它判断"这个页的磁盘版本可不可信"——欠着的不能信
  2. 调度自由度:什么时候还这笔债是 策略问题,不是正确性问题

第二面才是 writeback 的精髓。write() 系统调用只把数据拷进 page cache、打上 dirty 标志就返回,落盘被推迟到未来某个时刻。推迟本身就是收益:一万次小写聚合成几次大顺序写,磁盘最擅长的活。

2. Linux 的双档节流

推迟的度由两个参数拉扯:

  • vm.dirty_background_ratio:脏页占比超过它,flusher 线程(每块设备一组)后台异步 回写启动,写者无感
  • vm.dirty_ratio:占比超过它,写者被 同步阻塞,亲手把脏页刷下去才能继续写——balance_dirty_pages 是系统的最后防线

两档的分工:异步档保 吞吐(攒批量、顺序刷),同步档保 安全(给掉电窗口封顶)。这是所有 writeback 系统共享的骨架——一条允许积压的带,一根不许越过的顶。

3. InnoDB:欠条按"最老的"排序

InnoDB 不信 page cache,自己在 buffer pool 里管脏页:

  • 脏页挂进 flush list,按最老修改的 LSN 排序——队头就是欠得最久的债
  • checkpoint 推进到最老脏页的位置:这点之前的日志可以安全丢弃,崩溃恢复从这点开始重放(上一篇 讲过 WAL 侧的账)
  • 刷新节奏分 async/sync 两档水位,和 Linux 的双阈值同一个思想:先后台劝,超线了强制拖

注意这里的传导链:checkpoint 能推进到哪,由 flush list 的队头决定;恢复时间的上限,就是最老那张欠条的账龄。攒债攒得越凶,重放窗口拖得越长——欠条不只是空间问题,是恢复时间问题。

4. fsync 到底买了什么

write()     → 数据进 page cache,断电可能全丢
fsync()     → 数据 + 相关元数据物理落盘,返回后断电不丢
fdatasync() → 只保证数据本身(省掉时间戳这类元数据)

数据库对 fsync 的执念来自这里:WAL 的"先记后做"里,"记"的持久化必须由 fsync 兑现,否则日志也躺在 page cache 里,断电连日志一起没。

至于为什么 InnoDB 这类引擎干脆用 O_DIRECT 绕开 page cache:数据已经在自己的 buffer pool 里缓存了一份,再进 page cache 是双倍内存;更关键的是写入时机——page cache 何时刷不可控,引擎要自己掌握"哪笔债什么时候还"。绕开操作系统的账本,自己记账自己还。

5. 欠条经济学

把所有 writeback 参数放到同一条轴上看,调的都是同一对乘积:

掉电窗口大小 × 回写批量大小 = 恒定的风险-吞吐权衡

激进(窗口大、批量大)= 高吞吐 + 断电丢得多;保守反之。Linux 双阈值是给这对乘积画了一条带而不是一个点:带内自由积攒(吞吐),触顶强制还债(安全)。InnoDB 的水位线、ZFS 的 txg 周期,全是同一对乘积的不同画法。

Tip

dirty 不是一个状态,是一张欠条。欠条的价值在于把"什么时候还"从正确性问题降级成策略问题——所有 writeback 调优,都是在替业务回答"你能接受丢多少"。

小结

writeback 的完整图景:write 只记账不还钱;flusher 按双阈值的节奏还;fsync 是你亲手清账的按钮;恢复逻辑读账本决定信谁。脏页标记则是整个系统的图灵奖级洞察——用一位 bit 把"不一致"从缺陷重新定义成可调度的资源。

一句话概括:延迟落盘不是偷懒,是把"何时持久化"从硬件约束升级成了业务决策。下一篇是存储支线的最后一篇:双缓冲家族——为什么所有"边用边更新"的方案,命根子都扎在切换的原子性上。


最后更新:2026-09-08

评论