Page Cache 管理不当导致的 load 飙高——内存回收篇
本文是线上问题实战录系列的第 8 篇 叙事框架:
现象 → 排查过程 → 根因 → 修复 → 预防
问题现象
2026 年 6 月 17 日下午 2 点 15 分,监控告警同时触发了四条规则:
- Load 飙高:CMS 图片处理节点
cms-prod-01load average 达到 15.8(阈值 5.0) - 内存不足:可用内存仅 2.3GB(可用率 7.2%)
- IO 饱和:磁盘 sda 使用率 94.5%
- Swap 使用率高:Swap 已使用 6.1GB/8GB(74%)
但一个令人困惑的现象是:CPU 并不忙。top 显示 CPU idle 仍有 73%,us 只有 8.5%。

排查过程
第一步:观察 load 与 CPU 的背离
登录生产服务器,top 显示了一个典型的「CPU 不忙但 load 高」的状态:
top - 14:25:00 up 12 days, 3:15, 3 users, load average: 15.8, 12.3, 8.1
Tasks: 312 total, 3 running, 307 sleeping, 2 stopped, 0 zombie
%Cpu(s): 8.5 us, 5.2 sy, 0.0 ni, 73.1 id, 12.4 wa, 0.3 hi, 0.5 si, 0.0 st
CPU idle 73%,但 load average 15.8——说明大量进程处于 不可中断睡眠(D 状态),而不是在消耗 CPU。

更关键的线索是进程列表中出现了 kswapd0、kswapd1 和 kcompactd0——内核在拼命回收内存。
第二步:vmstat 确认 D 状态进程和内存回收
vmstat 2 6
r b swpd free buff cache si so bi bo in cs us sy id wa st
3 18 6057 412 1024 12408 0 45 12 48 412 892 10 5 72 13 0
2 21 6089 386 1024 12456 0 52 1024 2048 567 1234 8 6 65 21 0
- b 列 18-23:大量进程在等待 IO(D 状态)
- cache 列 持续增长:Page Cache 在不断吞食内存
- free 仅 412MB:32GB 内存几乎耗尽
- so(swap out)约 50/秒:系统在换出页面

第三步:sar -B 量化内存回收压力
sar -B 2 5
14:25:00 pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
14:25:02 1024.5 2048.0 24567.8 12.5 31245.6 289.5 34.2 256.8 79.3
14:25:04 1152.0 3072.0 28912.3 15.2 35678.9 345.6 42.1 312.5 80.6
关键指标:
- pgscank/s 300+:kswapd 后台扫描的页面数,说明后台回收在全力工作
- pgscand/s 30+:直接回收(direct reclaim)的页面数——进程在内存分配时被阻塞等待回收
- majflt/s ~13:大量 major page fault,进程需要从磁盘读回换出的页面
- %vmeff ~80%:回收效率尚可,但扫描量太高达不到平衡

第四步:查看 /proc/meminfo 确认 Page Cache 状态
cat /proc/meminfo
MemTotal: 32803572 kB
MemFree: 386224 kB
Cached: 12890124 kB
Active(file): 7555566 kB
Inactive(file): 5000000 kB
Dirty: 234567 kB
Writeback: 12345 kB
SwapTotal: 8388608 kB
SwapFree: 2185808 kB
- Cached 12.9GB + Active(file) 7.5GB:Page Cache 占了几乎所有剩余内存
- Dirty 234MB:大量脏页待回刷
- Swap 已用 6.1GB:物理内存不足,大量匿名页被换出

第五步:iostat 确认 IO 瓶颈
iostat -x 2 5
Device r/s w/s rkB/s wkB/s await r_await w_await svctm %util
sda 768.0 896.0 12288.0 14336.0 52.4 38.7 58.2 1.24 91.8
%util 91.8%,await 52ms——磁盘已经饱和。注意这里的 IO 不只是业务文件的读写,还包括 kswapd 换入换出产生的 IO。

第六步:perf top 确认热点在内核内存回收
sudo perf top -K -g --sort=comm -n 10
Overhead Shared Object Symbol
12.45% [kernel] [k] shrink_page_list
8.23% [kernel] [k] folio_referenced
7.56% [kernel] [k] page_cache_ra_unbounded
6.89% [kernel] [k] do_read_cache_folio
6.12% [kernel] [k] folio_mark_accessed
5.78% [kernel] [k] kswapd
5.34% [kernel] [k] try_to_free_pages
- shrink_page_list 12.45%:内核回收页面的核心函数,说明系统在全力回收内存
- page_cache_ra_unbounded + do_read_cache_folio + filemap_read:Page Cache 读取路径的热点
- kswapd + try_to_free_pages:内存回收线程在工作
这是最直接的证据——CPU 时间主要消耗在内存回收路径上,而不是业务代码。

第七步:检查脏页配置
sysctl vm.dirty_ratio vm.dirty_background_ratio
vm.dirty_ratio = 30
vm.dirty_background_ratio = 10
dirty_ratio = 30%:进程可以产生脏页直到占满 30% 的内存(约 9.6GB),然后才被阻塞等待回刷。对于大文件写入场景,这会积累大量脏页,回刷时产生巨大的 IO 尖刺。

根因分析
问题链路
图片批量读取/写入
→ Page Cache 膨胀(12.9GB)
→ 内存不足(free 仅 400MB)
→ kswapd 启动回收
→ 回收产生 IO(换入换出 + 脏页回刷)
→ IO 阻塞进程(b 列 20+)
→ load 飙高(15.8)
→ 可用内存不足导致更多直接回收
→ 恶性循环
为什么 CPU idle 还有 73% 但 load 很高?
这是理解 Page Cache 问题的关键。load average 统计的是 R(运行)+ D(不可中断睡眠) 状态的进程数。
当大量进程在等待磁盘 IO 时,它们处于 D 状态,不消耗 CPU 但会计入 load。所以出现「CPU 不忙、load 很高」的现象时,要立即想到 IO 阻塞 或 内存回收。
为什么测试没发现?
- 测试环境文件数少(几十张),内存充足,Page Cache 占不满
- 测试时没有配合高并发用户请求,匿名页和文件页之间没有竞争
- 业务代码上线前只做了功能测试,没有做 IO profile 和内存压力测试
修复方案
两个方向同时进行:
方向一:代码层面——主动释放 Page Cache
// 修复前:批量读取不释放缓存
for (Path source : sourceImages) {
byte[] imageData = Files.readAllBytes(source);
// Page Cache 持续增长,永不释放
byte[] resized = resizeImage(imageData, 1920, 1080);
Files.write(outputDir.resolve(source.getFileName()), resized);
}
// 修复后:分批次处理 + MappedByteBuffer unmap 释放 Page Cache
private static final int MAX_BATCH_SIZE = 50;
private void dropPageCache(List<Path> files) throws IOException {
for (Path file : files) {
try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ)) {
ch.map(MapMode.READ_ONLY, 0, ch.size());
// unmap -> 内核收到 MADV_DONTNEED -> 释放对应 Page Cache
}
}
}

方向二:内核参数调优

# /etc/sysctl.d/99-cms.conf
# 降低脏页阈值,减少突发 IO
vm.dirty_ratio = 10
vm.dirty_background_ratio = 3
# 降低 vfs_cache_pressure,优先保留目录项缓存
vm.vfs_cache_pressure = 50
# 降低 swappiness,减少不必要的 swap
vm.swappiness = 10
# 预留更多紧急内存
vm.min_free_kbytes = 524288
验证结果
修复后,监控显示:
top - 15:30:00 load average: 0.8, 3.2, 6.5
%Cpu(s): 6.5 us, 2.1 sy, 0.0 ni, 89.2 id, 1.8 wa
MiB Mem: 32034.7 total, 8245.6 free, 19876.3 used, 3912.8 buff/cache
- load 从 15.8 降到 0.8
- CPU idle 从 73% 恢复到 89%
- free 从 400MB 恢复到 8.2GB
- Swap 使用从 6GB 降到 300MB

避坑建议
1. 大文件 IO 操作必做 Page Cache 评估
所有涉及批量文件读写(读取原图、批量导出、数据同步等)的代码,必须评估 Page Cache 影响: - 文件总大小是否会超过可用内存的 50%? - 读完后是否需要保留缓存在内存中? - 是否可以分批次处理?
2. 用完即弃:主动释放 Page Cache 的策略
FileChannel.map + unmap:Java 层面的 MappedByteBuffer 方案posix_fadvise(POSIX_FADV_DONTNEED):C/JNI 层面的精准释放O_DIRECT绕过 Page Cache(适合大文件顺序读且只读一次的场景)
3. 内核参数调优
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
vm.dirty_ratio |
30 | 5-10 | 降低脏页上限,减少 IO 突刺 |
vm.dirty_background_ratio |
10 | 3-5 | 后台回刷阈值 |
vm.vfs_cache_pressure |
100 | 50-100 | 降低可优先保留 dentry/inode |
vm.swappiness |
60 | 10-30 | 减少不必要的 swap |
vm.min_free_kbytes |
auto | 512MB+ | 预留紧急内存 |
4. 监控指标补充
系统级监控不要只看 CPU 和内存总量,必须补充以下指标:
| 指标 | 命令 | 说明 |
|---|---|---|
| Page Cache 量 | /proc/meminfo 的 Cached |
缓存是否异常增长 |
| 脏页量 | /proc/meminfo 的 Dirty |
回刷压力 |
| pgscank/pgscand | sar -B |
后台/直接回收扫描量 |
| D 状态进程数 | vmstat 的 b 列 |
IO/内存回收阻塞 |
| await + %util | iostat -x |
磁盘是否饱和 |
5. 诊断手记
遇到「CPU 不忙但 load 高」时,排查路径:
top (CPU idle 高但 load 高)
→ vmstat (b 列高 -> D 状态进程)
→ sar -B (pgscank 高 -> 内存回收)
→ /proc/meminfo (Cached 高 -> Page Cache)
→ iostat -x (await 高 -> IO 瓶颈)
→ perf top (shrink_page_list 热 -> 确认内存回收)
附:完整命令清单
系统资源排查
top -b -n 1 | head -25 # 查看进程负载排行和 CPU 状态
vmstat 2 6 # 查看 IO 阻塞(b列)和内存回收
sar -B 2 5 # 查看 page scan/reclaim 统计
sar -W 2 3 # 查看 swap 换入换出
iostat -x 2 5 # 查看磁盘 IO 利用率
cat /proc/meminfo # 查看 Page Cache / 脏页 / 内存分布
cat /proc/pressure/memory # PSI 内存压力指标
内核热点分析
sudo perf top -K -g --sort=comm -n 10 # 内核热力图
sudo perf top -K -g -p $(pgrep -d, -f kswapd) # 单独看 kswapd 线程
内核参数查看与调优
sysctl vm.dirty_ratio vm.dirty_background_ratio # 脏页阈值
sysctl vm.vfs_cache_pressure vm.swappiness vm.min_free_kbytes # 内存回收相关
sysctl -w vm.dirty_ratio=10 # 临时调整脏页阈值
Demo 验证
mvn compile exec:java -Dexec.mainClass="cn.opencao.onlineissue.pagecachememoryreclaim.PageCacheDemo" -Dexec.args="v1" # V1:不释放 Page Cache
mvn compile exec:java -Dexec.mainClass="cn.opencao.onlineissue.pagecachememoryreclaim.PageCacheDemo" -Dexec.args="v2" # V2:主动释放 Page Cache
📖 全文带可复现 Demo 和排查截图 🔗 个人博客:https://opencao.cn 📺 公众号:Ai拆代码的曹操 🌟 知识星球:源阅会 (82877104)