提到 pagecache 引起的生产速度变慢很多人第一反应是不信pagecache 是内核拿来加速文件读写的缓存不给你减速就不错了怎么还可能反过来拖垮生产吞吐。可我前阵子排查一条批处理链路时生产速度从单机每秒 1300 条掉到 400 条free一查内存里全是 pagecacheCPU 却持续高位空转日志里全是阻塞等待。这篇文章把当时的排查路径、pagecache 背后的内核机制、以及真正落地的调优手段完整拆开讲适合后端开发、运维和正在读监控数据但不知道下一步该做什么的同行。1. pagecache 的基本工作原理它为什么“看起来慢”1.1 pagecache 到底是加速还是减速pagecache 是操作系统为了保护磁盘而生的一层缓冲。当你读取一个文件时内核会把读出来的磁盘块放在内存里下一次再读同一块数据直接命中内存不再碰磁盘。写入也是同样思路应用write()的数据先落进 pagecache标记为“脏页”由内核里的回写线程在后台慢慢刷到磁盘这样应用不必每次write()都等物理硬盘写完。所以 pagecache 本身是实打实的加速器。但生产速度变慢时监控图上第一眼看到往往是“内存的 used 不高buff/cache 占了几十个 G”于是大家就把锅扣到了 pagecache 头上。其实这个判断方向是错的pagecache 占内存不等于它会拖慢系统真正拖速度的是 pagecache “被回收”“被回写”“被反复换入换出”这些动态行为而不是它静态占用了多少内存。1.2 什么时候 pagecache 会引起生产速度变慢pagecache 产生性能问题的典型阶段有三个内存水位紧张阶段系统内存不足内核的回收机制开始扫描 LRU把 pagecache 里的干净页释放把脏页先回写再释放。这个过程如果持续发生会消耗大量 CPU 和磁盘 IO也会让分配内存的进程进入同步等待。脏页回写阶段应用写了很多数据进 pagecache当脏页数量超过/proc/sys/vm/dirty_ratio时write()会从“异步写完就走”变成“同步等刷盘”这个等待直接落在生产线程上吞吐立刻掉一个台阶。缓存抖动阶段如果你手动清理 pagecache或者内存不足时 pagecache 被大量回收随后生产任务再次读到同样的热数据就得重新从磁盘加载。内存空出来很多但磁盘读 IO 变成了新瓶颈整体速度比保留缓存时更差。这也是为什么“pagecache 引起生产变慢”这个问题排查重点不在于“要不要清理缓存”而在于“内核在什么条件下动了缓存动缓存时有没有影响生产线程”。2. 一整套可以复现的排查流程怎么坐实是 pagecache 的事2.1 从 free 和 vmstat 开始接到生产变慢的告警我一般先开两个窗口一个跑free -h一个跑vmstat 1 5。free主要看 available 是否缩水。注意这里的 available 不等于 free它已经把可回收的 pagecache 算进去了。如果 available 还有 20G 以上应用却报内存不足那大概率不是内存不够而是有别的资源卡住了。vmstat主要看这几列参数含义异常时代表什么si/so换入换出 4KB 页数量持续非零说明系统已经在做 swap 或反复换页bo块设备写入速度很高时脏页回写可能成为瓶颈wa等待 IO 的时间占比pagecache 回收或回写抢占了磁盘sy内核态时间占比回收、内存扫描、Copy-on-Write 都可能拉高它如果vmstat看到的现象是us不高、sy和wa都不低、bo经常有突发性的高峰同时si/so偶发出现基本可以锁定是内存子系统在做回收和回写下一步直接看更细的内核计量。2.2 追到根源内存状态 PSI 进程 IO只看free和vmstat还不够还需要确认是不是 pagecache 占大头。用下面命令看一组关键字段grep -E ^(MemTotal|MemFree|MemAvailable|Cached|Dirty|Writeback|Shmem|SwapTotal|SwapFree) /proc/meminfo我重点看Dirty和Writeback两个值。如果Dirty长时间超过内存的 5%而且Writeback经常非零大概率是回写路径出了问题。Cached本身大并不说明问题因为 pagecache 就是设计来占内存的它占得越多反而说明热数据命中越多。为了判断生产进程到底有没有被内存阻塞Linux 4.14 之后提供了 PSIPressure Stall Information直接看文件cat /proc/pressure/memory some avg1030.00 avg6010.00 avg3005.00 total123456 full avg100.00 avg600.00 avg3000.00 total0some表示至少有一个任务因为等待内存而卡住的时间占比full表示所有任务都被内存卡住的时间占比。如果some长期超过 20%说明生产线程的调度已经被内存回收影响哪怕free看起来还有富余。再进一步确认是谁在承受 IO 压力用pidstat或iotop定位具体进程pidstat -d 1 5 iotop -o -P生产速度变慢时进程的KB_rd/s如果突然增高而KB_wr/s不高大概率是 pagecache 丢了应用在重新读磁盘换出。这个信号比内存占用百分比更实在。2.3 翻内核日志和容器级证据dmesg -T也是必看项主要找有没有下面这些痕迹Out of memory内存确实耗尽pagecache 回收都救不了kswapd0: 0 pages released之类日志回收线程在空转但系统仍然卡block 层的限流或 IO 调度报错说明磁盘侧已经被写回任务打满。如果生产环境用的是容器或 K8s还要看 cgroup 的内存统计。很多模块跑在容器里容器的 memory limit 不仅限制进程级匿名内存也把 pagecache 算在内。也就是说容器里读了一堆大文件pagecache 涨到接近 limit内核就会在这个容器内做回收而这个回收动作是同步的会直接卡住容器里所有任务的运行。# 容器内或 host 上指定 cgroup 路径 cat /sys/fs/cgroup/memory.current cat /sys/fs/cgroup/memory.stat cat /sys/fs/cgroup/memory.events对比memory.events里的high、max、oom计数器如果high事件在增长说明容器已经频繁越过软限制内核正在回收内存这里边很大一部分就是在回收 pagecache。3. 底层机制拆解让 pagecache 拖速度的三种典型姿势3.1 内存回收风暴kswapd 替应用扛了所有错pagecache 虽然在free里显示为可用内存但它并不是“免费劳动力”。当一个进程需要比较大的物理内存时内核会先去空闲内存链表里找如果找不到就会先触发后台回收kswapd再不行就直接回收direct reclaim。direct reclaim 是同步的谁申请内存谁就负责把内存抠出来。如果这个申请发生在生产进程里进程要一边执行自己的逻辑一边去把 pagecache 里的干净页摘掉、脏页刷盘甚至把其他进程的匿名内存换到 swap。这时候 CPU 大量花在内核态生产线程却被堵在页表操作和 LRU 扫描上。具体表现就是free显示还有大量 pagecache 可回收但生产速度已经掉了。因为 pagecache 的回收成本不低尤其是在磁盘读写压力大的场景下脏页回收要把数据落盘干净页回收也要做 TLB 刷新的清理工作。这种现象在“文件大、一次读入内存、处理完就走”的批处理任务里尤其明显。任务每跑一步都需要文件块pagecache 反复被换入换出内存分配器和回收器在互抢时间业务线程只能排队。3.2 脏页回写写入缓冲反倒是吞吐的刹车片pagecache 引起的生产变慢另一大来源是脏页回写。默认配置里vm.dirty_background_ratio10、vm.dirty_ratio20是常见值。意思分别是脏页占可用内存 10% 时内核后台线程开始异步刷盘脏页占可用内存 20% 时写入进程自己会被按头刷盘write()调用转为同步阻塞。如果一台机器内存 64G那么脏页上限大约是 12G 左右。应用如果短时间写入大量数据比如批量导入、日志盘写、消息落库Dirty增长到上限时后续每个write()都得等磁盘把前面的脏页刷完。磁盘吞吐成了操作系统的“红绿灯”生产速度自然被踩住。还有一种更隐蔽的情况上游应用在疯狂产生脏页但 disk 回写速度跟不上。后台刷盘线程每 5 秒vm.dirty_writeback_centisecs500醒来一次把超过 30 秒vm.dirty_expire_centisecs3000的脏页落盘。一旦积压量超过上限同步刷盘生效不是平滑的减速而是断崖式掉速。我见过一个模块本来每秒写 500 条稳定得很一次大促流量翻了三倍脏页冲到上限同一台机器的写入速度直接掉到每秒几十条。3.3 重新加载冷页drop_cache 和缓存抖动造成的惩罚很多人看到 pagecache 占用高第一反应是sync; echo 3 /proc/sys/vm/drop_caches把缓存清掉。这招在测试环境验证内核行为可以放到生产环境就是给自己挖坑。drop_caches只能释放干净页、目录项和 inode 缓存脏页并不会因此消失得先sync或者等回写。清理之后pagecache 里的热数据全部丢掉下次生产任务访问同样的文件又要从磁盘读一遍。磁盘读 IO 比内存命中慢好几个数量级刚刚“省下来”的内存马上变成双倍 IO 开销。更麻烦的是很多任务不是跑一次就结束而是循环扫描同一批文件。清掉 pagecache 后第一轮读盘会显著拉长任务执行时间整个生产链路都会出现“周期性踩刹车”的现象。这个状态下你用iostat -x 1会看到%util很高但vmstat里的内存字段却很正常。如果不结合业务进程的 IO 行为很容易误判成磁盘故障。所以我的建议是drop_caches只在两类场景使用一是做内核内存回收相关的实验二是你想验证“如果生产进程没命中 pagecache 能不能扛得住”这种极端场景。日常生产环境不要把它当作清理工具。4. 可以用到生产环境的调优手段4.1 用 sysctl 调整脏页行为并确认参数pagecache 引起的慢很多时候不需要牺牲内存去换调整内核的脏页回写参数就能见效。核心参数就四个vm.dirty_background_ratio vm.dirty_ratio vm.dirty_expire_centisecs vm.dirty_writeback_centisecs你可以按下面思路做二次调优如果业务是写多且写入峰值明显把dirty_background_ratio调低到 2~5让后台回写在脏页积累初期就开始刷盘避免攒到高峰同步阻塞dirty_ratio建议不大于 15。设太高突发写入会攒出一大堆脏页一旦同步刷盘磁盘 IO 瞬间打满设太低则写吞吐会被磁盘极限拖死。我有一个折中习惯先设dirty_background_ratio3、dirty_ratio10压测看曲线再微调。dirty_expire_centisecs默认 3000表示脏页 30 秒没落盘就会被强制刷。如果你的业务对持久化实时性要求高可以降到 1000~2000让脏页老得没那么快。dirty_writeback_centisecs默认 5005 秒一般不用动。如果刷盘线程醒来太频繁反而会打高 CPU 和 IO低于 300 就不建议了。例如sysctl -w vm.dirty_background_ratio3 sysctl -w vm.dirty_ratio10 sysctl -w vm.dirty_expire_centisecs1500写入/etc/sysctl.d/99-pagecache.conf用sysctl --system生效。需要注意这个方案只解决回写节奏问题不解决内存回收问题。如果系统本身就处于频繁回收的状态光调 dirty 参数效果会打折。4.2 按容器/进程边界限制 pagecachepagecache 是全局的但很多生产系统已经容器化容器的 memory limit 天然限制了 pagecache。用好这层隔离比全局调内核参数更精准。在 cgroup v2 环境里给某个处理大文件的模块设置memory.max比如设为 8G那么该模块读入文件的 pagecache 最多占 8G超过之后内核会优先回收这个 cgroup 里的页缓存不会干扰其他模块也不会让整个宿主机 kswapd 一直在跑。# 假设 cgroup 名为 service-a echo 8G /sys/fs/cgroup/service-a/memory.max这个方案的要点是业务进程要注意它分配内存的峰值不能只给应用匿名内存留总量得把 pagecache 的余量也算进去。比如一个 Java 模块堆内存 4G还要处理几十 G 的文件给 8G 限制就合适给了 6G 就会导致文件热数据存不住反而频繁读盘。K8s 场景同理Pod 的memory.limit包含了 pagecache所以给 Pod 设限时要同时估算文件读写的缓存需求。很多生产环境里 Pod 被 OOMKilled并不是应用堆内存真的超了而是 pagecache 把它挤到了 limit 边界内核在回收失败后发起了 OOM。4.3 应用层绕过或提示内核O_DIRECT、fadvise 和批量读取如果调内核参数和容器限制都不够就得回到应用层解决问题。pagecache 不是免费的对于大规模顺序扫描型任务完全没必要让内核缓存全量文件。先用O_DIRECT跳过 pagecache。比如数据库备份、批量导大数据、视频转码这类“一次性读取处理完就不再碰”的场景open()时加O_DIRECT让数据直接从磁盘到用户态缓冲区不污染、不依赖 pagecache。注意O_DIRECT对缓冲区对齐、块对齐有要求写业务代码时要处理align_alloc一类细节否则会遇到Invalid argument。对偶尔读一次的文件用posix_fadvise提示内核用完即弃posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);在读完之后提醒内核可以释放这部分页。要注意不能读一遍就DONTNEED一遍如果文件马上还要再读这个调用反而会让缓存命中失效。它适合“读一次就不再高频访问”的临时文件。对大文件做分批读取而不是整个读到内存。很多数据管道代码习惯一次性把文件全量载入内存比如file_read_all()这种封装在小文件上没问题在几十 G 文件上就是对 pagecache 的灾难。改成按记录流式读取pagecache 只会留存当前处理窗口的热数据不会被一整轮任务撑爆。4.4 从 IO 层面反向稀释回写影响pagecache 脏页回写的最终路径是块设备。如果磁盘本身慢或者 IO 调度器把回写请求和生产业务请求混在一起排长队前面所有参数调整都白搭。排查时用iostat -x 1 5重点看每个磁盘的avgqu-sz平均队列长度和%util。当回写任务堆积时你常会看到avgqu-sz长时间超过磁盘容量对应的合理值。比如普通 SATA 盘 16 队列你观察到avgqu-sz80那瓶颈就在磁盘本身不是在 pagecache 参数上。这种情况的应对思路是把回写量小的路径和回写量大的路径拆分到不同磁盘比如应用日志放一份盘、数据库数据放一份盘如果条件允许回写最猛的目录放到 SSD 上SSD 的随机写并发能力比机械盘强一个量级使用高频小文件写入时可以考虑在中间层加异步队列由少量线程集中批量写避免生产线程直接write()触发同步刷盘。注意IO 调度器参数scheduler在多数现代内核上建议保持none不要自己改成老旧的cfq或deadline改错反而会让回写请求在队列里绕圈。5. 实战复盘与避坑清单5.1 一个典型 pagecache 拖慢生产的实录模拟一下前阵子我处理过的场景节点代号叫“批量入库任务”。有个模块负责把上游消息落库并生成索引文件。某个版本上线后生产速度从稳定 1300 条/s 掉到 400 条/s监控里 CPU 平均才 30%看起来什么都很正常。我用free -h看到内存 32Gbuff/cache占了 23Gavailable只有 4GDirty持续在 6G 附近Writeback偶发 1Gvmstat的si/so有频率很低的换入换出sar -B里pgscand和pgsteal数值明显偏高。再往细看是模块先把上游数据暂存成几 GB 的临时文件然后循环读取这些文件做索引。文件读完后不删而是等任务结束才删。于是 pagecache 里全是这些临时文件的残留页。下一轮任务开始后旧缓存还没被回收彻底又要读新文件内存回收线程和业务线程互相抢资源。脏页回写又因为落盘速度跟不上把write()卡住。整个链路就变成了任务读文件慢、写文件也慢、内核回收还占 CPU。处理步骤很简单把dirty_background_ratio从默认 10 调到 3dirty_ratio从默认 20 调到 10给这个模块的 systemd service 增加MemoryMax8G限制容器内 pagecache 上限应用代码里对临时文件改用O_DIRECT读取并在读完一页后调用POSIX_FADV_DONTNEED临时文件按批处理窗口提前删除不在同一轮里占用大量 pagecache。三个小时后生产速度恢复到 1100 条/s虽然没有完全拉回 1300 条/s但稳定性明显好了没有出现断崖式掉速。核心收益是脏页不会再积压成同步阻塞pagecache 也不会无限膨胀到触发 kswapd。5.2 排查中常见的误判和正确解释这段过程里最容易出现的误判有几个。误判一buff/cache 高 内存泄漏。事实是 pagecache 本来就该占满空闲内存内核设计如此。某个进程狂读大文件导致 pagecache 升高不等于内存泄漏。真正要看的指标是Dirty和pgscan这一类回收计数而不是buff/cache的绝对值。误判二available 低 内存不够。available本身就包含可回收的 pagecache。如果业务进程没分配太多内存但是 pagecache 占了 90%available 低并不一定代表内存不够而是 pagecache 回收动作正在和业务线程打架。误判三清掉 pagecache 就能释放内存。不能。清掉之后下一轮任务会重新读磁盘整体延迟反而变高。除非你想验证冷启动性能否则别在生产环境乱echo 3 /proc/sys/vm/drop_caches。误判四调大dirty_ratio能提升写入速度。调大dirty_ratio只是允许攒更多脏页应用看起来写入快了但最终这些脏页都要落到磁盘。磁盘吞吐不变等于把“延迟”换成“最后一次性付账”。突发写入场景下调大dirty_ratio的结果往往是磁盘队列瞬间爆炸系统大面积卡顿。一张速查表方便复盘现象可能原因先看哪个指标wa高Dirty持续增长脏页回写跟不上Writeback、iostat队列si/so非零业务线程卡住冷 pagecache 匿名内存交换/proc/pressure/memorydrop_caches后反而变慢热数据丢失冷读惩罚/proc/vmstat的pgfault容器 OOM 但业务堆内存不高cgroup 内存限制吃进 pagecachememory.events5.3 可取方法把 pagecache 当资源而不是病排查完整个案例我最大的体会是pagecache 不是病把它当成病去治才会出问题。生产速度变慢绝对不只是“缓存占用高”这一个原因你要做的是把内核回收、脏页回写、进程 IO 路径、容器限制四件事放在同一张图画里看。先看/proc/pressure/memory确认有没有内存阻塞再看/proc/meminfo的Dirty和Writeback确认回写节奏最后看业务进程的读写模式确认是不是一次读入太多文件。对症之后再决定是调sysctl、设容器memory.max、改应用O_DIRECT还是拆磁盘。我个人实际用下来最推荐的组合是“容器限制 应用流式处理 dirty 参数微调”。它不需要牺牲业务正确性也不用盲目 drop cache。对于已经踩过坑、不想再来一轮的同学建议把cat /proc/pressure/memory和grep Dirty /proc/meminfo直接加进监控告警里生产速度掉的时候这两个指标能让你在五分钟内判断是不是 pagecache 的锅。