草草聊事

Page Cache 管理不当导致的 load 飙高——内存回收篇

2026/06/19
3
0

Page Cache 管理不当导致的 load 飙高——内存回收篇

本文是线上问题实战录系列的第 8 篇 叙事框架:现象 → 排查过程 → 根因 → 修复 → 预防


问题现象

2026 年 6 月 17 日下午 2 点 15 分,监控告警同时触发了四条规则:

  • Load 飙高:CMS 图片处理节点 cms-prod-01 load 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。

top 输出

更关键的线索是进程列表中出现了 kswapd0kswapd1kcompactd0——内核在拼命回收内存。

第二步: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/秒:系统在换出页面

vmstat 输出

第三步: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%:回收效率尚可,但扫描量太高达不到平衡

sar -B 输出

第四步:查看 /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:物理内存不足,大量匿名页被换出

proc/meminfo

第五步: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。

iostat

第六步: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 时间主要消耗在内存回收路径上,而不是业务代码。

perf top

第七步:检查脏页配置

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
        }
    }
}

V1 问题代码 V2 修复代码 Git Diff

方向二:内核参数调优

内核参数配置

# /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)