回到工程:Linux VM 如何改变性能诊断
前面 16 篇从地址空间到 OOM,逐层拆解了 Linux 虚拟内存子系统的结构和机制。这些知识的价值不只在于读源码——更在于它提供了一种分层诊断习惯:遇到内存相关的性能问题时,先定位层次,再找对象,再看状态转移,最后用指标验证。
核心问题可以压成一句话:
Linux VM 的价值不只在源码知识,而在一种分层诊断习惯:先定位层次,再找对象,再看状态转移,最后用指标验证。
系列概念地图
整个系列覆盖的层次和对象:
1 | |
每一层有自己的对象、状态转移和可观测指标。诊断问题的关键是确定"问题发生在哪一层"。
症状到层次的诊断表
| 症状 | 可能的层次 | 首先检查的指标 | 对应文章 |
|---|---|---|---|
| RSS 持续增长 | VMA / page fault | /proc/<pid>/smaps、maps |
01, 03 |
| 延迟尖刺(毫秒级) | direct reclaim / compaction | vmstat、sar -B、/proc/vmstat allocstall |
09, 12 |
| TLB miss 导致 CPU stall | huge page / TLB | perf stat dTLB-load-misses |
12 |
| 进程被 SIGKILL | OOM killer | dmesg、memory.events |
16 |
| 容器 OOM 但宿主有空闲 | memcg 限制 | memory.current vs memory.max |
15, 16 |
| NUMA remote access 高 | 内存位置 | numastat、numa_maps |
11 |
| slab 内存高 | SLUB cache | /proc/slabinfo、slabtop |
14 |
| swap 频繁进出 | 匿名页压力 | vmstat si/so、/proc/vmstat pswpin/pswpout |
10 |
| page fault 延迟高 | 分配路径 / buddy 碎片 | /proc/buddyinfo、compact_stall |
03, 13 |
| 文件 I/O 第二次变快 | page cache 命中 | free cached 列、/proc/meminfo Cached |
05 |
这张表不是穷举,但提供了一个起点:从可观察的症状出发,快速定位到最可能的 VM 层次。
诊断方法论
1 | |
Step 1: 症状分类
先确定问题的性质:是内存使用量异常(RSS 增长、OOM)还是延迟异常(stall、尖刺)还是吞吐异常(带宽低)。这决定了向哪一层排查。
Step 2: 定位层次
用诊断表或经验判断最可能的层次。一个关键启发:延迟问题多在 reclaim/compaction/NUMA 层;使用量问题多在 VMA/page fault/memcg 层。
Step 3: 找到对象
确定层次后,找到具体的对象。例如:reclaim 层的对象是 LRU 上的页面、buddy 层的对象是 free_area 中的块、memcg 层的对象是 cgroup 预算。
Step 4: 看状态转移
对象在什么条件下发生状态变化?页面从 active → inactive → reclaimed、从 free_area → allocated → freed → buddy merge。状态转移的触发条件和频率是性能的关键。
Step 5: 用指标验证
每一层都有对应的 /proc 或 /sys 指标。用指标验证假设:如果假设是"direct reclaim 导致延迟",就看 allocstall_normal 的增量是否与延迟尖刺时间吻合。
案例一:RSS 增长但 maps 看不出问题
症状:Java 服务的 RSS 在 top 中持续增长到 8GB,但 /proc/<pid>/maps 中各段看起来正常,heap 段也不大。
诊断路径:
- 层次:RSS 增长 → VMA / page fault 层
- 对象:用
smaps_rollup看 RSS 组成1
2
3
4cat /proc/<pid>/smaps_rollup
# Rss: 8388608 kB
# Anonymous: 1048576 kB ← 匿名页只有 1GB
# 其他去哪了? - 细分:用
smaps按段查看1
2grep -A5 "rss" /proc/<pid>/smaps | sort -k2 -n -r | head
# 发现大量 file-backed mapping (mmap'd JAR/SO 文件) - 状态转移:file-backed pages 是 page cache,被 mmap 后计入 RSS 但实际可回收
- 验证:
MemAvailable仍然充裕,Inactive(file)增长与 RSS 增长一致
结论:RSS 增长来自 file-backed page cache(JVM 加载的 JAR、SO 文件)。这些页面在内存压力下会被回收,不是内存泄漏。真正需要关注的是 Anonymous 部分。
案例二:延迟尖刺来自 direct reclaim
症状:Web 服务的 P99 延迟偶尔从 5ms 飙到 50ms,无明显流量变化。
诊断路径:
- 层次:延迟尖刺 → reclaim / compaction 层
- 指标采集:
1
2
3
4# 监控 allocstall(direct reclaim 次数)
watch -n1 'grep allocstall /proc/vmstat'
# 监控 compact_stall(直接 compaction 次数)
watch -n1 'grep compact_stall /proc/vmstat' - 关联:
allocstall_normal增量与尖刺时刻吻合 - 根因:zone Normal 的 low watermark 被频繁触及,kswapd 回收速度不够,请求线程进入 direct reclaim 阻塞
- 验证:
kswapd_steal和pgsteal_direct的比例——正常系统 kswapd 负责 >90% 的回收,如果 direct 比例高说明 kswapd 跟不上
解决方向:
- 调高
vm.min_free_kbytes→ 提前唤醒 kswapd - 减少 page cache 占用(调低
vm.dirty_ratio) - 如果是 THP compaction 导致:设置
defrag=defer避免同步 compaction
案例三:容器被 OOM kill 但宿主有空闲内存
症状:Kubernetes pod 被 OOMKilled(exit code 137),但 node 的 MemAvailable 显示 32GB 空闲。
诊断路径:
- 层次:OOM → memcg 层(不是全局)
- 确认 scope:
1
2
3
4
5
6
7# 查看 pod 的 memory limit
kubectl describe pod <name> | grep -A5 Limits
# memory: 512Mi
# 查看 cgroup events
cat /sys/fs/cgroup/.../memory.events
# oom_kill 1 - 分析使用模式:
1
2
3cat /sys/fs/cgroup/.../memory.stat
# anon 450M ← 匿名页高
# file 60M ← page cache - 状态转移:匿名页无法回收(没有 swap),file pages 已经被回收到极低,仍然不够 → memcg OOM
- 验证:
memory.current触碰memory.max时 OOM 发生
解决方向:
- 调高 pod 的 memory limit(如果宿主有余量)
- 排查应用内存泄漏(anon RSS 持续增长?)
- 启用 swap(允许匿名页换出而不是直接 OOM)
- 设置
memory.high实现 graceful degradation
工具速查
| 工具 / 文件 | 用途 | 对应层次 |
|---|---|---|
/proc/<pid>/maps, smaps |
进程地址空间布局 | VMA |
/proc/<pid>/pagemap |
虚拟页 → 物理页映射 | 页表 |
/proc/vmstat |
全局 VM 计数器 | 全层 |
/proc/meminfo |
全局内存分类统计 | 全层 |
/proc/buddyinfo |
buddy allocator 碎片状态 | buddy |
/proc/slabinfo |
slab cache 统计 | SLUB |
/proc/zoneinfo |
per-zone 水位线和统计 | zone/reclaim |
numastat, numa_maps |
NUMA 分布 | NUMA |
memory.stat, memory.events |
per-cgroup 统计 | memcg |
perf stat |
硬件 PMU 计数器(TLB miss 等) | TLB/huge page |
vmstat, sar -B |
系统级 paging 活动 | reclaim/swap |
dmesg |
OOM 日志 | OOM |
后续阅读路线
从本系列出发,几条深入方向:
源码阅读:mm/ 目录是入口。按需读——带着具体问题进入源码比通读更有效。每篇的"源码锚点"提供了起点。
性能分析:Brendan Gregg 的 Systems Performance(第二版)覆盖了从 CPU 到内存到 I/O 的性能方法论。本系列提供的是内核视角的底层机制,Gregg 提供的是诊断工作流。
容器内存管理:cgroup v2 文档 + Kubernetes memory management 文档。理解 memcg 之后,Kubernetes 的 requests/limits 语义会变得透明。
NUMA 和大内存系统:numactl、numad、AutoNUMA(NUMA balancing)的实际调优。在 128+ 核的机器上,NUMA 策略的影响远大于小系统。
内存安全:KASAN(内核地址消毒剂)、KMSAN(未初始化内存检测)。理解 SLUB 的 redzone 和 poisoning 后,这些工具的原理会更清晰。
模式提炼:分层诊断
1 | |
这个诊断框架不限于内存。CPU 问题(哪个核?哪个调度类?哪个 cgroup?)、I/O 问题(哪个设备?哪个 I/O 调度器?哪个文件系统?)、网络问题(哪一层协议?哪个 socket?哪个 netns?)都可以用同样的分层思路。Linux 内核的设计本身就是分层的——诊断方法与系统结构对齐,效率最高。
系列导航
- 上一篇:OOM Killer:内核什么时候决定杀进程
- 本文:回到工程:Linux VM 如何改变性能诊断
- 系列首篇:重读 Linux VM:给系统研究生的虚拟内存导读
全系列索引
| # | 标题 | 核心问题 |
|---|---|---|
| 00 | 重读 Linux VM:给系统研究生的虚拟内存导读 | 虚拟内存全貌与阅读路线 |
| 01 | 地址空间不是数组:mm_struct 和 vm_area_struct | 进程地址空间的内核表示 |
| 02 | 页表:CPU 能读懂的翻译结构 | 多级页表的结构与遍历 |
| 03 | 缺页异常:一次访问如何进入内核 | page fault 分发与处理 |
| 04 | 匿名页和 COW:fork 为什么便宜,写入为什么变贵 | copy-on-write 机制 |
| 05 | 文件映射和 page cache:文件内容怎样变成页面 | 文件 I/O 与缓存 |
| 06 | Folio:为什么内核重新组织 page 抽象 | page → folio 的演进 |
| 07 | 反向映射:从物理页找回虚拟地址 | rmap 机制 |
| 08 | LRU 和 workingset:内核如何近似"最近使用" | 页面老化与 working set |
| 09 | 页回收:kswapd 和 direct reclaim | 内存回收机制 |
| 10 | Swap:换出去的页怎样回来 | swap 生命周期 |
| 11 | NUMA:内存为什么有远近 | 非均匀内存访问 |
| 12 | Huge Page:TLB 压力和页表开销 | 大页与 TLB 优化 |
| 13 | Buddy system:物理页如何按阶分配 | 物理页分配器 |
| 14 | SLUB:小对象为什么不直接按页分配 | 内核对象分配器 |
| 15 | cgroup memory:内存从全局资源变成局部预算 | 内存资源隔离 |
| 16 | OOM Killer:内核什么时候决定杀进程 | 最终兜底机制 |
| 17 | 回到工程:Linux VM 如何改变性能诊断 | 诊断方法论与案例 |
参考资料
- Memory Management - The Linux Kernel documentation
- Brendan Gregg, Systems Performance, 2nd Edition (Addison-Wesley, 2020)
- proc(5) - Linux manual page
- Control Group v2 - The Linux Kernel documentation
