这篇文章既是面试前复习提纲,也是一份可以照着执行的线上排障手册。核心方法是:先判断系统是否真的有内存压力,再区分进程匿名内存、文件页缓存、内核 Slab、Swap、缺页异常或 cgroup 限制,最后把问题落到具体进程、内存映射和分配调用栈。
一、先记住完整排查路径
内存告警、延迟升高或进程被杀
|
+-- free -h + /proc/meminfo:available 是否不足,内存花在哪里?
|
+-- vmstat 1 + sar -r/-S/-B:问题是持续增长、回收、Swap 还是缺页?
| |
| +-- 进程 RSS/PSS 增长:top/ps/pidstat -> smaps_rollup/pmap
| | -> memleak/Valgrind/ASan 找分配栈
| +-- Cached 增长:cachestat/cachetop -> 找高 miss 或 dirty 的进程
| +-- Slab 增长:slabtop + /proc/slabinfo -> 找内核对象
| +-- si/so 持续非零:sar -W + smem/VmSwap -> 找换入换出进程
| +-- majflt/s 高:pidstat -r -> 文件映射、冷启动或 Swap I/O
| +-- OOM:journalctl/dmesg -> 读 victim、cgroup、oom_score
|
+-- 宿主机看着正常但容器 OOM:memory.current/events/stat/pressure
|
+-- NUMA 机器还有 free 却回收:numastat + /proc/zoneinfo
排查时要形成证据链:总量说明有没有压力,趋势说明问题何时发生,分类指标说明内存被谁使用,进程映射或分配栈说明代码根因。只凭 free 的 free 列很小,不能得出“内存不足”的结论。
二、先理解 Linux 内存工作的主线
1. 虚拟内存、页表、MMU 和 TLB
每个进程看到的是独立、连续的虚拟地址空间。内核通过页表记录虚拟页到物理页的映射,CPU 的 MMU 负责地址转换,TLB 则缓存近期的页表转换结果。
Linux 通常以 4 KiB 页为最小映射单位,也支持 2 MiB、1 GiB 等 Huge Page。大页可以减少页表项和 TLB miss,适合数据库、DPDK 等大内存工作负载,但会提高连续物理内存需求和管理复杂度,不能看到“大页更快”就盲目启用。
当进程第一次访问尚未建立有效映射的地址时,会发生缺页异常:
- 次缺页(minor fault):不需要从磁盘读取数据,例如匿名页首次分配,或目标页已在页缓存中。
- 主缺页(major fault):需要存储 I/O,例如从文件或 Swap 读回页面,延迟明显更高。
系统调用 malloc() 成功通常只代表获得一段虚拟地址空间。受按需分配影响,物理页往往在首次写入时才真正分配。因此 VIRT 很大不等于物理内存已经耗尽。
2. 进程地址空间有哪些部分
典型用户空间从低地址到高地址包括:
- 代码和只读段:程序指令、只读常量。
- 数据段:已初始化和未初始化的全局/静态变量。
- 堆:动态分配区域,传统上向高地址增长。
- 文件映射区:动态库、普通文件映射、共享内存和匿名
mmap()。 - 栈:局部变量、返回地址和调用上下文;线程通常各自拥有栈。
glibc 的 malloc() 可能通过 brk() 扩展堆,也可能使用匿名 mmap()。具体阈值不是固定的 128 KiB,它会受分配器版本、运行状态和配置影响。小块释放后也可能被分配器缓存而不立即归还内核,所以“RSS 没降”不一定等于泄漏。
3. 内存不足时内核做什么
Linux 会优先回收不活跃页面:
- 丢弃可重新从磁盘读取的干净文件页。
- 回写脏文件页后再回收。
- 启用 Swap 时,把不活跃匿名页换出。
- 回收仍无法满足分配时,触发 OOM killer 终止进程。
kswapd 在后台按水位回收;分配路径来不及等待时会发生 direct reclaim,业务线程直接参与回收,容易造成延迟尖峰。现代系统还可以通过 PSI 观察任务因内存回收或等待而停顿的程度。
三、free:先判断系统是否真的内存不足
free -h
free -h -w
free -h -s 1
-h:按 GiB、MiB 等单位显示。-w:把buffers和cache分开显示,便于进一步区分。-s 1:每秒刷新一次;按Ctrl+C结束。适合快速看趋势,但正式趋势分析优先用vmstat或sar。
关键字段:
| 字段 | 含义 | 排障重点 |
|---|---|---|
total |
可管理的物理内存总量 | 不是简单等于内存条标称容量 |
used |
按 free 当前口径计算的已用内存 | 不同 procps 版本口径可能略有差异 |
free |
完全未使用的内存 | 小并不一定有问题 |
shared |
主要是 tmpfs 等共享内存 | 容器 /dev/shm、tmpfs 异常时会增长 |
buff/cache |
Buffer、页缓存和可回收 Slab 等 | 多数可在压力下回收,不等于泄漏 |
available |
不发生明显 Swap 时,新应用大致可用的内存估计 | 判断内存余量优先看它 |
Linux 会利用闲置内存做页缓存,因此 free 很小而 available 仍很大是正常现象。真正值得关注的是:available 持续下降、Swap 活跃、回收压力升高、业务延迟恶化,或者出现 OOM。
Swap 行中的 used 也只是当前有多少页面留在 Swap,并不代表系统此刻仍在频繁交换。判断性能影响要看 vmstat 的 si/so 或 sar -W 的实时速率。
四、/proc/meminfo:拆开内存构成
grep -E 'MemTotal|MemFree|MemAvailable|Buffers|Cached|SwapCached|Active|Inactive|AnonPages|Mapped|Shmem|Slab|SReclaimable|SUnreclaim|Dirty|Writeback|PageTables|Committed_AS|CommitLimit|Huge' /proc/meminfo
watch -n 1 "grep -E 'MemAvailable|Cached|AnonPages|Slab|SUnreclaim|Dirty|SwapFree' /proc/meminfo"
第一条输出关键分类,第二条每秒观察变化。/proc/meminfo 是累计状态快照,比较连续样本才能看趋势。
重点指标:
MemAvailable:系统估算的新负载可用内存。Cached:文件页缓存,不包含SwapCached。AnonPages:用户态堆、栈等匿名页的重要组成。Shmem:tmpfs、共享内存等使用量。Slab:内核对象缓存,分为可回收SReclaimable和不可回收SUnreclaim。Dirty:已修改但未回写存储的文件页。Writeback:正在回写的页面。PageTables:页表占用;进程或映射数量异常时可能升高。Committed_AS:系统已承诺给进程的虚拟内存总量,不等于当前 RSS。CommitLimit:当前 overcommit 策略下的承诺上限。
查看内存承诺策略:
sysctl vm.overcommit_memory vm.overcommit_ratio
vm.overcommit_memory 常见值为:0 启发式判断,1 总是允许超额承诺,2 严格按承诺上限检查。不要为了绕过分配失败直接改成 1;这可能把问题推迟到真正访问页面时,以 OOM 的方式爆发。
五、Buffer、Cache 和 Slab 怎么区分
1. 不要只背“Buffer 写磁盘、Cache 读文件”
现代 Linux 中更实用的理解是:
Buffers主要与块设备元数据和原始块缓冲相关,通常不大。Cached主要是普通文件的页缓存,读文件会填充它,缓冲写也会先形成脏页。SReclaimable是内核 Slab 中可回收的对象缓存,例如部分 inode、dentry。SUnreclaim是当前不可回收的 Slab,持续异常增长时要警惕内核对象或驱动问题。
free 的 buff/cache 会合并多个分类,所以发现它很大后要回到 /proc/meminfo 判断究竟是文件页还是 Slab。
2. vmstat:观察内存、Swap、I/O 和回收趋势
vmstat -w 1
vmstat -w 1 10
vmstat -s
-w:宽输出,避免大数值挤在一起。1:每秒采样。1 10:每秒一次,共 10 组。第一行通常是开机以来平均值,分析当前问题看后续样本。-s:输出开机以来的内存事件和计数,适合总览,不适合判断瞬时速率。
内存相关字段:
| 字段 | 含义 | 看到异常后怎么走 |
|---|---|---|
swpd |
已使用 Swap | 只代表存量,继续看 si/so |
free |
完全空闲内存 | 与 available、缓存一起判断 |
buff |
Buffer | 回到 /proc/meminfo 验证 |
cache |
页缓存等 | 持续增长时用缓存工具找来源 |
si |
每秒从 Swap 换入 | 持续非零说明访问了已换出页面 |
so |
每秒换出到 Swap | 持续非零说明匿名页回收活跃 |
bi/bo |
块设备读入/写出 | 与 Swap 和文件回写交叉判断 |
vmstat 的 si/so 位于 swap 区域,不能和 top CPU 行中的软中断 si 混淆。
3. cachestat:看整机页缓存命中率
BCC 版本常见命令:
sudo cachestat 1 10
sudo /usr/share/bcc/tools/cachestat 1 10
不同发行版命令路径和字段名可能不同,先用 command -v cachestat 确认。参数 1 10 表示每秒输出一次,共 10 次。常见字段包括缓存命中次数、未命中次数、脏页事件和命中率。
适用场景:
- 应用读延迟升高,怀疑工作集超过内存导致缓存命中率下降。
- 文件读取带宽很大,要区分请求来自内存页缓存还是磁盘。
- 发布或冷启动后主缺页增加,验证缓存预热是否完成。
低命中率同时伴随块设备读取和主缺页上升,说明确实在访问存储;仅命中率波动而没有 I/O 和延迟变化,不能单独定性为瓶颈。
4. cachetop:按进程观察页缓存活动
sudo cachetop
sudo cachetop 5
sudo /usr/share/bcc/tools/cachetop 5
5 表示每 5 秒刷新。它通常按缓存访问量排序,展示 PID、命中、未命中、脏页和命中率,用于回答“哪个进程正在大量读写页缓存”。
判断方法:
/proc/meminfo发现Cached持续增长。cachestat 1确认页缓存活动和命中率变化。cachetop 5找出访问最活跃或 miss 较高的进程。- 再结合
pidstat -d 1、iostat或应用请求日志,确认是否为大文件扫描、备份、日志或冷数据访问。
eBPF/BCC 工具依赖内核能力、BTF/内核头和权限。生产机无法运行时,可用 sar -r、pidstat -d、mincore() 类工具或应用级指标替代,但观测维度会弱一些。
5. slabtop:页缓存不大但 Slab 很大
sudo slabtop -o
sudo slabtop -s c
grep -E 'Slab|SReclaimable|SUnreclaim' /proc/meminfo
-o:输出一次后退出,适合保存现场。-s c:按 cache size 排序;具体排序字符用man slabtop确认。
重点看对象名、对象数量、单对象大小和总 cache size。常见大项包括 dentry、inode、网络连接跟踪等。SReclaimable 大通常可以在压力下回收;SUnreclaim 持续增长且无法随负载回落,更值得检查内核模块、网络对象或驱动泄漏。
六、top、ps、pidstat:定位高内存进程
1. top:实时按内存排序
top
top -p 1234
top -b -n 3 -d 1 -o %MEM
进入交互界面后按 M 按内存排序;-p 只看指定进程;批处理命令采样 3 次,适合记录现场。
进程字段:
VIRT:全部虚拟地址空间,包括未实际驻留、共享库、映射文件和已换出页面。大不等于泄漏。RES:当前驻留物理内存,通常包含共享驻留页,不能简单把多个进程 RES 相加。SHR:可能共享的驻留页,包括共享库、代码段和共享内存;不保证每一页当前真的被多个进程共享。%MEM:RES 占物理内存的比例。
2. ps:保存进程内存快照
ps -eo pid,ppid,user,stat,vsz,rss,pmem,comm,args --sort=-rss | head -n 20
ps -p 1234 -o pid,ppid,vsz,rss,pmem,etime,comm,args
vsz:虚拟内存,通常以 KiB 显示。rss:常驻集,通常以 KiB 显示。etime:已运行时间,有助于区分刚启动峰值与长期增长。
ps 是瞬时快照。判断泄漏不能只看一次 RSS,而要持续采样,最好同时关联请求量、连接数、队列长度和缓存大小。
3. pidstat -r:连续看进程内存和缺页
pidstat -r -p ALL 1
pidstat -r -p 1234 1 10
-r:报告进程缺页和内存使用。-p ALL:监控所有进程,也可以指定 PID。1 10:每秒一次,共 10 次。
常见字段:
minflt/s:每秒次缺页,不需要存储 I/O;程序首次触碰大量匿名页时可能很高。majflt/s:每秒主缺页,需要存储 I/O;持续升高通常对延迟影响明显。VSZ、RSS、%MEM:虚拟、常驻和比例。
如果 RSS 持续增长而负载规模稳定,进入 smaps/pmap;如果 majflt/s 高,结合 sar -B、vmstat 的 si/bi 和存储指标判断是 Swap 还是文件映射读取。
七、RSS、PSS、USS:统计进程内存不要重复计算
1. 三个指标的区别
RSS:进程驻留的全部物理页,共享页会在每个进程中重复计算。PSS:共享页按共享进程数量平摊,再加私有页。汇总多个进程时更合理。USS:进程独占的私有驻留页,进程退出后理论上可立即释放的部分。
所以不能把所有进程 RSS 直接相加,尤其是大量 worker 共享代码和库时,结果会明显大于实际物理使用量。
2. /proc/PID/status:快速查看一个进程
grep -E 'Name|Pid|VmPeak|VmSize|VmHWM|VmRSS|RssAnon|RssFile|RssShmem|VmSwap|Threads' /proc/1234/status
VmHWM:历史 RSS 峰值。VmRSS:当前 RSS。RssAnon:匿名驻留页,堆栈增长常反映在这里。RssFile:文件映射驻留页。RssShmem:共享内存驻留页。VmSwap:该进程换出的匿名私有页;共享 Swap 的统计存在口径限制。
3. smaps_rollup:低成本获得 PSS 汇总
grep -E 'Rss|Pss|Pss_Anon|Pss_File|Pss_Shmem|Private|Shared|Anonymous|Swap' /proc/1234/smaps_rollup
smaps_rollup 把所有映射汇总,适合快速判断匿名、文件、共享和 Swap 构成。较老内核没有该文件时,读取 /proc/1234/smaps 并自行汇总。
权限受 ptrace_scope、用户身份和容器边界限制;无权限时不要为了方便关闭整机安全设置,应由目标用户或获得最小必要诊断权限后读取。
4. pmap:找出哪段地址空间增长
pmap -x 1234
pmap -XX 1234
watch -n 5 'pmap -x 1234 | tail -n 1'
-x:展示映射地址、大小、RSS、脏页和映射名。-XX:输出更完整的内核映射字段,但格式依 procps 版本变化。watch示例每 5 秒观察汇总行;要找具体增长区,保存两次pmap -x再比较。
判断方向:
[heap]或大量匿名映射增长:检查分配器缓存、对象生命周期和泄漏。- 某个文件映射 RSS 增长:检查 mmap 文件、数据库或索引工作集。
- 线程栈映射数量增长:检查线程泄漏和线程池上限。
- VIRT 增长但 RSS 不变:只是地址空间预留,暂时不是物理内存压力。
5. smem:按 PSS 排序更准确
sudo smem -tk
sudo smem -r -k -s pss
sudo smem -p -k -P 'your-process-regex'
-t:显示合计。-k:使用可读单位。-r:反向排序。-s pss:按 PSS 排序。-P:按进程映射/命令正则筛选,具体匹配口径以本机smem --help为准。
smem 从 procfs 读取 PSS/USS/RSS,未必默认安装。无法安装时可读取 smaps_rollup;统计全系统 PSS 会遍历大量映射,进程很多时不要高频运行。
八、发现内存持续增长:怎么确认泄漏
“内存上涨”可能是泄漏,也可能是正常缓存、分配器 arena、业务工作集增长或流量增加。正确顺序是:
free -h与vmstat 1确认available是否持续下降。pidstat -r -p ALL 1找 RSS 持续增长的进程。/proc/PID/smaps_rollup区分匿名、文件和共享内存。pmap -x PID找增长的映射类型。- 在可控时间窗用
memleak、Valgrind、ASan/LSan 或堆分析器取得分配栈。
1. BCC memleak:附加到正在运行的进程
sudo /usr/share/bcc/tools/memleak -p 1234
sudo /usr/share/bcc/tools/memleak -p 1234 -a
sudo /usr/share/bcc/tools/memleak -p 1234 5 6
不同 BCC 版本的参数可能有差异,必须先运行:
sudo /usr/share/bcc/tools/memleak --help
一般来说,-p 指定目标 PID,-a 显示每个未释放地址,末尾的 5 6 表示每 5 秒输出一次、共输出 6 次。工具会周期输出 outstanding allocations 及调用栈;过滤参数的准确含义以本机版本为准。
使用方法:让应用在稳定请求下运行,观察同一调用栈的未释放字节和对象数是否跨多个周期持续增长。单次看到 outstanding allocation 不一定是泄漏,因为对象可能只是生命周期较长。
目标程序最好保留调试符号,并使用帧指针或可靠的 DWARF 展栈信息。容器进程要注意宿主机 PID、符号路径和 mount namespace;工具无法解析时可在宿主机以容器对应 PID 运行,或在诊断镜像中部署符号。
2. Valgrind Memcheck:适合测试环境复现
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all --track-origins=yes --log-file=valgrind.%p.log ./your_program --args
--tool=memcheck:启用内存错误检查。--leak-check=full:输出每条泄漏的分配栈。--show-leak-kinds=all:显示 definitely/indirectly/possibly lost 和 still reachable。--track-origins=yes:追踪未初始化值来源,开销更大。--log-file=...:按 PID 保存日志。
重点先修 definitely lost。still reachable 表示退出时仍可达,可能是缓存或运行库保留,不应机械当作泄漏。Valgrind 会显著拖慢程序并增加内存开销,通常不直接挂在线上高流量进程,也不能像 BCC memleak 那样方便地附加到任意已运行进程。
3. AddressSanitizer/LeakSanitizer:开发和 CI 优先
clang++ -g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined app.cpp -o app
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 ./app
ASan 能发现越界、use-after-free 等问题,LSan 负责退出时泄漏检测。它需要重新编译,适合测试和 CI。生产二进制不应未经评估直接开启 sanitizer。
C++ 中更根本的修复通常是使用 RAII:std::vector、std::string、std::unique_ptr 和明确所有权,避免裸 new/delete 跨多条异常路径手工配对。对于高频分配,再考虑对象池、arena 和分配器统计,而不是先把池子无限放大。
九、Swap 升高:区分存量和正在交换
1. swapon 和 free:看配置与存量
swapon --show --bytes
cat /proc/swaps
free -h
sysctl vm.swappiness
swapon --show:列出 Swap 设备/文件、大小、已用量和优先级。/proc/swaps:底层数据源。vm.swappiness:文件页和匿名页回收偏好,范围通常为 0 到 200(现代内核可能支持超过 100);它不是“内存使用百分比”。
Swap 已用量高但长时间没有 si/so,可能只是冷匿名页仍留在磁盘,对当前性能影响有限。真正的抖动是持续换入换出并伴随延迟、I/O 和 PSI 上升。
2. sar -S、sar -W:看 Swap 容量与速率
sar -S 1 10
sar -W 1 10
sar -r ALL 1 10
-S:Swap 空间使用量。-W:每秒换入、换出页数。-r ALL:内存利用率及更完整分类;字段依 sysstat 版本变化。1 10:每秒一次,共 10 次。
pswpin/s、pswpout/s 持续非零说明交换活跃。若同时块设备延迟、vmstat wa 和 /proc/pressure/memory 上升,Swap 正在影响响应时间。
3. 找到使用 Swap 的进程
有 smem 时:
sudo smem -r -k -s swap
没有 smem 时:
for status in /proc/[0-9]*/status; do
awk '/^(Name|Pid|VmSwap):/{printf "%s ", $2} END{print ""}' "$status" 2>/dev/null
done | sort -k3 -nr | head
输出依次为进程名、PID 和 VmSwap 数值,通常单位是 KiB。这个脚本只做快速归因;共享页面和进程退出竞态会造成统计偏差。
进一步查看目标进程:
grep -E 'Rss|Pss|Anonymous|Swap' /proc/1234/smaps_rollup
4. 为什么还有空闲内存却发生 Swap
常见原因:
- Swap 是此前压力期留下的存量,内核不会只因当前 free 增加就主动全部换回。
- NUMA 节点局部内存不足,虽然整机还有空闲页。
- cgroup 内存达到限制,宿主机仍很空闲。
- 内核水位、内存碎片或高阶页分配触发回收。
- 文件页与匿名页回收权衡导致部分冷匿名页被换出。
查看 NUMA:
numactl --hardware
numastat -m
numastat -p 1234
grep -E 'Node|zone|pages free|min|low|high' /proc/zoneinfo
numastat -m 看各节点内存,-p 看进程分布;/proc/zoneinfo 展示各 zone 的 free 和水位。它们用于解释局部回收,不建议看到水位就直接调整 vm.min_free_kbytes,错误设置会浪费大量内存或加剧分配失败。
5. 不要在线上直接“清空 Swap”
sudo swapoff -a
sudo swapon -a
swapoff 会尝试把已换出页面全部搬回物理内存。若 available 不足,可能造成严重 I/O、分配失败甚至 OOM。它不是常规清理命令,只应在维护窗口、确认有足够物理余量并评估业务影响后使用。
修改 vm.swappiness、禁用 Swap 或用 mlockall() 锁页也都属于容量与延迟策略,不是看到 Swap 告警后的通用修复。数据库、Kubernetes 节点、休眠系统和通用服务器的取舍并不相同。
十、缺页异常和内存访问延迟
1. sar -B:观察系统分页活动
sar -B 1 10
-B 报告分页统计,常见字段包括每秒 page fault、major fault、页面扫描和回收效率;字段会随 sysstat 版本变化,先用 man sar 对照本机含义。
判断方式:
fault/s高但majflt/s低:可能是正常的匿名页首次触碰或页缓存命中。majflt/s高且磁盘读、Swap 换入同步上升:内存访问被存储 I/O 拖慢。- 页面扫描/回收持续很高且 PSI 上升:系统存在回收抖动。
2. pidstat -r:把缺页归因到进程
pidstat -r -p ALL 1
pidstat -r -p 1234 1
先由 sar -B 确认系统级分页异常,再用 pidstat 找 majflt/s 高的进程。可能根因包括:
- 服务冷启动加载大量动态库、模型或索引。
- 内存映射文件的工作集超过内存。
- 进程页面刚从 Swap 换回。
- 随机访问模式造成页缓存命中率低。
如果主缺页来自文件映射,结合 pmap -x 找映射文件,使用缓存和存储工具确认;如果来自 Swap,回到上一节找 VmSwap 和换入速率。
3. perf stat:检查页错误和 TLB miss
perf stat -e page-faults,minor-faults,major-faults,context-switches ./your_program
perf stat -e dTLB-loads,dTLB-load-misses,iTLB-loads,iTLB-load-misses -p 1234 -- sleep 30
第一条在程序完整运行期间计数;第二条附加 PID 采样 30 秒。TLB 事件名称和硬件支持依 CPU、内核而异,可用 perf list | grep -i tlb 查看本机可用事件。
TLB miss 高时再考虑访问局部性、数据结构布局、Huge Page 或线程/NUMA 亲和性。大页是验证后的优化方向,不是发现缺页异常后的第一步,因为缺页异常和 TLB miss 不是同一个概念。
十一、OOM:先读日志,再判断是整机还是容器
1. 查看内核 OOM 日志
journalctl -k -g 'Out of memory|Killed process|oom-kill' --since '1 hour ago'
dmesg -T | grep -Ei 'out of memory|killed process|oom-kill'
journalctl -k:只看内核日志。-g:按正则过滤,较旧 systemd 可能不支持,可改用管道grep。--since:限制时间窗口。dmesg -T:将内核时间戳转为可读时间;容器内或普通用户可能无权限。
日志重点读:触发分配的进程、被杀进程、oom_score_adj、内存与 Swap 状态、是 global OOM 还是 memory cgroup OOM。被杀进程不一定就是泄漏源,它只是 OOM 算法在当时选出的 victim。
2. 查看和调整 OOM 分值
cat /proc/1234/oom_score
cat /proc/1234/oom_score_adj
oom_score 是内核动态计算结果;oom_score_adj 范围为 -1000 到 1000,越大越容易被杀,-1000 基本禁止选择。老资料中的 /proc/PID/oom_adj 已过时,现代系统使用 oom_score_adj。
不要随意把关键进程都设为 -1000。如果所有大进程都受保护,内核可能杀死更不合适的进程,甚至让系统无法恢复。正确做法是先设置合理容量、限制和优雅降级,再谨慎调整优先级。
3. 容器 cgroup v2 内存排查
先确认进程所在 cgroup:
cat /proc/1234/cgroup
进入对应 cgroup 目录后查看:
cat memory.current
cat memory.max
cat memory.high
cat memory.swap.current
cat memory.swap.max
cat memory.events
cat memory.stat
cat memory.pressure
memory.current:当前 cgroup 内存使用。memory.max:硬上限;max表示无限制。memory.high:软节流阈值,超过后任务会承受回收压力。memory.events:high、max、oom、oom_kill等计数。计数增长比单次快照更关键。memory.stat:拆分 anon、file、kernel、slab、pagetables、workingset 等。memory.pressure:该 cgroup 的 PSI 内存压力。
宿主机 free 很充足但容器 oom_kill 增长,说明问题是容器限制而不是整机容量。Kubernetes 还应结合 Pod requests/limits、退出码 137、OOMKilled 状态和节点 eviction 事件。
cgroup v1 文件名不同,常见为 memory.usage_in_bytes、memory.limit_in_bytes、memory.failcnt。先用 stat -fc %T /sys/fs/cgroup 判断环境,不要混用两套文件。
十二、PSI:内存还有余量但业务已经卡顿
cat /proc/pressure/memory
watch -n 1 'cat /proc/pressure/memory'
示例字段包括:
some:至少一个任务因内存压力停顿的时间比例。full:所有非空闲任务同时因内存压力停顿的比例,通常更严重。avg10/avg60/avg300:过去 10、60、300 秒的平均压力。total:累计停顿微秒数。
PSI 衡量的是任务因回收、压缩或内存等待而失去运行机会的时间,不是内存使用百分比。available 尚未归零但 PSI 持续升高,说明系统可能已经在频繁回收,业务延迟会先于 OOM 恶化。
证据链可以是:memory.pressure 上升 -> sar -B 页面扫描和主缺页上升 -> vmstat si/so 或 I/O 活跃 -> 找到高 RSS、缓存 miss 或 Swap 进程。
十三、按现象选择命令
| 现场现象 | 第一层确认 | 第二层定位 | 最终归因 |
|---|---|---|---|
free 很小 |
free -h 看 available |
/proc/meminfo 拆 anon/cache/slab |
正常缓存或真实压力 |
| available 持续下降 | vmstat 1、sar -r |
pidstat -r、top |
smaps_rollup、pmap |
| 某进程 RSS 增长 | pidstat -r -p PID 1 |
status、smaps_rollup、pmap -x |
memleak/Valgrind/ASan |
| buff/cache 很大 | /proc/meminfo |
Cached 用 cachestat/cachetop;Slab 用 slabtop | 文件访问或内核对象 |
| Swap used 很大 | vmstat 1、sar -W 看是否活跃 |
smem -s swap、VmSwap |
进程、NUMA、cgroup、水位 |
| 主缺页高 | sar -B 1 |
pidstat -r -p ALL 1 |
映射文件、冷启动或 Swap |
| OOM | journalctl -k、dmesg |
读 global/memcg 和 victim | 泄漏、限制或容量规划 |
| 宿主机空闲但容器 OOM | memory.current/max/events |
memory.stat/pressure |
limit、工作集、缓存和并发 |
| Slab 持续增长 | SUnreclaim 趋势 |
slabtop -o |
内核对象、网络或驱动 |
| 内存不满但延迟抖动 | /proc/pressure/memory |
sar -B/-W、vmstat |
direct reclaim、Swap、压缩 |
| VIRT 巨大、RES 正常 | top/ps |
pmap -x |
地址预留或文件映射,未必异常 |
十四、四类典型故障的完整闭环
场景 1:C++ 服务内存泄漏
free -h
vmstat -w 1
pidstat -r -p ALL 1
grep -E 'Rss|Pss|Anonymous|Swap' /proc/1234/smaps_rollup
pmap -x 1234
sudo /usr/share/bcc/tools/memleak -p 1234
证据链:available 持续下降 -> 进程 RSS 随时间单调增长且吞吐稳定 -> PSS 和匿名私有页增长 -> [heap] 或匿名映射增长 -> 同一分配调用栈的未释放字节持续增加。最后才能定性为泄漏,并回到对象所有权或异常路径修复。
场景 2:文件页缓存挤压内存
free -h -w
grep -E 'Cached|Dirty|Writeback|Slab|SReclaimable' /proc/meminfo
vmstat -w 1
sudo cachestat 1 10
sudo cachetop 5
pidstat -d -p ALL 1
证据链:buff/cache 增长 -> /proc/meminfo 确认主要是 Cached 而非 SUnreclaim -> 页缓存 miss/dirty 活跃 -> cachetop 找到文件访问进程 -> pidstat -d 和业务日志确认大文件扫描、备份或日志任务。若 available 仍充足且没有回收压力,这可能只是正常利用内存,不需要“清缓存”。
场景 3:Swap 抖动造成延迟
vmstat -w 1
sar -W 1 10
cat /proc/pressure/memory
sudo smem -r -k -s swap
numastat -m
证据链:si/so 持续非零 -> PSI 和磁盘 I/O 同期上升 -> 找到 Swap 较多的进程 -> 判断是进程工作集过大、NUMA 局部不足、容器限制还是历史冷页。修复可能是减少工作集、限制并发、调整容量/NUMA 策略;不能先执行 swapoff -a 掩盖根因。
场景 4:容器被 OOMKill,但宿主机内存足够
cat /proc/1234/cgroup
cat /sys/fs/cgroup/<path>/memory.current
cat /sys/fs/cgroup/<path>/memory.max
cat /sys/fs/cgroup/<path>/memory.events
cat /sys/fs/cgroup/<path>/memory.stat
cat /sys/fs/cgroup/<path>/memory.pressure
证据链:宿主机 available 正常 -> cgroup 的 oom_kill 计数增加 -> memory.current 接近 memory.max -> memory.stat 判断 anon、file 或 kernel 主导 -> 再定位进程和请求模式。解决方向是修泄漏、控制并发/缓存、合理设置 limit 和 request,而不是给宿主机盲目加内存。
十五、线上排障最容易犯的错误
- 把
free列小当作内存不足。 优先看available、回收、Swap 和 PSI。 - 把 VIRT 当作物理占用。 物理压力看 RSS/PSS,地址构成看
pmap/smaps。 - 把所有进程 RSS 相加。 共享页会重复,汇总优先使用 PSS。
- 只看 Swap 已用量。 性能影响看
si/so、pswpin/s、I/O 和压力趋势。 - 看到 buff/cache 大就 drop caches。 缓存是性能机制;清缓存会制造冷启动和 I/O 峰值,而且不能修复泄漏。
- 线上直接
swapoff -a。 强制换入可能造成 OOM 和严重抖动。 - 只采样一个时刻。 泄漏、回收和 Swap 必须看时间序列,并与业务负载对齐。
- 忽略分配器缓存。
free()后 RSS 不降可能是 allocator 保留 arena,不必然是对象仍不可达。 - 忽略容器和 NUMA 边界。 整机有余量,不代表 cgroup 或本地节点有余量。
- 在生产直接跑高开销工具。 Valgrind、完整 smaps 扫描、无过滤 eBPF 都应评估开销和权限。
echo 3 > /proc/sys/vm/drop_caches 只会丢弃可回收缓存,并不会解决匿名内存泄漏;执行前还常伴随 sync,会放大 I/O。它适合受控基准测试,不是生产告警的修复按钮。
十六、面试高频问答
1. 为什么 Linux 的 free 很小却不一定内存不足?
Linux 会把空闲内存用于页缓存和可回收内核缓存。free 只表示完全未使用页,available 还估算了可回收缓存,更能表示新负载余量。
2. VIRT、RSS、PSS 有什么区别?
VIRT 是全部虚拟地址空间;RSS 是驻留物理页但共享页会重复;PSS 把共享页按进程平摊,统计多进程总物理成本时更合理。
3. minor fault 和 major fault 有什么区别?
两者都表示页表映射未满足访问。minor fault 不需要存储 I/O,例如匿名页首次分配或页已在缓存;major fault 需要从文件或 Swap 读入,延迟高得多。
4. 内存释放后 RSS 为什么不下降?
用户态分配器可能保留已释放块以便复用;页面仍可能留在 arena,或只有部分页可归还。先用堆分析器确认对象是否不可达,再区分真实泄漏与分配器缓存/碎片。
5. 如何排查内存泄漏?
先用 free/vmstat 确认系统余量下降,用 pidstat -r 找增长进程,用 smaps_rollup/pmap 确认匿名私有映射增长,再用 memleak、Valgrind 或 LSan 找持续增长的分配栈,最后修复对象生命周期。
6. Swap 使用高就一定有性能问题吗?
不一定。Swap used 是存量;没有持续换入换出时,可能只是冷页。要结合 vmstat si/so、sar -W、磁盘延迟和 PSI 判断当前是否抖动。
7. OOM 为什么杀了看起来不是最大的进程?
内核根据可杀内存、oom_score_adj、cgroup 边界等综合选择 victim。被杀者不一定是根因,必须读完整 OOM 日志并查看限制和内存增长来源。
8. Buffer 和 Cache 有什么区别?
实务上,Buffers 主要与块设备缓冲/元数据有关;Cached 主要是普通文件页缓存;free 的 buff/cache 还可能包含可回收 Slab。现代内核不要只靠“Buffer 写、Cache 读”的口诀判断。
十七、最后的命令速查
# 系统总览
free -h -w
vmstat -w 1
grep -E 'MemAvailable|Cached|AnonPages|Slab|Dirty|SwapFree' /proc/meminfo
cat /proc/pressure/memory
# 进程内存与缺页
top
ps -eo pid,ppid,vsz,rss,pmem,comm --sort=-rss | head
pidstat -r -p ALL 1
grep -E 'Rss|Pss|Anonymous|Swap' /proc/PID/smaps_rollup
pmap -x PID
sudo smem -r -k -s pss
# 缓存和 Slab
sudo cachestat 1 10
sudo cachetop 5
sudo slabtop -o
# Swap 和分页
swapon --show
sar -S 1 10
sar -W 1 10
sar -B 1 10
sudo smem -r -k -s swap
# 泄漏
sudo /usr/share/bcc/tools/memleak -p PID
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all ./app
# OOM
journalctl -k -g 'Out of memory|Killed process|oom-kill' --since '1 hour ago'
cat /proc/PID/oom_score
cat /proc/PID/oom_score_adj
# 容器 cgroup v2
cat memory.current
cat memory.max
cat memory.events
cat memory.stat
cat memory.pressure
# NUMA
numastat -m
numastat -p PID
grep -E 'Node|zone|pages free|min|low|high' /proc/zoneinfo
面试回答内存排障题时,先说清楚你要区分的是匿名内存、文件页缓存、内核 Slab、Swap、缺页、OOM 还是 cgroup 限制;再用总量、趋势和进程指标逐层排除;最后用映射分布或分配调用栈落到代码。命令只是取证工具,完整证据链才是性能分析能力。
评论
评论由 GitHub Discussions 托管。