本文是线上问题实战录系列的第 8 篇 叙事框架:
现象 → 排查过程 → 根因 → 修复 → 预防
2026 年 6 月 17 日下午 2 点 15 分,监控告警同时触发了四条规则:
cms-prod-01 load average 达到 15.8(阈值 5.0)但一个令人困惑的现象是:CPU 并不忙。top 显示 CPU idle 仍有 73%,us 只有 8.5%。

登录生产服务器,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 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

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
关键指标:

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

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。

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
这是最直接的证据——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)
→ 可用内存不足导致更多直接回收
→ 恶性循环
这是理解 Page Cache 问题的关键。load average 统计的是 R(运行)+ D(不可中断睡眠) 状态的进程数。
当大量进程在等待磁盘 IO 时,它们处于 D 状态,不消耗 CPU 但会计入 load。所以出现「CPU 不忙、load 很高」的现象时,要立即想到 IO 阻塞 或 内存回收。
两个方向同时进行:
// 修复前:批量读取不释放缓存
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

所有涉及批量文件读写(读取原图、批量导出、数据同步等)的代码,必须评估 Page Cache 影响: - 文件总大小是否会超过可用内存的 50%? - 读完后是否需要保留缓存在内存中? - 是否可以分批次处理?
FileChannel.map + unmap:Java 层面的 MappedByteBuffer 方案posix_fadvise(POSIX_FADV_DONTNEED):C/JNI 层面的精准释放O_DIRECT 绕过 Page Cache(适合大文件顺序读且只读一次的场景)| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
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+ | 预留紧急内存 |
系统级监控不要只看 CPU 和内存总量,必须补充以下指标:
| 指标 | 命令 | 说明 |
|---|---|---|
| Page Cache 量 | /proc/meminfo 的 Cached |
缓存是否异常增长 |
| 脏页量 | /proc/meminfo 的 Dirty |
回刷压力 |
| pgscank/pgscand | sar -B |
后台/直接回收扫描量 |
| D 状态进程数 | vmstat 的 b 列 |
IO/内存回收阻塞 |
| await + %util | iostat -x |
磁盘是否饱和 |
遇到「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 # 临时调整脏页阈值
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)