Linux 内存泄漏排查实战:从 OOM Killer 日志到 valgrind 定位

凌晨三点,监控告警突然响起——某台线上服务器的内存使用率在短短两小时内从 40% 飙升到 98%,随后 SSH 连接超时,业务进程被系统强制杀死。登录带外管理口查看 dmesg,一行熟悉的日志映入眼帘:

Out of memory: Kill process 12345 (myapp) score 892 or sacrifice child
Killed process 12345 (myapp) total-vm:8192000kB, anon-rss:7864320kB

这是典型的 OOM Killer 出手了。但问题在于:为什么 myapp 会吃掉 7.8GB 内存? 是配置不当、突发流量,还是真正的内存泄漏?本文将从 OOM Killer 日志出发,一步步还原一次真实的内存泄漏排查过程,最终用 valgrind 精准定位到问题代码。

一、读懂 OOM Killer 日志:线索藏在细节里

很多人看到 OOM 日志只关注“哪个进程被杀”,但真正有价值的信息在后面的数字:

  • total-vm:8192000kB:进程虚拟内存总量约 8GB
  • anon-rss:7864320kB:匿名页常驻内存约 7.8GB,这是堆内存的直接体现
  • score 892:OOM 评分,越高越容易被杀

关键判断:anon-rss 持续增长且不回落,基本可以排除“缓存占用”或“共享库映射”的干扰,指向堆内存泄漏。如果只是流量高峰导致的短时内存上涨,OOM 后重启进程内存会恢复正常;而泄漏的特征是——重启后内存曲线依然呈阶梯式上升

二、现场保护与初步排查

OOM 发生后,切忌急着重启了事。先做三件事:

1. 检查系统级内存分布

free -m
cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|Slab|SReclaimable"

如果 Slab 异常大,可能是内核对象泄漏;如果 SReclaimable 很小而 MemFree 极低,则确认是用户态进程吃掉了内存。

2. 查看进程内存映射

如果进程还活着(或 core dump 还在),用 pmap 观察堆区:

pmap -x <pid> | sort -k3 -n -r | head -20

重点关注 [heap] 段和大量匿名映射([anon])。如果 [heap] 持续增长,说明 malloc/new 分配的内存没有释放。

3. 确认泄漏速率

while true; do
  ps -o rss= -p <pid> >> /tmp/rss.log
  sleep 60
done

观察 RSS 是否线性增长。如果每小时稳定增长 200MB,那基本可以坐实泄漏。

三、用 valgrind 精准定位泄漏点

确认存在泄漏后,下一步是找到哪一行代码在泄漏。valgrind 的 memcheck 工具是最经典的选择。

1. 编译带调试信息的版本

gcc -g -O0 -o myapp myapp.c

注意:-O0 避免优化干扰行号,-g 保留符号表。

2. 运行 valgrind

valgrind --leak-check=full \
         --show-leak-kinds=all \
         --track-origins=yes \
         --log-file=valgrind.log \
         ./myapp

参数说明:

  • --leak-check=full:显示每个泄漏点的完整调用栈
  • --show-leak-kinds=all:包括 definitely/indirectly/possibly lost
  • --track-origins=yes:追踪未初始化值的来源,对定位有帮助

3. 解读输出

valgrind 报告通常长这样:

==12345== 4,096 bytes in 1 blocks are definitely lost in loss record 12 of 20
==12345==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck.so)
==12345==    by 0x400B2A: create_buffer (buffer.c:45)
==12345==    by 0x400C1F: process_request (handler.c:88)
==12345==    by 0x400D33: main (main.c:22)

关键看 definitely lost——这是确定泄漏。调用栈直接指向 buffer.c:45malloc,而 handler.c:88 调用了它却没有对应的 free

四、生产环境无法跑 valgrind 怎么办?

valgrind 会带来 10~50 倍性能损耗,线上服务不可能直接跑。替代方案:

方案一:使用 AddressSanitizer(ASan)

编译时加 -fsanitize=address -fno-omit-frame-pointer,运行时开销约 2 倍,适合预发环境。泄漏报告格式与 valgrind 类似,但更快。

方案二:tcmalloc/jemalloc 的堆分析

如果程序链接了 tcmalloc,可以通过环境变量开启堆采样:

HEAPPROFILE=/tmp/heap ./myapp

然后用 pprof 分析堆快照,对比两个时间点的增长,直接定位分配热点。

方案三:gdb 附加 + malloc 钩子

对于已经出问题的进程,可以用 gdb attach 后调用 malloc_stats() 查看分配统计,或者用 mtrace 记录分配轨迹。

五、常见泄漏模式与修复

根据经验,Linux C/C++ 程序的内存泄漏集中在几类:

  1. 异常路径未释放trynewcatch 中直接 return 忘了 delete。修复:用 RAII 或智能指针。
  2. 容器只增不减std::vectorstd::map 持续 push_back/insert,从不清理。修复:加定期清理逻辑或改用 LRU 缓存。
  3. 回调注册未注销:对象销毁了,但回调仍被全局管理器持有。修复:析构时反注册。
  4. 循环引用shared_ptr 互相持有。修复:一方改用 weak_ptr

六、总结

内存泄漏排查的完整链路是:

  1. OOM 日志确认 anon-rss 异常增长
  2. pmap / ps 确认泄漏速率和堆区增长
  3. valgrind / ASan / tcmalloc 定位到具体代码行
  4. 修复 + 回归测试,并用监控验证内存曲线是否平稳

记住一个原则:先保护现场,再复现问题,最后才动代码。OOM Killer 杀掉的不仅是进程,还可能抹掉唯一的线索。养成保留 core dump 和日志的习惯,排查效率会提升数倍。

未经允许不得转载:任鹏个人博客 » Linux 内存泄漏排查实战:从 OOM Killer 日志到 valgrind 定位

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏