操作系统笔记:Page Cache 与回写(writeback):为什么会抖
发布于:
线上常见的“IO 抖动”现象里,page cache 与 writeback(回写)经常是主角:吞吐看起来没变,但 P99/P999 延迟突然上升,甚至出现周期性尖刺。
这篇笔记把 page cache 当成一个“缓冲 + 合并 + 延迟写”系统来看,重点解释:
- 写入为什么看起来很快(写进 cache 就返回)
- 为什么会突然变慢(脏页太多触发回写/节流)
- 回写如何影响前台读写延迟(IO 队列竞争)
- 工程上如何观测与定位(脏页水位、回写速率、direct IO 的取舍)
1. page cache:把文件 IO 变成内存 IO(大部分时候)
page cache 是内核维护的文件内容缓存(以页为单位)。读写路径可以粗略抽象为:
1.1 读
read(file, offset, len):
if pages in page cache:
memcpy to user buffer
else:
schedule disk read -> fill cache -> memcpy
1.2 写(buffered write)
write(file, offset, data):
copy data into page cache pages
mark pages dirty
return (often before disk write completes)
写得快的原因:很多写只做了内存拷贝 + 标记脏页。
2. 脏页(dirty pages):延迟写带来的“债务”
脏页 = page cache 中被修改但尚未写回磁盘的页面。
脏页带来两个核心风险:
- 持久性窗口:崩溃会丢最近写(取决于 fsync/写策略)
- 回写压力:脏页积累到一定程度后必须写回,否则内存会被占满
为了控制风险,内核有一套水位与策略(抽象):
- background threshold:超过后后台开始回写
- dirty threshold:超过后前台写会被节流(throttle),甚至阻塞等待回写
3. writeback:后台回写与前台节流
3.1 为什么会抖:从“写进 cache”到“等磁盘”
当系统健康时:
- 写入速率 <= 磁盘可持续回写速率
- 脏页水位稳定
当写入突增或磁盘变慢时:
- 脏页上涨
- 达到阈值后触发更激进的回写
- 前台写被节流,延迟上升
time ->
dirty pages:
low/steady -> rising -> hit threshold -> throttle -> recover
latency:
low -> ok -> spikes (throttle) -> ok
3.2 回写为何会影响读延迟
回写会占用:
- 磁盘带宽
- IO 队列深度
- 设备内部缓存/调度
如果没有 QoS 隔离,前台读与后台回写会在同一设备队列竞争,读 P99 会抬升。
4. fsync / fdatasync:把“债务”提前结清
应用调用 fsync 的语义是:要求相关数据与元数据落盘(抽象,具体取决于文件系统)。
工程直觉:
- fsync 频繁:写延迟更高、更稳定(把回写前移到请求路径)
- fsync 很少:写延迟低,但可能周期性抖(债务集中结算)
如果系统依赖 WAL(写前日志):
- WAL 通常要求 fsync(或等价持久性)保证提交语义
- 数据文件可以更异步(取决于设计)
5. buffered IO vs direct IO:两条路
5.1 buffered IO(默认)
优点:
- 利用 page cache 提高命中率
- 合并小写、顺序化写
缺点:
- 脏页 + writeback 带来不可预测的尾延迟
- 双缓存(应用缓存 + page cache)可能浪费内存
5.2 direct IO(绕过 page cache)
优点:
- 延迟更可预测(少了脏页债务)
- 避免双缓存
缺点:
- 应用必须自己做缓存与对齐(复杂)
- 小 IO 性能可能更差(无法利用 cache 合并)
工程常见折中:
- 数据文件 direct IO + 应用缓存
- 日志文件 buffered + 频繁 fsync(或相反,视系统)
6. 观测指标与排查
6.1 脏页与回写指标
建议关注:
- 脏页占比/大小(dirty pages)
- 回写速率(writeback MB/s)
- 节流次数/时长(writeback throttle)
- IO 队列深度、设备 util、平均等待时间
6.2 常见诊断路径
现象:写延迟周期性尖刺
排查:
- 看脏页是否周期性冲到阈值
- 看回写速率是否跟不上写入速率
- 看设备是否在某些时段变慢(GC、重映射、共享盘干扰)
现象:读 P99 随写入升高
排查:
- 回写是否占满队列导致读被排队
- 是否需要 IO 优先级/队列隔离
7. 小结
page cache 把大量 IO 变成“内存操作”,但它引入了一个必然的系统性代价:脏页债务。写入突增或磁盘变慢时,债务会被集中回收,从而表现为 writeback/throttle 造成的尾延迟抖动。工程上,稳定性依赖:
- 脏页水位与回写速率的匹配
- 前台与后台 IO 的隔离能力
- 是否选择 direct IO/应用缓存的架构取舍