资讯动态

Linux Page Cache深度解析:从原理到运维调优实践

发布时间:2026/10/17 16:20:50 来源:尧图企业网站定制
先说一个前几天刚处理完的事。某业务在高峰期突然频繁告警登录服务器一看free -h里 available 内存几乎打光但检查所有进程的 RSS 加起来连一半都不到硬生生被整得像是内存泄漏。翻了半天才发现真正的主角是几 GB 的页缓存Page Cache——再加上那会儿业务正在大量随机读写文件数据在内存里反复换进换出系统 IO 压力直接翻倍应用卡顿自然跑不掉。这类场景对做运维和系统调优的人来说太常见了。Page Cache 是 Linux 内核里最基础也最容易被人误解的机制之一它既是提升文件读写性能的关键功臣又是明明有内存却显示不够用这种诡异现象的根源。很多刚接触的同学一看到free里 cached 几个 G 就开始紧张动不动就想着echo 3 /proc/sys/vm/drop_caches手工清缓存结果要么引发性能雪崩要么问题根本没解决。这篇文章我打算结合自己平时排查和调优的经验把 Page Cache 的原理、观察方法、常见误区和实操注意事项一次说清楚希望对做运维、后端开发和相关底层系统优化的朋友有实际帮助。1. 从故障场景切入Page Cache 到底是个什么东西1.1 为什么每次文件读写都绕不开 Page CachePage Cache 这个名字直译过来就是页缓存核心逻辑一句话就能讲明白把磁盘上的文件数据按内存页的大小预先缓存在物理内存里后续读写如果命中了缓存就直接操作内存而不必真的发起磁盘 IO。要理解它为什么重要得先看一组数量级的对比。普通机械磁盘每秒能提供的随机 IOPS 大概在一百到几百之间顺序吞吐量能跑到一两百 MB/s 就算不错哪怕是现代 NVMe 固态盘随机读 IOPS 通常也只能做到几十万。而内存的随机访问延迟是纳秒到微秒级别吞吐量动辄每秒几个 GB和磁盘相比差了至少两到三个数量级。操作系统作为资源的管理者必然要把这块高速缓存利用起来。所以内核会在第一次读取文件时把数据加载到内存页里之后如果再读同一个文件只要页面还没被回收就直接从内存返回完全不需要碰磁盘。在真实业务中这个机制带来的效果非常直观。比如一个静态资源服务图片和前端文件被大量重复访问只要热数据都留在 Page Cache 里即使后端是普通的 HDD压测时也能跑出接近内存带宽的速度。又比如数据库实例的 innodb_buffer_pool 没被命中时MySQL 底层读 InnoDB 文件走的也是操作系统的 Page Cache 这一层两层缓存叠加是极其常见的结构。这里有一个容易混淆的概念——早年间 Linux 是区分 Page Cache 和 Buffer Cache 的。前者缓存文件内容本身后者缓存的是块设备上的原始数据块。现代内核其实已经把它们都统一到同一个页面缓存框架里了只是free、/proc/meminfo这些工具里还保留了历史字段名。所以你在free里看到buff/cache这一列其实绝大多数都是 Page CacheBuffer Cache 那部分占比通常很小。1.2 Page Cache 解决的核心痛点和它的代价Page Cache 的引入不是没有代价的。它本质上是拿内存换 IO也就是说系统需要决定哪些内存应该给进程用哪些应该留给缓存。如果无限制地让文件数据占用内存一旦发生内存回收就可能出现一边缓存占着大量空间、一边运行中的进程被换出到 swap 的尴尬情况。这也是很多内存泄漏误判的来源。free里 cached 值很高并不等于内存耗尽available列才是真正反映还能安全分配给新程序的指标它算的是可回收的缓存 真正空闲的内存。理解这个细节之后很多故障排查思路会清晰很多。从代价的角度看Page Cache 主要带来三类问题脏页dirty pages需要定时写回磁盘如果写回不及时或者参数配置不当可能造成瞬间大量 IO 冲击。内核页回收是异步的但一旦触发同步回收进程就会被卡住等待表现为延迟抖动。部分运维同学不清楚drop_caches的适用场景盲目清缓存反而把热数据全部丢掉导致后续读写全落到磁盘上性能急剧下降。所以在实际运维中正确理解 Page Cache 机制不仅是为了解释现象更是为了避免错误操作。后面我就会讲清楚它的完整生命周期和每个阶段要注意的点。2. Page Cache 的完整生命周期分配、命中、回写与回收2.1 页面从磁盘走进内存的路径当进程发起一个 read 系统调用时内核会先把文件对应的逻辑页转换成物理内存页。如果目标数据已经在 Page Cache 里就直接把该页映射到用户空间完成拷贝返回如果没有就要发起磁盘 IO先把数据从磁盘读到内存页里然后再交给进程。这里面有一个很重要的附带机制叫预读read-ahead。机械硬盘最怕随机读顺序读则要快得多因此内核会在检测到进程按顺序读文件时主动多读一些后续的数据页进来缓存。这样连续流式读取时很多请求其实已经被内存接住了磁盘只需要一次较大块的数据传输。我之前排查过一个日志分析任务的 IO 问题把日志文件通过顺序扫描的方式读取时Page Cache 命中率从 30% 提到了 80% 以上磁盘等待时间明显下降。这个机制对大数据离线作业、日志处理这类顺序读场景尤其友好。页面进入 Page Cache 之后并非永远待在那里内核会把这些页挂进一个双向 LRU 链表Least Recently Used并且分为活跃链表和不活跃链表。新读入的页先进入不活跃链表的头部如果再次被访问就可能被提升到活跃链表活跃页被持续访问就不会轻易被内核回收。这种区分对待的设计是为了在内存紧张时优先牺牲冷数据保住真正高频使用的热页。2.2 脏页回写机制与关键参数读操作进缓存写操作同样会先进缓存。进程调用 write 时内核并不会立刻把数据刷入磁盘而是先把数据写入 Page Cache 中对应的页并将这一页标记为脏页。后续再由内核的工作线程在合适的时机统一写回磁盘。这种延迟写策略能极大减少磁盘繁琐的写入次数但要是一台服务器突然断电所有尚未写回的数据就会丢失所以说 Page Cache 是可靠性与性能折中的产物。脏页的写回主要由几个参数控制在/proc/sys/vm/下面可以实时查看参数默认值含义与影响dirty_background_ratio10脏页占系统内存总量的比例达到该值时后台内核线程开始异步回写进程不会被阻塞dirty_ratio20脏页占比达到该值时进程自身写入也会参与回写所有 write 系统调用可能被同步阻塞dirty_expire_centisecs3000脏页在内存中最长存活时间单位是百分之一秒默认 30 秒dirty_writeback_centisecs500内核回写线程周期性唤醒的间隔默认 5 秒理解这几个参数非常重要。比如默认 dirty_background_ratio 是 10%如果一台服务器物理内存是 128G那么脏页最多可以累积到 12.8G 之后才开始后台刷盘。虽然看似利用率很高但一旦触发大规模回写可能就是一瞬间把磁盘带宽打满产生 IO 尖峰。很多线上故障正是大内存 默认参数 大文件复制组合出来的结果。在调优时可选的思路有两个一是把触发比例调低让脏页更早更分散地写回比如dirty_background_ratio调到 5二是直接限定绝对值比如dirty_background_bytes设置为 1G这在内存差异很大的机器上更可控。对于写密集型的业务我还建议关注dirty_expire_centisecs如果数据可以接受小概率的丢失风险并且希望写性能更平滑也可按需把回写频率提高。2.3 回收机制与页缓存分发策略内存总是不够的内核必须有一套机制在内存紧张时回收 Page Cache。回收策略的核心依据是页面的活跃度和访问时间LRU 链表在这里发挥关键作用内核倾向于先从不活跃链表尾部回收最久没被访问的页如果回收的是干净页直接丢弃就行不会产生额外的磁盘 IO如果是脏页则要先写回再回收。另一个关键参数是 swappiness取值从 0 到 100控制内核在回收内存时是倾向回收文件页还是把匿名页换到 swap 分区。默认值是 60即内核会比较均衡地进行回收。对数据库这类应用如果触发大量 swap 会导致性能断崖式下跌所以很多优化实践会把 swappiness 调到 10 甚至 0以保证内存回收有限使用文件页缓存。但这里需要特别说明一个容易踩坑的地方调低 swappiness 并不意味着要么 swap 永远不被使用只要内存压力足够大内核仍然会将进程的匿名页换出。真正减少 swap 的方法是合理控制内存占用或者为关键进程配置 cgroup 内存限制。页面回收还有一个优先级问题。物理页被分为不同的回收区域DMA、Normal 和 HighMem在不同架构上有差异。在 64 位系统上这个层级带来的问题相对较少但在 32 位系统上HighMem 区域回收和映射开销是真实存在的优化难点。日常运维不需要到这么深的层次但在理解内核为什么会选择把某些进程换出而不是回收更多 Page Cache时知道区域划分和 swappiness 的存在会更容易看明白内核日志中的线索。3. 运维视角如何精准观察 Page Cache 的运行状态3.1 读懂 free、meminfo 与 vmstat别被 cached 吓到排查问题首先要会看数据。最常用的当然是free命令但要提醒大家的是只看第一行 total/used/free 很容易误判。我们来看一次真实的free -h输出$ free -h total used free shared buff/cache available Mem: 31Gi 2.1Gi 23Gi 27Mi 5.8Gi 28Gi Swap: 8.0Gi 0B 8.0Gi这里 used 只有 2.1Gfree 有 23Gbuff/cache 占用 5.8Gavailable 显示 28G。说明系统内存非常充裕。真正需要警惕的是另一种形态$ free -h total used free shared buff/cache available Mem: 31Gi 18Gi 1.2Gi 45Mi 12Gi 12Gi同样是 31G 总内存这里 free 只有 1.2G看起来快没了但 available 还有 12G。这 12G 其实就是 Page Cache 可以被回收的部分。如果只看 free 列就会得出内存不足的结论进而错误地开始清缓存或者盲目扩容。更详细的数据在/proc/meminfo里它记录了 Cached、Dirty、Writeback、AnonPages、Shmem 等信息。排队排查时可以这样组合观察CachedPage Cache 中缓存的文件页总量通常是最占空间的。Dirty等待写回的脏页数量。如果 Dirty 持续增长且 Writeback 也有值说明存在较大的刷盘压力。AnonPages进程匿名内存页也就是进程真正用到的堆和栈这部分基本不可回收除非换到 swap。Shmem共享内存和 tmpfs 占用的页也属于 Page Cache 的一部分但它不属于普通文件页。3.2 用 vmstat、sar 观测 IO 与缓存的联动很多人看 vmstat 只盯 CPU 那一列其实在排查 IO 时vmstat 里 procs 和 wa 都很有参考价值。vmstat 1输出的bo表示每秒块设备写入wa表示 CPU 等待 IO 的时间占比。如果出现bo瞬间飙到几十万且wa持续升高大概率是目录页回写引起的 IO 尖峰。sar 工具则可以从历史维度看问题。常用组合是sar -B查看页交换统计重点关注 pgpgin/pgpgout 和 pswpin/pswout。sar -d查看磁盘设备级的使用率、await、svctm。sar -r查看内存使用情况和缓存量变化。我曾经排查过一个间歇性慢查询问题单看一两个时刻的内存数据看不出端倪但把 sar 拉出来看一天的数据发现每次数据库定时全量扫描时Page Cache 命中率就会因为文件反复被读取而升高同时磁盘 await 也会出现规律性的峰值。顺着这条线才能定位到是表扫描策略和 Page Cache 回收策略之间的冲突。3.3 计算缓存命中率不能只靠感觉如果要量化业务对 Page Cache 的依赖程度需要算缓存命中率。命中率 (读请求次数 - 真正的磁盘读次数) / 读请求次数。但光靠 vmstat 的物质层面还不太够因为 pgpgin 和 pgpgout 是按页计数的增量无法直接区分哪些读是从缓存服务的。Linux 下的工具不少看实时缓存命中情况可以用 bcc 工具集里的cachestat和cachetop它们基于内核跟踪点统计了缓存命中和未命中次数。举个例子$ cachestat -1 HITS MISSES DIRTIES HITRATIO BUFFERS_MB CACHED_MB 10346 102 5430 99.02% 1256 18723可以明显看到如果 HITRATIO 长期在 95% 以下且 MISSES 数值很大说明热点数据没有在内存里驻留住要么内存偏小要么有进程在周期性刷缓存要么 LRU 回收得太激进。很多线上故障其实是命中率突然从 90% 跌到 40% 引发的。比如某天有人手动执行了drop_caches之后业务访问全部穿透到磁盘IO 延迟直接变成原来的十倍。所以运维规范里应当明确生产环境不允许随意清缓存除非有明确的性能和容量评估依据。4. 实操手册页缓存运维操作与参数调优4.1 到底能不能用 drop_caches什么场景才适合关于手工清缓存我一向建议非常谨慎。drop_caches是内核提供的一个调试接口不是日常运维工具它的完整路径在/proc/sys/vm/drop_caches支持三个值值作用1清除 Page Cache 中的干净页2清除目录项和 inode 缓存3清除上述两者注意脏页不会通过这个接口直接丢弃必须先写回或等待回写完成后才能回收。另外即使执行了它后续任何文件访问都会立刻让缓存重建——换句话说唯一的效果是让热缓存短暂失效白白增加一次冷读开销。什么场景下确实可以考虑用它一种是在某些压测场景里需要模拟完全冷启动的缓存状态另一种是在做缓存回填测试时需要保证进程确实从磁盘读取而不是吃到旧缓存。生产环境里除非你有充足的论证否则我不建议执行。我见过有监控脚本写死内存超过 80% 就清缓存的结果每次业务高峰期缓存的命中率都被强行打断IO 压力比原来没清时更严重。做一个负责任的操作判断时我推荐先看 available 而不是 free如果真的内存紧张也应该先去排查是否真的有进程内存泄漏、配置是否合理而不是把 Page Cache 当作替罪羊。4.2 脏页参数的配置实践压平 IO 尖峰如果你遇到写多读少、磁盘压力集中爆发的问题最直接的调优方向就是调整脏页回写参数。理论上说小内存机器和默认行为差异不大但大内存比如 64G 以上机器需要多留意。我常用的配置方案是sysctl -w vm.dirty_background_ratio5 sysctl -w vm.dirty_ratio10含义是脏页达到内存总量的 5% 时开始后台回写达到 10% 时阻塞写入。例如 64G 内存那么回写阈值分别约为 3.2G 和 6.4G。默认配置下则是 6.4G 和 12.8G。后者在一次性大量写入的场景下很容易导致刷盘风暴。如果机器内存差异较大或者业务希望更精确定义脏页上限下面两个参数更合适sysctl -w vm.dirty_background_bytes1073741824 sysctl -w vm.dirty_bytes2147483648这两个参数与上面的 ratio 参数同时存在时内核会忽略 ratio。设置为 1G 后台回写、2G 强制回写对于单机 IO 能力一般的环境比较安全。当然如果磁盘是高性能 SSD 阵列也可以适当放宽让数据在内存里多积攒一些减少磁盘写入次数。关于是否需要修改dirty_expire_centisecs默认 30 秒对大部分场景够用。但如果业务对数据丢失容忍度极低可以调小到 100即 1 秒代价是回写线程唤醒更频繁系统调用和上下文切换开销升高。这个参数没有绝对最优只有适合当前场景的值。4.3 swappiness 调优与容器场景下的特殊性swap 被很多人当成了万恶之源但事实上Linux 引入 swap 是为了在极端内存压力下能腾出空间保住进程运行。对于希望最大化利用 Page Cache 的业务适当调低 swappiness 是合理的sysctl -w vm.swappiness10这就意味着内核回收内存时更倾向于先回收文件页缓存而不是把匿名内存页换出。这样即使 available 显示下降进程依然能够稳定运行在内存。之后观察 swap 使用量和系统整体延迟就能判断配置是否合适。在容器containers或虚拟机场景下观察 Page Cache 会多一层次问题。宿主机上你会看到 cached 总量很大但单个容器内部看到的/proc/meminfo是 cgroup 层面隔离视窗不能直接反映整机情况。此时如果容器内的应用程序基于自身统计误判内存充足或不足都可能引发问题。排查这类问题时直接到宿主机上查看整个页缓存变化趋势才是更可靠的手段。4.4 内存和文件系统层面会互相影响的地方Page Cache 不仅服务于常规的文件 IO还与mmap方式映射的文件紧密相关。用 mmap 加载大对象很多服务都这么干文件被映射进进程地址空间后文件内容实际上也是通过 Page Cache 来缓存。这意味着进程访问 mmap 区域产生的页错误和回收行为都会被计到 Page Cache 的统计数据里。有时候我们看到 Cached 占用很高但du计算磁盘文件总大小却对不上就是因为同个文件可以被多个进程映射多次或者存在共享内存和 tmpfs 文件系统上的数据——它们的页面同样记录在 Cached 里但不会真正参与普通的文件磁盘占用。理解这一点对解释容量监控之间的偏差很有帮助。5. 问题排查实录与经典案例复盘5.1 案例一内存显示不足排查却发现实际很充裕现象某服务在 64G 内存的机器上频繁触发内存告警free 里的 free 列长期不足 1G监控平台按此触发通知。排查过程执行free -h总内存 64Gused 约 48Gbuff/cache 约 15Gavailable 在 14G 左右。进一步看/proc/meminfoCached 15GDirty 只有 20M说明大量缓存是干净页随时可以被回收。检查进程 RSS 总和大约 47G确实占了不少但还有 14G 可回收缓存兜底实际剩余 14G 可用于新进程并未到崩溃边缘。对照业务的内存占用曲线发现主要是某缓存组件的堆内存持续增长并非 Page Cache 的锅。最后结论监控平台只看 free 列是常见误报源。应该把监控指标改成 MemAvailable。内核对 available 的计算会考虑可回收 Page Cache 的数量这才更贴近业务能否继续分配内存的真实情况。5.2 案例二IO 突增与脏页回写风暴现象夜间批量任务启动后磁盘 util 瞬间打满应用写入延迟变大持续数分钟。排查过程vmstat 1看到 bo 数值在短时间内从几千跳到几十万。cat /proc/meminfo看到 Dirty 数值在任务启动后迅速增长接近触发阈值Writeback 长时间有值。查看 sysctl 配置dirty_background_ratio 还是默认的 10%机器内存 128G意味着要累积约 12.8G 脏页才开始后台回写。批量导入程序写得太快终于把脏页推到阈值附近结果一次性大规模刷盘。将 dirty_background_ratio 调为 3dirty_ratio 调为 6 后脏页回写更均匀地分散在任务执行过程中IO 尖峰明显变平缓。这个案例给我们的经验是大内存机器上使用比例参数要小心最好通过 dirty_background_bytes 限定一个与后端存储能力匹配的绝对值。5.3 案例三手动清缓存真的是元凶现象某文件服务突然响应变慢磁盘 IO 居高不下页面访问延迟翻倍。排查过程看磁盘 IO 使用率 100%但 CPU 的 iowait 非常高。翻 sar 历史记录发现某一时刻 Cached 值从 20G 骤降到 500M然后开始缓慢爬升。查操作记录发现有人执行了sync echo 3 /proc/sys/vm/drop_caches目的释放内存。热数据被丢掉所有请求都打到磁盘上IO 自然瞬间爆掉。确认没有内存压力后禁用了该操作让服务依靠正常 LRU 机制自发热更新缓存约半小时后各项指标恢复。这个案例说明生产环境的运维操作一定要有明确目的和流程约束像清缓存这样的破坏性调试命令应该从日常操作规范里剔除。5.4 排查方法思维导图式总结真实排查时我的惯用顺序是先看 available 内存是否真的不足区分缓存占用与内存耗尽。再观察 Cached 和 Dirty 的趋势判断是否有回写压力。然后看具体设备 IO 的 util 和 await判断瓶颈是否在磁盘。最后结合进程上下文和内核日志确认是否有 page allocation 失败、直接回收等事件。动态调整参数后持续观察至少几个业务周期而不是改完一拍脑袋就收工。这个顺序能避免大量的上来就清缓存式误操作。6. 不同业务场景下的页缓存调优实践总结6.1 数据库服务给 buffer pool 和 Page Cache 合理分工数据库类服务比如 MySQL、PostgreSQL对延迟极其敏感且自身通常会维护一个比较大的缓冲池。很多人误以为直接关闭操作系统的 Page Cache 就能让 buffer pool 完全接管文件 IO但实际上数据库的日志文件、临时文件、系统表空间等都会经过文件系统层。在数据库主机上核心目标不是消灭 Page Cache而是确保内存不至于被缓存的非热点页面挤占。实践中我会调整 swappiness 到 10 左右尽量避免匿名页被换出。控制脏页回写参数避免日志刷盘和脏页回写撞在一起产生 IO 尖峰。为数据库实例所在目录设置更合理的 IO 调度策略比如使用 noop 或 none 调度器配合 NVMe。监控 available若长期偏低优先看 buffer pool 是否配置过大而不是尝试清缓存。6.2 静态文件与 CDN 源站缓存命中率就是生命线对图片、视频、静态文件服务来说Page Cache 直接决定回源压力。这类场景收益最高的手段往往是尽可能让热文件长期留在缓存里。不要随意 drop_caches。避免在同一台机器上跑大量内存型业务导致 Page Cache 频繁被回收。可以针对大文件开启顺序预读支持让顺序读取的命中率更高。如果缓存命中率长期无法提升优先考虑升级内存规格或者引入更上层的 CDN 缓存而不是调内核参数。6.3 大数据任务IO 平滑比瞬时速度更重要数据仓库跑批、日志清洗、离线模型训练这类任务经常涉及庞大的顺序读和写中间结果。Page Cache 的预读机制能有效降低磁盘访问次数但中途生成的大量临时文件如果全是脏页也会带来不小的写回压力。写密集型跑批任务开始前可以考虑临时调整脏页参数更保守一些或者确保中间数据落盘路径有足够带宽。任务结束后再恢复默认避免影响其他实时业务。这种做法看上去简单但实战中很省钱不需要硬件升级就能显著减少 IO 抖动。6.4 详细参数调整速查哪些该动哪些别碰我把平时大部分场景会用到的参数调整整理成一个速查表方便拿过去对照操作调整目的参数推荐动作注意事项减少 swap 对性能的影响vm.swappiness设 10 或更低不能完全禁用 swap极端情况下换了总比 OOM 好平滑脏页回写 IOvm.dirty_background_bytes按磁盘能力设置 1G~4G与 ratio 参数互斥设置时注意顺序限制脏页堆积上限vm.dirty_ratio保持 10 以下设置太低会频繁阻塞写入进程缩短脏页存活时间vm.dirty_expire_centisecs300~3000 之间越小越安全但上下文开销越高调试用缓存清理vm.drop_caches非必要不生产使用会清掉热缓存导致性能瞬间下降这些参数修改最好统一通过/etc/sysctl.conf固化而不是只在当前环境中临时执行不然服务器一重启就全部回到默认值。7. 从实践中来几个值得长期坚持的运维习惯Page Cache 机制讲再多落到日常运维上其实就是几个习惯问题。我在这块摸爬滚打一段时间后总结了几条自己一直坚持的做法。最重要的一条是改参数前先量化现状。无论是调 swappiness 还是 dirty 参数我都会先用 sar、vmstat、free 等工具收集至少一周的基线数据搞清楚当前的 IO 峰值、内存分配、缓存命中率等到底是怎样。没有基线判断的调优最终只能靠运气。第二条是监控指标不能只盯着某一项。CPU、内存、IO、缓存这几个指标是相互关联的出现异常时往往不是某一个单一问题而是链路中的一个环节失衡。比如磁盘 util 高可能要回到脏页回写和缓存命中率去解释。第三条是在测试环境里复现和验证再上生产。不只是新版本部署要这样做内核参数调整也要这样。我见过有人直接把dirty_ratio调到 1结果写性能掉了 30%就是因为没有做实验就盲目配置。另外在系统层面之外也要留个心眼很多容器编排平台和监控工具自带自动清理或自动扩缩容策略如果这些策略在没有感知 Page Cache 的情况下误判内存不足就可能触发频繁迁移和重启。检查自动化策略时记得把 MemAvailable 而不是 MemFree 作为触发条件。最后关于 Page Cache 的运维知识我认为不需要死记硬背内核源码里的细节但一定要理解读写走内存、回写落磁盘、LRU 定生死这三件事。掌握住这个主线绝大多数实际使用中遇到的问题都能拆解出合理的排查路径。这也是一直以来我处理各种缓存问题的底层思路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑