
1. 脏页从哪来写操作背后的内存账本很长一段时间里我在排查服务器性能问题时总是先看 CPU、再看磁盘 IO直到被一次“负载不高但写入极慢”的故障折磨了一整天才反应过来——真正拖慢系统的是内存里的脏页而不是磁盘本身。从那以后我养成了一个习惯遇到写入性能异常先打开/proc/meminfo看脏页水位再决定要不要怀疑磁盘。如果你也是第一次接触这个概念可以从一个生活场景切入。想象你在一家餐厅后厨工作客人点了菜你不会每炒一道菜就往洗碗间跑一趟而是攒够几盘或者等洗碗工催促了再一起送过去。Linux 的写操作也是同样的逻辑应用程序调用write()时数据并不会立刻落到磁盘而是先写进内存中的页缓存page cache并给这些页面打上“已修改”的标记。这个“已修改但还没写回磁盘”的页面就是脏页dirty page。这里要区分两个概念页缓存和脏页。页缓存是内核用来缓存文件数据的通用内存池任何读过的文件内容都可能留在里面方便下次命中。只有那些被修改过的页缓存页面才会从普通缓存状态变成脏页状态。换句话说脏页是页缓存的一个子集是被写过但尚未落盘的页面。脏页的产生看似简单实际上有三个必要条件。第一应用程序对文件发起写操作比如echo重定向、数据库的 redo log 写入、日志文件的追加等。第二数据落在页缓存里而不是直接通过 O_DIRECT 绕过缓存。第三内核还没来得及把这块数据写回磁盘。第二点很重要因为 O_DIRECT 模式会绕过页缓存这种情况下根本不会有队列化的回写问题但代价是每一笔写入都要同步等待磁盘完成。既然脏页最终总归要写回磁盘为什么内核不立刻执行呢这就涉及性能与安全性的权衡。如果每一笔写操作都同步刷盘应用进程会被磁盘延迟卡死批量写入场景的吞吐量会断崖式下降。但反过来如果脏页无限堆积万一机器断电或者内核崩溃内存里的修改就全丢了。所以内核设计了一套回写机制既允许脏页在内存里攒一会儿又通过多种条件保证它们不会无限期滞留。这也是我写这篇东西最想讲清楚的部分回写不是“想起来就写”而是有一套完整的触发逻辑。2. 脏页回写的四种触发方式定时、水位、同步与全局压力内核里和脏页回写相关的核心代码在fs/fs-writeback.c日常使用中我们不需要读源码但需要理解它的四种触发路径。这四种路径互不排斥可能同时生效而且不同路径的优先级和写入方式也不一样。第一种是定时回写。内核有一个周期性任务每隔dirty_writeback_centisecs的时间默认值是 500单位是百分之一秒也就是 5 秒唤醒一次检查是否有脏页需要处理。如果系统里存在超过dirty_expire_centisecs默认 3000即 30 秒还没有被写回的脏页就会触发一次过期回写。简单说即使系统非常空闲脏页在内存里待满 30 秒后也会被强制写盘。这个机制保证任何脏页都不会滞留太久。第二种是比例水位触发。内核设置了两个阈值dirty_background_ratio和dirty_ratio。当脏页占内存的比例超过dirty_background_ratio默认 10%时内核会启动后台回收线程flush 线程开始写回此时应用进程不会被阻塞。当脏页比例继续攀升超过dirty_ratio默认 20%时内核就会强制让发起写操作的进程同步等待一边写一边刷盘直到脏页比例降回安全线以下。这个过程在业务侧的表现就是写入突然变慢iostat里await升高但 CPU 和磁盘使用率都不高。第三种是同步写回。当某个进程执行fsync()、fdatasync()或者文件关闭时内核会直接把该文件关联的脏页同步写盘等待完成才返回。数据库场景尤其依赖这层保证MySQL 的innodb_flush_log_at_trx_commit1就是靠fsync确保 redo log 不丢的。需要特别注意的是fsync触发的不只是当前进程的脏页而是整个文件的所有脏页如果多个进程同时写同一个文件其中一个调用fsync其他进程的数据也可能被跟着刷下去。第四种是内存压力触发。当系统内存紧张时内核要回收页缓存来满足其他内存请求这时候脏页就不能直接丢弃了必须先写回磁盘再释放。这个路径最容易引发性能抖动因为它同时涉及内存回收和磁盘写入两条链路。我在实际排查中见过不少这类案例系统内存只剩 200MBkjournald或者kswapd进程突然占满 CPU磁盘写入开始飙升实际上就是在处理内存压力触发的脏页写回。四种触发方式的侧重点完全不同。定时回写负责最终一致性比例水位负责平滑控制节奏同步写回负责数据安全内存压力回写负责应急。现实中你看到的写入波动往往是好几种机制叠加的结果很少是单一因素。这一点在排查时尤为重要光看脏页比例还不够还要确认是哪个触发源在主导。3. 内存页交换物理内存不够时的腾挪术聊完脏页必须接着聊另一块内容内存页交换swap。脏页回写解决的是“数据往磁盘同步”的问题而页交换解决的是“物理内存不够用时把哪些页挪到磁盘”的问题。两条链路都会产生磁盘 IO也都受内存水位影响但它们的对象和处理逻辑很不一样。swap 的基本原理并不复杂当物理内存不足时内核把一部分内存页写到磁盘的 swap 分区或 swap 文件中释放物理内存给更需要的进程当进程再次访问这些页面时内核通过缺页异常page fault把数据从 swap 里读回内存。整个过程对应用是透明的但代价是 swap 的读写速度比物理内存慢几个数量级所以 swap 用得多通常意味着性能在恶化。这里有个关键参数叫swappiness取值范围是 0 到 200新版内核放宽到 200旧版常见范围是 0 到 100。它控制的是内核在内存回收时倾向于回收匿名页进入 swap还是文件页直接丢弃或回写。swappiness值越高内核越积极地把不常用的匿名页换出值越低内核越倾向于回收文件页。默认为 60表示两种回收的倾向相对平衡实际效果是“文件页优先但匿名页也有可能被换出”。很多运维喜欢把vm.swappiness调到 0 或 1目的就是尽量不用 swap这在内存足够时是合理的但在内存真的不够时反而会让系统陷入更深的危机后面我会细说。页交换的执行主体是内核线程kswapd和直接回收路径。kswapd是一个后台守护线程它监控内存水位线当空闲内存低于某个阈值时开始回收内存页。回收的目标内存页分两类文件页和匿名页。文件页如果干净不是脏页直接丢弃就行磁盘上还有副本如果是脏页需要先写回再丢弃匿名页则只能先换出到 swap。所以你会看到内存紧张时脏页回写和 swap 几乎总是同时发生——因为kswapd既要处理文件页的脏页写回又要处理匿名页的换出。和脏页回写类似页交换也分轻量级和重量级路径。轻量级是kswapd在后台慢慢回收不阻塞应用重量级是当内存水位跌到极致时进程在自己执行内存分配时同步参与回收也就是“直接回收”direct reclaim。直接回收是性能杀手它在进程申请内存的路径上同步执行回收操作进程会卡住系统整体吞吐量骤降。判断系统是否进入了直接回收状态可以看/proc/vmstat里的pgscan_direct和pgsteal_direct字段这两个值在持续快速增长时基本可以断定内存压力已经非常严重了。还有一个值得注意的现象文件页在内存压力下被丢弃后如果进程再次读这个文件还会从磁盘重新加载到页缓存。这就会造成一种奇怪的现象——内存明明很紧张swap 的占用却不高而磁盘读入量却很大。很多同事看到siswap in不高就认为内存没问题其实真正的瓶颈是页缓存的往复颠簸。4. 关键参数与水位线怎么调才不踩坑每次有人问我 Linux 内存相关的调优怎么入门我都会先让他记住一句话先看水位线再看参数。水位线watermark是内核内存管理的核心状态指示参数只是控制水位线附近的决策行为。如果不理解水位线直接调参数就是盲人摸象。内存水位线在/proc/zoneinfo里可以看到主要分三种pages_min最低水位、pages_low低水位、pages_high高水位。当空闲内存低于pages_low时kswapd开始后台回收低于pages_min时内核会启动直接回收同时可能触发 OOM killer。了解这个机制后再看vm.min_free_kbytes就豁然开朗了这个参数可以直接拉高pages_min的值让内核更早开始回收而不是等到内存快耗尽才手忙脚乱。不过调大min_free_kbytes意味着内核会保留更多空闲内存可用内存变少过度设置反而会降低缓存利用率。再回到脏页参数。vm.dirty_background_ratio和vm.dirty_ratio控制脏页水位的上限它们本身是比例值但也有对应的绝对值形式dirty_background_bytes和dirty_bytes。这两组参数是互斥的内核文档明确说明不能同时设置比例和字节数实际配置时也只能选一种。在内存很大的机器上比如 512GB比例值的颗粒度会显得太粗dirty_ratio默认 20%意味着脏页堆积到 100GB 才会触发同步等待这个量级对磁盘来说几乎是灾难。所以大内存机器上我一般建议用字节数限制比如把dirty_bytes设置成 8GB 到 16GB让回写节奏更平滑。说到调优经验有几点是踩过坑之后才总结出来的。第一数据库类应用不建议把swappiness调到 0。数据库通常自己管理缓存和缓冲区操作系统层面的 swap 对数据库性能是负面的这一点没错。但如果swappiness0导致内核在内存紧张时完全不换出匿名页就只能疯狂回收文件页数据库的共享内存和页缓存会被反复颠簸结果反而是更差的性能和更高的延迟。折中方案是把swappiness调到 10 左右让匿名页有可能被换出但不至于太积极。需要注意的是swappiness0并不是绝对禁用 swap只是“尽可能不换出匿名页”真要到了极端内存压力下内核照样会换出。第二脏页比例参数要看磁盘的实际写能力来定。如果磁盘是很慢的机械盘dirty_ratio设得太高会让同步等待失控反之如果后端是 NVMe SSD适当调高脏页比例反而能提升写入合并效率。我曾在一台全闪存储节点上测试把dirty_background_ratio从默认 10 调到 5延迟稳定了但吞吐下降约 12%调到 15吞吐上去了但峰值延迟偶尔会飙升到原来的 5 倍。最终选在 8属于折中。这个例子说明不要盲目照搬默认值或网上的推荐值你的磁盘性能和业务写入模式才是指挥棒。第三修改这些参数时要注意持久化。sysctl -w只是临时生效重启就丢了。正确做法是把配置写入/etc/sysctl.conf或/etc/sysctl.d/下的独立文件比如/etc/sysctl.d/99-custom-memory.conf。下面是一个我常用的基础配置模板# 脏页回写相关 vm.dirty_background_ratio 5 vm.dirty_ratio 10 # 让kswapd更早介入内存回收 vm.min_free_kbytes 1048576 # 内存足够时降低换出倾向但不完全禁用 vm.swappiness 10 # 提高缓存回收的效率可选 vm.vfs_cache_pressure 200这个模板适合 64GB 到 128GB 内存的通用服务器只是起点不是标准答案。每台机器的负载特征不一样必须结合监控数据调。5. 实际排查用工具撕开脏页与交换的真相理论讲了这么多落地的时候还是要靠工具。这里分享一套我自己常用的排查链路每次遇到内存相关的性能问题都先按这个顺序走一遍能少走很多弯路。第一步看整体内存态势。执行free -h和cat /proc/meminfo重点关注这几个字段Dirty当前脏页总量、Writeback正在写回的脏页量、SwapTotal和SwapFreeswap 总容量和剩余、MemAvailable可用内存的大致估算值。Dirty的值会波动但如果长时间维持在高位不降说明回写可能遇到了瓶颈。第二步看历史趋势。用sar -r查看历史内存使用和 swap 活动用sar -B查看页交换活动pgpgin和pgpgout表示换入换出的页数用sar -d查看磁盘活动。历史数据的价值在于对比你可以知道这个问题是偶发还是持续和业务高峰是否对应修改参数前后有没有改善。我遇到过一例脏页堆积故障靠sar -B看到pgpgout在每日凌晨的备份任务期间暴涨才定位到是备份脚本触发了大面积文件写操作和内核配置无关。第三步定位实时进程。top里按P排序看 CPU注意有没有kswapd刷到列表前面按M排序看内存看哪个进程的 RES 特别大。如果kswapdCPU 占用高但没有任何进程的 RES 离谱那大概率是页缓存回收压力大而不是某个具体应用的锅。再配合pidstat -r看每个进程的缺页异常次数指标是majflt主缺页需要访问磁盘和minflt次缺页内存内就能解决。majflt飙升意味着进程在不断从磁盘加载页面典型的内存不足信号。第四步验证水位。查看/proc/zoneinfo里每个 zone 的pages_low、pages_high和当前nr_free_pages。如果nr_free_pages长期低于pages_low说明kswapd一直在后台回收系统处于慢性内存压力状态。这种情况下即使业务没有报错也应该警惕了因为一旦遇到突发内存请求直接回收会在所难免。我自己完整走一遍大约五分钟比起盲调参数要高效得多。有一次在客户环境遇到应用写入延迟抖动第一反应是磁盘坏了但iostat显示磁盘利用率不到 30%await却高达 300 毫秒。按上面流程走下来发现Dirty持续在 6GB 以上nr_free_pages低于pages_lowmajflt频繁出现——是内存压力导致回收线程和业务写线程互相争抢磁盘 IO所以延迟才会被放大。后来把内存从 16GB 扩到 32GB问题直接消失。6. 常见误区与面试常见考点顺带把知识补完整这些机制也是 Linux 面试的常客毕竟内核知识是后端工程师和运维工程师的硬功夫。结合常见误区和考点一起讲相当于把零散的知识点串成系统框架。第一个高频误区是“swap 用得多就代表性能差”。这句话在大方向上没错但换个角度swap 用得恰到好处其实是内存资源池化的体现内核把不太活跃的匿名页放到磁盘把内存留给活跃的页缓存整体命中率反而更高。真正需要警惕的是持续的 swap in 和 swap out也就是内存颠簸。判断是否颠簸不要只看free里的 swap used要看/proc/vmstat的pswpin和pswpout是否持续增长。第二个误区是“脏页比例高就一定要调低”。脏页比例高有两种情况一种是回写能力不足导致堆积这时调低dirty_background_ratio让回收更早开始是有意义的另一种是写入吞吐本身很高脏页生成速度快而回写也在持续进行此时脏页比例维持在一个较高但稳定的水平并不是故障信号。区别在于趋势稳定的高位是稳态持续攀升才是危险信号。所以我说调参之前一定要有监控数据支撑不能凭感觉动参数。第三个误区是“swappiness0等于禁掉 swap”。前面已经说过这只是降低内核换出匿名页的意愿真正的硬性禁用是去掉 swap 设备或swapoff。而且在实际的 Linux 内核实现中即使把swappiness设为 0在内存严重不足时内核依然会通过 OOM killer 来释放内存而不是直接换出匿名页——这就意味着进程可能被莫名杀掉。生产环境我见过不止一次因为过度调低swappiness导致 OOM 误杀的案例代价非常沉重。面试题方面有几个经典问题可以覆盖大部分相关考点。脏页回写有哪几种触发方式除了定时、阈值、内存压力别忘了同步调用fsync/sync。dirty_background_ratio与dirty_ratio的区别是什么一个是后台异步回收不阻塞一个是同步阻塞应用。什么是内存水位线pages_min、pages_low、pages_high各代表什么状态kswapd的作用是什么它和直接回收有什么差异文件页与匿名页的回收有什么区别文件页干净时直接丢弃脏时需要先写回匿名页必须换出到 swap。OOM 的触发时机是什么通常是在内存分配时无法满足且回收无效内核调用 OOM killer 选择进程终止。这些知识点如果只看结论而不理解机制很容易在具体场景中卡壳。比如有人问我“为什么/proc/meminfo里的MemAvailable和free命令的 available 不太一样”这就涉及到页缓存可回收性的估算逻辑free命令显示的是内核导出的估算值不同版本内核的计算方式有差异。这类问题没有源码级知识也能答对但知道了脏页和水位的联动机制后解释起来会自然很多。7. 写在最后把内核机制当作性能优化的地图从脏页回写到 swap 页交换再到水位线和kswapd这些知识点单个拆开都不太难难点在于它们会互相影响。内存紧张时kswapd既回收文件页又换出匿名页文件页中的脏页触发回写回写占用磁盘 IO 导致业务读写延迟上升延迟上升又让内存分配变慢形成恶性循环。理解了这条链路Linux 内存和 IO 的大半问题都能顺藤摸瓜找到根因。我在实际排查中最大的体会是内核参数本身没有绝对的对错只有适合不适合。默认参数照顾的是通用场景生产环境必须根据自己的业务写入模式、内存容量、磁盘性能去调。调优过程中监控是底线小步调整是方法校验对比是验证手段。再分享一个实用技巧每次调整内核参数后我建议记录三样东西——调整前的监控截图、调整后的监控截图、变更说明。这三样东西在问题复发时是排查的关键凭证。有一次我在群里看到一个同行发了一个性能问题底下的讨论从内核配置扯到了文件系统最后发现根因是备份脚本把目录权限改了导致缓存失效如果没有变更记录这种问题排查起来非常折磨。最后想说的是Linux 内核机制的核心文档其实就摆在那里Documentation/admin-guide/sysctl/vm.rst和Documentation/admin-guide/sysctl/fs.rst。遇到参数不懂时先翻文档再看实际效果最后综合判断。这套流程听起来朴素但远比网上东拼西凑的“推荐配置”可靠得多。