☰
生产环境变慢元凶:pagecache内核机制排查与调优
2026/10/14 3:48:58 网站建设 项目流程

生产环境半夜突然接到告警,接口响应时间从几十毫秒涨到两三秒,数据库连接池被打满,但数据库本身慢查询并不多。看了半天CPU、内存、磁盘都没异常,最后发现真正的瓶颈竟然是一个平时默认配置、很少有人会去看的内核机制——pagecache。

这个场景,很多做后端和运维的同学应该都遇到过。pagecache本来是Linux用来加速文件读写的,结果在某些业务形态下,它反而成了生产速度变慢的元凶。这篇文章我想把这类问题的排查思路和解决方案完整梳理一遍,包括底层原理、定位方法、参数调优和一坑一坑的经验,给同样被这种诡异性能问题折磨过的朋友一个可以照抄的作业。

1. pagecache拖慢生产,底层原因到底是什么

很多人在碰到性能问题时,会先怀疑慢查询、代码逻辑、数据库连接池,很少有人会想到pagecache。这不怪大家,因为pagecache的名声是"加速器"——文件读了之后缓存在内存里,下次再读就直接命中,多快啊。问题在于,任何没有边界意识的缓存,在特定压力下都有可能变成瓶颈。

1.1 先给pagecache一个准确的"人设"

pagecache是Linux内核的页缓存,核心作用是把磁盘上的文件内容按页(通常是4KB)缓存到内存中。读文件时如果命中缓存,就直接从内存拿数据;写文件时也不是立刻刷到磁盘,而是先写进内存标记为脏页(dirty page),后续再异步回写到磁盘。这种设计大幅降低了磁盘I/O的次数,正常情况下对性能提升非常明显。

但这里有个关键点:pagecache占用的内存,在系统内存紧张时可以回收。也就是说,它本质上是一块"可以被牺牲的缓存"。生产环境里,数据库、应用进程、中间件都需要内存,当应用申请内存时,内核会优先回收pagecache来满足,这个回收过程如果是顺畅的,倒还好;如果pagecache太脏、回收太慢、或者回收后又立刻被大量读操作填满,就很容易出现性能抖动。

我更喜欢把pagecache理解成一个"公共仓库"。仓库里堆满了别人寄存的货,你需要地方放自己的货时,管理员会先清理仓库。清理速度快,一切好说;但如果你每放一次货,仓库就要翻一遍底朝天,性能自然就崩了。生产环境遇到的现象,往往是这个"翻仓库"的过程。

1.2 缓存呆得越久,问题就藏得越深

pagecache导致生产变慢,背后通常隐藏着几个不同的机制,它们的表现很相似——服务变慢、I/O升高、响应超时,但根源完全不同。我梳理下来,最常见的有三种慢路径:

第一种是脏页回写阻塞。业务写入量很大时,内存中的脏页越来越多,一旦到达dirty_ratio阈值,内核会强制同步刷盘,此时所有写操作都要等待刷盘完成。表现就是磁盘写等待(iowait)飙升,明明磁盘利用率不高,但写入就是卡顿。

第二种是内存回收带来的连锁反应。应用进程申请内存时发现pagecache占满了内存,内核触发回收,回收过程中会伴随大量内存页的换入换出和内核的扫描开销。如果回收操作很频繁且占用CPU时间,就会看到系统态CPU(sys CPU)升高,应用线程被调度延迟拉高。

第三种是cache颠簸(thrashing)。有些业务是海量文件读取,比如大数据导出、日志分析,一个文件几十GB,读一次就把整个pagecache冲掉了,真正的热数据反而被逐出内存。业务每次访问热数据都触发缺页中断,然后重新从磁盘读,整体速度可想而知。

1.3 补充一个容易忽略的细节:major fault

我一直强调,判断性能问题时要区分两类缺页中断:minor fault和major fault。minor fault是说页表里没有映射,但物理内存里有数据,很快就能处理;major fault则是数据不在内存中,必须发起磁盘I/O。如果系统观测到major fault突然变多,往往就是pagecache没承接住访问,数据被迫从磁盘加载。

举个例子,某服务的Java进程启动后,代码扫描了大量配置目录,之后配置文件的pagecache被其他大数据任务冲掉了,结果每次请求都要触发major fault去磁盘读配置。这个损耗在单次调用里可能只有几毫秒,但叠加到高并发下,接口RT就上去了。这类问题如果不看major fault指标,很容易误判到代码层。

2. 三步定位:从"服务慢"到"实锤pagecache"

排查这类问题,最忌讳的是直接去调系统参数。你不先搞清楚pagecache在真实链路里扮演什么角色,乱调参数大概率会把问题搞得更大。我习惯用一套三步法:先看用户态现象,再找内核态证据,最后做根因区分。这套方法在很多生产事故里都帮我快速缩小了范围。

2.1 第一步:用户态现象和数据流异常

当你接到"生产变慢"的反馈时,先把现象分类。系统慢分成三种典型形态:CPU高、磁盘I/O高、内存压力大。pagecache引发的变慢通常能同时踩中两个以上,但各有侧重:

  • 如果是脏页写回占主导,你会看到iowait明显升高,磁盘util可能并不高,但读写请求的平均服务时间拉长。你用iostat -x 1观察,await和svctm会异常偏高。
  • 如果是内存回收占主导,你会看到系统态CPU升高,top里%sys经常超过%usr,同时伴随kswapd进程活跃。
  • 如果是cache颠簸占主导,服务整体吞吐下降,但CPU和磁盘看起来都"还好",只是响应时延悄悄变长。

然后是数据流异常。无论是哪种形态,生产变慢时通常伴随着连接数、线程数、队列长度的上涨。比如Tomcat线程池堆积、数据库连接池打满、消息消费延迟增大。这些现象只能说明"出口堵了",还定位不到是pagecache干的,所以必须往下走。

2.2 第二步:内核态证据链

要在Linux上确认pagecache是否出了问题,我有几个习惯性的命令组合,建议直接收藏:

# 查看内存分布和脏页情况 free -h cat /proc/meminfo | grep -E "^MemFree|^MemAvailable|^Buffers|^Cached|^Dirty|^Writeback|^WritebackTmp" # 查看内存回收相关的统计 cat /proc/vmstat | grep -E "pgpgout|pgscand|pgsteal|pgmajfault" # 实时观察CPU的sys占比以及kswapd等内核线程 top -b -n 1 | grep -E "kswapd|kworker|%Cpu" vmstat 1 10

这些输出怎么解读?我用一个排查场景举例。某系统在做数据批量导入时变慢,free -h显示buff/cache接近128GB内存的80%以上,/proc/meminfo里Dirty徘徊在6~8GB,Writeback持续非零,同时vmstat的bo(块设备写出)很高。基本就可以初步判定:脏页回写压力大,系统在持续刷盘,写入业务被压制。

另一个场景是应用进程周期性卡顿。此时/proc/meminfo里的Cached很高,但Dirty并不高,反而是pgscand(内存扫描数)和pgsteal(回收页数)很高,说明内核一直在回收pagecache来满足内存申请。回收过程有锁竞争,应用线程会不时被阻塞,明显感觉到间歇性卡顿。

vmstat输出里还需要重点关注cs(上下文切换)和wa(I/O等待)。pagecache问题引发的上下文切换和I/O等待往往同步上涨,这是一个很典型的组合信号。

2.3 第三步:区分pagecache问题与普通磁盘I/O问题

这一步最关键。有些团队看到iowait高就直接认为磁盘坏了,或者认为是业务SQL有问题,忽略了pagecache这层。我常用的区分方法是查看pgmajfault。

cat /proc/vmstat | grep pgmajfault

在问题发生的时间窗口内连续记录几次。如果pgmajfault数值持续快速增大,说明大量内存页需要从磁盘重新加载——这通常指向缓存失效、cache颠簸或者内存回收后重新填充的恶性循环。而如果pgmajfault增长平稳,但磁盘I/O持续打满,那更像磁盘自身或文件层的问题。

另外一个很实用的操作是观察kworker线程数量。如果问题期间有大量kworker在跑回写任务,说明内核在忙着把脏页刷到磁盘,此时你需要进一步查看哪些进程产生了大量文件写入,而不是急着给磁盘扣帽子。

3. 三种典型根因场景与动手解决

定位到pagecache之后,真正动手解决之前,我还要把场景分了类。因为同样是"pagecache导致生产变慢",读密集和写密集的解法方向完全相反,参数调整方向也完全不同。如果搞反了,会直接雪上加霜。

3.1 批量读把pagecache搅浑了

这个场景我遇到最多的,是数据导出、备份扫描、日志检索这类一次性的大文件读取。程序一次性把几十GB的文件读进pagecache,热数据缓存被冲刷,后续正常业务的cache命中率暴跌。症状是偶发性的RT尖刺,持续一段时间后自己好转,但高峰期一开会就循环出现。

解决思路不是去调大pagecache,而是让大文件读取不要污染缓存。Linux提供了posix_fadvise,可以在读取文件时明确告诉内核:"这段数据我用完就不需要了,别让它占着缓存"。

// C语言参考代码 #include <fcntl.h> int fd = open("/data/export.log", O_RDONLY); // 通知内核:顺序读取时,不用积极缓存已读过的页面 posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);

如果不想改代码,有些工具本身支持类似能力。比如用dd做拷贝时指定iflag=nocache,在读取时避免填充pagecache;或者在应用层直接把读取方式切换为O_DIRECT,绕过页缓存走直接I/O。注意,O_DIRECT适合顺序大块读取,不适合小随机读。

还有一个实用技巧:如果业务允许,把大数据导入安排在低峰期,并在任务启动前把内核参数vm.drop_caches临时置为允许清理(配合sync),任务结束后恢复。这个操作虽然有争议,但比我下面要讲的"无脑清缓存"要安全得多。

3.2 脏页写回卡住了整个IO链路

写密集场景下,问题焦点集中在Dirty页和回写机制。内核默认的vm.dirty_ratio是20%,vm.dirty_background_ratio是10%。这是什么意思?当脏页占物理内存的比例达到10%,内核开始后台异步刷盘;达到20%,所有写操作都要阻塞,强制同步刷盘。

想象一下:128GB内存的机器,默认配置下脏页可以堆积到20多GB才触发强制刷盘。一旦触发,系统瞬间把20多GB数据写回磁盘,磁盘瞬间被打满,业务写入卡死。这种"憋大招式"的刷盘行为是生产环境大忌。

解决方向是让回写均匀化。我的做法是调低后台回写阈值、缩短脏页存活时间:

# 临时调整 sysctl -w vm.dirty_background_ratio=5 sysctl -w vm.dirty_ratio=10 sysctl -w vm.dirty_expire_centisecs=3000 sysctl -w vm.dirty_writeback_centisecs=500

dirty_expire_centisecs默认3000(单位是百分之一秒),也就是脏页最迟30秒必须回写。对于写密集型业务,可以适当调低到2000左右,让数据更快落盘,避免长时间积累。但也要注意,调得太低会导致回写频率变高,磁盘持续忙碌。这个参数需要在磁盘吞吐capacity和业务延迟之间做权衡。

配套的还有存储层优化。例如日志类写入,先写内存循环缓冲区再批量刷盘,减少小文件碎片落盘;数据库类写入,关注doublewrite和fsync策略,避免每个事务都触发内核回写。

3.3 内存回收把应用进程"误伤"了

第三种场景最隐蔽。前两种都能看到I/O异常,这一种有时候看不出磁盘忙,但服务就是慢。根源往往在于:pagecache占据了大量内存,应用进程在分配内存时,内核必须先回收pagecache,而回收过程不只是"丢弃干净页"这么简单。

内核回收pagecache时,会扫描LRU链表,遇到干净的页可以直接释放,但遇到脏页还要先回写。更麻烦的是,回收过程要持有内存管理的锁,高并发下线程之间互相等待锁,会造成明显的线程调度延迟。再加上kswapd内核线程大量占用CPU,应用线程实际能获得的CPU时间片就减少了。

这种场景下,问题的根源是"pagecache占用没有边界"。常规解法是调整vm.vfs_cache_pressure和vm.swappiness:

sysctl -w vm.vfs_cache_pressure=50 sysctl -w vm.swappiness=10

vfs_cache_pressure默认100,表示内核在内存压力下对inode/dentry等元数据缓存的回收倾向。把它调低到50,可以降低元数据缓存被回收的概率,减少路径解析和文件打开时的I/O。swappiness默认60,代表内核使用交换空间的倾向,调低到10能让内核尽可能留在物理内存,减少swap抖动。

不过我也要特别提醒:这两个参数不是万能药。如果应用进程本身的内存需求就很高,回收pagecache是必然的,再怎么调参数也只是延缓冲突。根本解法是限制留给pagecache的可用空间,或者给关键应用配上cgroup内存限额,避免业务进程和文件缓存无限制争夺资源。

4. 参数调优与落地配置:把pagecache管起来

如果踩过几次坑你会发现,Linux默认内核参数更偏向"通用场景",并不适合所有业务。把pagecache管起来,本质上是要回答三个问题:脏页最多能积累多少、什么时候开始刷、内存回收的优先级怎么排。下面这几个参数组成了完整的控制面。

4.1 必须搞懂的六个内核参数

我把和pagecache强相关的内核参数整理成一张表。表格里的排序也是排查时的优先级顺序。

参数默认值作用调整方向
vm.dirty_background_ratio10脏页占内存比例达到该值时,后台开始异步刷盘写密集建议调低至3~5
vm.dirty_ratio20脏页占内存比例达到该值时,写操作强制同步刷盘写密集建议调低至8~10
vm.dirty_expire_centisecs3000脏页最久存活时间(百分之一秒),超过后必须回写攒批和实时性之间的权衡
vm.dirty_writeback_centisecs500内核后台回写线程唤醒间隔(百分之一秒)默认即可,也可适当调低
vm.vfs_cache_pressure100内核回收元数据缓存的倾向值读密集可降低到50
vm.swappiness60内核使用swap空间的倾向内存充足时调低到10

调整dirty_ratio和dirty_background_ratio之前,先通读一下业务写入特征。如果是短时间高吞吐写入,比如数据同步、批量导入,那么调低阈值会让刷盘压力更分散,整体更平滑;如果是持续低水平小写入,比如支付回调、消息落库,默认参数其实已经够用,乱调整反而增加磁盘磨损。

dirty_expire_centisecs是我非常在意的一个参数。太多人只调dirty_ratio,忽略了脏页还有一个"存活时限"。就算阈值没到,脏页在内存里待了30秒也必须回写。对于要求数据尽快落盘、减少崩溃损失的业务,我会把dirty_expire_centisecs调低到1500~2000,也就是15~20秒。

4.2 一台128GB内存机器上的参考配置

我给一个自己在常见业务服务器上使用过的调优模板,内存为128GB的物理机,业务混合读写、重点是延迟敏感型:

# /etc/sysctl.d/99-pagecache-tuning.conf # 脏页后台回写阈值:约6.4GB vm.dirty_background_bytes = 6871947674 # 脏页强制回写阈值:约12.8GB vm.dirty_bytes = 13743895344 # 脏页最长存活时间:20秒 vm.dirty_expire_centisecs = 2000 # 回写线程唤醒间隔:5秒 vm.dirty_writeback_centisecs = 500 # 元数据缓存回收倾向降低 vm.vfs_cache_pressure = 50 # 减少swap使用 vm.swappiness = 10

为什么用dirty_bytes和dirty_background_bytes而不是_ratio?因为ratio是百分比,会随内存大小线性放大到很离谱的量级;用bytes可以精确锁定一个绝对值。上面配置对应的就是后台6.4GB开始刷盘,强制12.8GB必须刷盘。这个量级对于32~128GB内存的机器都算稳妥,你可以按比例换算。

需要强调的是,这套配置只适合"延迟敏感型"和"混合读写"场景。如果是日志型的纯写服务器,我会把后台阈值再压到3~4GB;如果是海量读的缓存型服务器,重点反而是vfs_cache_pressure,而dirty_ratio可以维持默认。

4.3 调优后怎么验证效果

参数改完之后,别急着宣布修复。我会用一套固定流程验证:

先用vmstat 1 10观察bo和wa是否变得平滑,再对比调优前后同一时段内的/proc/vmstat里的pgscand和pgsteal增量。如果数值明显下降,说明内存回收压力减弱了。同时,用业务侧的请求成功率、RT的P99来确认收益。

# 同时抓取CPU、内存、I/O、上下文切换 vmstat 1 20 > /tmp/vm_before.txt # 调优后再次执行,对比两个文件 vmstat 1 20 > /tmp/vm_after.txt

对比时,我重点关注两点:一是wa列是否明显下降,二是cs列是否稳定。另外还要多盯一会儿Dirty和Writeback,确保新配置没有导致这两个值在低水位和高水位之间大幅震荡。

这里有一个常见误区:改完参数后看free -h发现buff/cache还是很高,于是觉得"没用"。实际上pagecache占用的物理内存很高并不是问题,只要它不会在关键时刻触发强制回收和强制刷盘,整个系统就能稳定运行。内存闲着也是闲着,给文件缓存用掉远比空置浪费要好。

5. 常见问题与避坑经验实录

这一节,我把日常工作中常被同事问到的问题和踩过的坑集中写出来。它们不是教科书上的概念,而是真实存在的生产教训。

5.1 "echo 3 > drop_caches"为什么不能随便用

网上很多性能调优文章会把echo 3 > /proc/sys/vm/drop_caches当作大招推荐。我要说清楚:这招只是清理pagecache和inode/dentry缓存,不是根治性能问题的手段,而且代价很大。

首先,drop_caches只能清理干净页。如果有大量脏页,就算你执行sync后清理,也会在清完后立刻重新缓存热数据;如果不清脏页直接drop,脏页回写还是照常执行。其次,清理缓存会瞬间抹掉所有热数据,紧接着的访问全部变成缓存未命中,大量major fault,性能会经历一个"先更慢"再慢慢恢复的过程。

在业务高峰期,任何形式的drop_caches都等于自己打断自己的腿。我见过有人半小时执行一次drop_caches,结果就是缓存永远热不起来,每次请求都走磁盘。真正需要腾出内存时,更合理的做法是重启资源占用的任务,或者调整cgroup限制,而不是强行清内核缓存。

提示:drop_caches只适合低峰期排查问题、测量冷启动性能,或者释放被脏数据进程占据的内存时使用。

5.2 看了Dirty只有几百兆,为什么还是慢

这是排查时最容易被误导的一点。/proc/meminfo里的Dirty只代表"尚未回写的脏页",如果只有几百MB,确实说明内核回写压力很小。但变慢依然可能是pagecache引起的,问题出在别的地方。

可能的情况是:Cached非常高,但MemAvailable很低。此时新进程申请内存,内核必须回收pagecache来凑内存。回收过程产生大量pgscand和pgsteal,虽然脏页不多、回写压力不大,但锁竞争和内存扫描还是会造成性能下滑。Dirty低只说明"没人刷盘",不代表"缓存没有占用内存"。

还有一种情况是pagecache命中率本身已经很低。大量随机小文件读取时,缓存命不中,每次都要major fault,这种现象看Dirty没意义,要盯pgmajfault和实际应用的读时延。记住,pagecache问题不等于脏页问题,这是两码事。

5.3 监控与告警:至少别漏掉这几个指标

经历过几次事后,我把pagecache相关的监控指标固定在了告警系统里,养成了习惯。如果你也想提前发现这类隐患,至少加上这几个监控项:

  • 主机内存的Dirty和Writeback绝对值,以及Dirty占总内存的比例。
  • /proc/vmstat中的pgmajfault变化率,超过基线值得注意。
  • vmstat输出中的wa和cs,超过基线持续5分钟以上触发告警。
  • kswapd进程CPU使用率,出现持续异常升高说明内存回收压力大。

监控有了,还要配合定期巡检。比如每两周检查一次Dirty的历史峰值,如果峰值一直逼近阈值,说明业务量在增长,参数可能需要重新评估。这种主动维护的意识,比等告警响了再排查要舒服得多。

写在最后的一点经验

处理这类pagecache引发的生产变慢问题,我最大的体会是:先把机制弄明白,再动手改参数。同一个表象"慢",背后的机制可能是脏页回写、内存回收、cache颠簸这些完全不同的东西,治法和参数方向也完全不同。Linux提供这么多调控参数,不是为了让人盲调,而是给人足够的工具去适配业务。每次配置变更,都要有前后对比、有验证、有回滚方案,这才是一个合格的线上变更流程。

最后再分享一个小技巧:如果你不想每次排查都翻命令,可以提前写一个性能体检脚本,把free -h、cat /proc/meminfo、vmstat、cat /proc/vmstat | grep pgmajfault这几条命令一次性收集好输出到文件里。出问题的时候,这些历史数据能帮你快速对比出异常点,节省大量救火时间。数据先于结论,排查自然有底气。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询