操作系统内功 · 第 63 篇|dirty 页面调参实战:vm.dirty_ratio 与写密集负载的调优

两级水位:黄线与红线

第 21 篇讲过 writeback 机制:脏页由内核回写线程按条件刷盘,时机有三类——超龄、水位、应用主动 fsync。这一篇讲驾驶侧的调参:脏页总量怎么管、谁在什么时机被踩刹车。核心是两组参数构成的两级水位:vm.dirty_background_ratio 与 vm.dirty_background_bytes 是黄线(后台线),脏页超过它,后台回写线程开始加速干活;vm.dirty_ratio 与 vm.dirty_bytes 是红线(阻塞线),脏页逼近它,正在写文件的进程被拖进 balance_dirty_pages 节流——write 系统调用当场变慢,进程按脏页生成速率被限流到回写速度,节流点在系统调用里,所以卡的是写应用全体,而不是某个倒霉进程。

两级设计的意图是提前错峰:黄线触发后台清活,理想情况下脏页永远到不了红线;红线是保底,防止回写能力真跟不上时脏页无限堆积吃光内存。默认口径多年稳定:dirty_background_ratio 为 10、dirty_ratio 为 20(都是占可用内存的百分比),另有两个时间参数——dirty_expire_centisecs 为 3000(脏页存活超 30 秒即可被刷)、dirty_writeback_centisecs 为 500(回写线程每 5 秒醒来巡一次)。

bytes 与 ratio 两套刻度互斥生效:同一层级一旦设置了 bytes,对应 ratio 即作废(内核把它清零处理)。大内存机器一律建议改用 bytes,原因下一节展开;这也是近几年各类性能手册一致的推荐——ratio 这套百分比刻度是为小内存时代设计的,在几百 GB 内存的服务器上已经荒谬。

时间参数的语义也顺带补全:dirty_expire_centisecs 与 dirty_writeback_centisecs 分别是「脏页多少秒后可刷」与「回写线程唤醒周期」(单位是百分之一秒,默认值对应 30 秒与 5 秒)。它们控制的是及时性下限——哪怕水位很低,超龄脏页也会被清;把 expire 调小能压数据丢失窗口,代价是更多小批量回写、后端负载更碎。水位管上限、时间管下限,四组参数合起来才是完整的水位体系。

水位还有两个口径细节要钉死,免得看文档犯迷糊。其一,ratio 的基数不是全部物理内存,而是「可用内存」(空闲加可回收的页缓存部分),同一台机器的水位会随内存压力小幅浮动;其二,回写的记账单位是块设备——每个存储设备一套回写结构,全局水位按各设备的回写权重分摊下去,/sys/class/bdi/ 下的权重上下限旋钮决定慢盘最多能占多少全局回写带宽,多块速度悬殊的盘共存时必须过目。

写风暴的体感:卡顿从哪里来

写密集负载的经典体验是「平时飞快,每隔一阵整段卡死」。把机制翻译成时间线:应用狂写,页缓存接住,write 立即返回,脏页水位上涨;涨过黄线,后台回写启动,但后端(一块机械盘、一个 USB 盘、一段拥堵的 iSCSI)吞吐有限,脏页继续涨;触到红线,所有写进程进入节流,write 从微秒级返回变成毫秒甚至秒级阻塞;回写把水位压下去,write 恢复飞快——然后重复。iostat 上就是教科书锯齿:每秒写入量顶格冲一段、await 飙高,随后写入归零、应用卡住,周而复始。典型案例:向 U 盘拷一个大文件,前几百 MB 神速(进了页缓存),随后进度条走走停停——停的部分正是在等红线以下的回写。

这就引出 ratio 的原罪:它是百分比,基数是内存容量。256 GB 内存的机器,黄线 10% 就是约 25 GB 的脏页配额——一台 200 MB/s 的盘要花两分钟才能清完,这期间应用崩溃就丢 25 GB 窗口;红线 20% 更是约 50 GB,节流风暴一旦触发,写应用集体顿足几十秒。容量越大,百分比刻度越荒谬,这就是大内存机器改用 bytes 计的根本理由:把黄线钉在 256 MB、红线钉在 1 GB 这样的绝对值上,行为不再随内存扩容漂移,容量规划与故障复盘都有据可依。

再补一层容易被误读的机制细节:节流不是「脏页到红线就原地罚站」,而是按比例调油门——脏页生成越快、离红线越近,write 内部的休眠越多,理论上能让写速度收敛到回写速度。所以红线附近的体验更像「逐步刹停」而不是「撞墙」;但在后端吞吐远小于写入速率时,收敛速度本身就慢,体感仍是长卡顿。理解这一点,才能理解为什么「把红线调低」比「调高」更常被推荐:低红线让刹车提前、平缓介入,而不是等临界点猛踩。

把数字走一遍更有体感。同样默认参数:16 GB 台式机的黄线约 1.6 GB,一只 100 MB/s 的 U 盘清掉要十几秒——U 盘拷贝的锯齿几乎人人见过;256 GB 的数据库主机黄线约 25 GB,而它挂的高性能存储可能每秒几个 GB——水位还没到黄线,写入可能先被存储层限流,参数反而从未触发。同样的百分比,小内存机器上太迟钝、大内存机器上太危险,两个方向都指向同一句话:参数必须随内存量级与后端吞吐重估。

节流的体感还有个「度」的问题值得单独说:黄线之上的后台回写与应用几乎无感(IO 在后台排队慢慢消化),红线之内的节流则是同步伤害。所以判断一台机器「还有多少余量」,看的不是写入吞吐峰值,而是脏页水位离红线有多远、后端带宽还剩多少——水位贴近黄线是健康的工作状态(后台回写被用起来了),贴近红线就是警报。把这两条线画进监控面板,比任何抽象的 IO 指标都直观。

一致性窗口:多丢多少数据的权衡

调参的本质是在三个变量间找平衡:回写的及时性(数据丢失窗口)、写路径的平滑度(毛刺)、后端吞吐的利用率(要不要攒大批再刷)。dirty_expire 与 dirty_writeback 控制及时性下限——哪怕水位低,超龄脏页也会被刷;黄线红线控制堆积上限。降低两级水位等于更小的丢失窗口加更频繁的回写,代价是写路径更早被节流;调高则相反。没有普适最优,只有按负载画像取舍。

与 fsync 的分工要分清(系列讲文件系统与 fsync 语义的一篇已经铺垫):fsync 是应用主动关闭自己那份窗口,dirty 参数管的是「没 fsync 的部分」暴露多少。数据库 WAL 这类关键路径靠 fsync 保证,不指望页缓存调参救——但 WAL 以外的旁路写(错误日志、临时表、监控采样、慢查询日志)仍受 dirty 水位影响,极端配置下它们在红线处被节流,会反噬主路径的延迟。数据库主机把硬红线压低还有一层意义:限制脏页堆积对读路径的污染——大量脏页挤占页缓存,读命中率先掉,延迟问题会升级成性能复合灾难。

还有一条跨主机的联动别忘了:共享块存储(SAN、分布式块存储)上多台主机的回写风暴会互相挤压后端带宽,单机的黄线红线配置要与存储侧的 QoS 一起看。「这台机器参数没动过为什么突然卡」的答案,经常在另一台主机或存储控制器上——水位调参从来不是单机游戏。

虚拟化与容器环境再补一层:guest 里改 dirty 参数只管 guest 自己的页缓存,宿主的回写行为、存储后端的限流都在 guest 视野之外——云主机上「参数改了体感没变」的第一嫌疑就是宿主层在限流。反过来,宿主上跑多台虚拟机时,宿主的全局水位要按所有 guest 峰值写入之和重新规划,否则一台 guest 的写风暴就能把宿主推进红线、殃及全部邻居。物理机的参数直觉进虚拟化要过两遍脑子:guest 一遍、宿主一遍。

负载画像与推荐组合

按负载给几组常见配置思路,数值是量级参考,落地前务必用压测校准。数据库与低延迟服务:压低两级水位换平滑,dirty_background_bytes 取 128 至 256 MB、dirty_bytes 取 512 MB 至 1 GB 是常见区间——回写器一直有活干,任何时刻的节流深度被限制在亚秒级,WAL 之外的旁路写不再有冲击红线的体量。消息队列与日志类顺序写大户:目标是吞吐,允许较大批量,黄线可以给到 512 MB 至 2 GB,红线给 2 至 4 GB,让顺序写尽量合成大块下盘,一致性交给副本机制——Kafka 类系统的设计前提本来就是「页缓存加批量刷」,给它配小水位反而制造无谓的节流。文件服务器与 NFS 网关:中等水位(几百 MB 量级),避免单一大文件写把全机写路径拖进节流。桌面与移动介质场景:两级都压小(几十 MB),用户体验优先——「复制进度条卡住」比吞吐低更招人烦。

实施与回滚的工程姿势:sysctl -w 即时生效,写进 /etc/sysctl.d/ 下的独立文件持久化(单独一个文件,方便变更管理而不是混在主配置里);变更前后各留一份全量 vm 参数快照;灰度按「先黄线后红线」的顺序调——黄线只影响后台回写强度,风险小,先观察锯齿是否变浅,再动会直接节流应用的红线。有混合负载的机器还要看每设备差异:/sys/class/bdi/ 下有每个块设备的回写权重可调(min_ratio/max_ratio),一只慢盘不该吃掉全机回写带宽。

观测点与验证方法

观测四个点位。其一,/proc/meminfo 的 Dirty 与 Writeback 两行:当前脏页总量与正在回写的量,看水位离红线多远——「离红线还有多远」应该成为写密集主机的常设监控项。其二,/proc/vmstat 的 nr_dirty 时间序列(监控系统采这个,趋势比瞬时值有用)。其三,iostat -x 1 的每秒写入次数、写入吞吐、await 与利用率组合判型——await 突刺与应用延迟毛刺对齐即节流发作;其四,回写线程的活跃度(观察 kworker 类线程的 CPU 占用)辅助判断「回写在干活还是卡住了」。想复现节流做实验:dd 大文件到慢速介质,同步开 watch 持续 grep Dirty,红线触发的瞬间一目了然。

验证方法照压测三段走:先默认参数跑基线 fio(顺序写、随机写加 fsync 混合三套),记录 write 延迟分位数与吞吐;再改参数重跑对比;最后上真实业务影子流量收口。两个常见误判要避开:把「write 偶尔慢」直接归罪于盘(其实是红线节流,看 Dirty 水位与卡顿时机是否对齐就能定性);反过来把「iostat 几乎无写入、应用却卡」当成节流(那是锁或网络的问题)。先确认水位曲线与卡顿时间轴对齐,再下结论——这句是本篇最值钱的一句方法论。

两条最常用的现场命令:

grep -E '^(Dirty|Writeback)' /proc/meminfo     # 当前脏页与在途回写
iostat -x 1                                    # 各设备写入吞吐与 await

配合 watch 循环盯住 Dirty 行随时间的变化,水位触发的时刻可以直接对齐到应用日志的时间戳。再把 Dirty 持续逼近红线的阈值写进监控告警,节流事故就能从「用户投诉驱动」变成「指标驱动」。

实验设计里再补一个变量控制提醒:调参实验要与「清空缓存」类操作隔离——清缓存后首轮写入行为与稳态完全不同,实验数据要以稳态段为准;同理,比较不同参数的基准结果时,运行时长要足够覆盖多个锯齿周期,否则抓到的可能只是暂态。性能实验的第一纪律是分清稳态与暂态,水位实验尤其如此。

小结

dirty 参数的两级水位是页缓存写路径的油门与刹车:黄线启动后台回写,红线节流写进程。默认的百分比刻度在大内存机器上已经失真,改用 bytes 绝对值是当代运维的一致共识;「先黄线后红线」的灰度顺序能把变更风险压到最低。

负载画像决定参数形态:数据库要低水位买平滑与更小的旁路冲击,消息队列要高水位买批量与吞吐,文件服务器取中,桌面压小。没有普适参数,只有与后端吞吐、丢失窗口容忍度匹配的参数;fsync 管关键数据的一致性,dirty 参数管其余数据的暴露面。

方法论收束成三句话:先观测(Dirty/Writeback 水位加 iostat 锯齿形态),再画像(读写比、后端吞吐、丢失容忍度),后灰度(黄线先行、红线殿后)。写路径的「玄学卡顿」大多能在水位曲线与卡顿时间轴的对齐里现出原形。

最后重申本篇与第 21 篇的分工:机制篇回答「内核在什么时候刷盘」,本篇回答「你希望它什么时候刷」。前者是内核的实现事实,后者是业务目标的翻译——先把业务对丢失窗口与延迟毛刺的容忍度写成数字,再去反推参数;顺序反了,就是拿默认值赌生产环境。下一篇转向安全与性能的交点:Spectre、Meltdown 与 KPTI——当 CPU 的优化机制本身成为攻击面。

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?