容器内存一路涨到 OOM,我没改一行业务代码,靠 eBPF 把"贼"揪出来了
我是 Echo_Wish。前阵子半夜被叫起来,一个跑在 K8s 里的服务每隔几个小时就被 OOMKilled 一次,重启完内存又从头爬坡,像西西弗斯推石头。
业务方的第一反应是:"能不能给个 heap dump?" 我说,这是 Python 加 C 扩展的混合服务,heap dump 只能看到 Python 那一层,真正涨的地方它看不见。
这就是我今天想聊的:很多时候,你没法直接进应用里排查,那就绕着走,从旁边的系统层面把它盯死。 eBPF 干的就是这个活。
第一步:先别急着上工具,先问"涨的是哪块内存"
我见过太多人一上来就 memleak,结果查了半天发现压根不是泄漏。先看容器的内存到底怎么涨的:
# 找到容器在宿主机上的 PID
PID=$(crictl inspect --output go-template --template '{
{.info.pid}}' <container_id>)
# 看 cgroup 层面的内存构成
cat /sys/fs/cgroup/kubepods.slice/.../memory.stat | egrep "^(anon|file|slab|sock) "
# 看进程自己的 RSS 变化
while true; do grep -E "VmRSS|RssAnon" /proc/$PID/status; sleep 10; done
结果很关键:anon 持续涨,file 和 slab 基本不动。这说明是匿名内存在涨,也就是堆和 mmap 出来的那种,page cache 没背锅。方向对了,才轮到 eBPF 登场。
第二步:不碰应用,从系统调用层面"数人头"
应用层我进不去,但应用要内存,总得向内核要。内核这扇门,eBPF 守得住:
bpftrace -e '
tracepoint:syscalls:sys_enter_mmap /pid == '$PID'/ {
@mmap_bytes[ustack(6)] = sum(args->len);
}
tracepoint:syscalls:sys_enter_munmap /pid == '$PID'/ {
@munmap_bytes = sum(args->len);
}
interval:s:60 { exit(); }
'
跑一分钟,对比 mmap 和 munmap 的量。我当时看到的是:申请的远多于释放的,而且绝大部分集中在同一条用户态调用栈上。 栈顶落在一个叫 decode_frame 的函数里,属于一个第三方 C 库。
这就是"间接"的好处:我没有去猜应用逻辑,而是让内核告诉我谁在要内存。
第三步:再用 page fault 交叉验证
只看 mmap 有个坑:申请了不代表用了。真正吃掉物理内存的是缺页中断:
bpftrace -e '
software:page-faults:1 /pid == '$PID'/ {
@faults[ustack(8)] = count();
}
interval:s:30 { exit(); }
'
两份数据的热点栈对上了,我才敢下结论。单一证据别轻易定罪,两条独立线索指向同一个人,才叫实锤。
第四步:最后上 memleak 确认"只借不还"
/usr/share/bcc/tools/memleak -p $PID -T 5 60
输出里 decode_frame 那条栈,未释放字节数一直在涨。回头翻代码,原来是一个异常分支提前 return,漏掉了 free(buf)。线上遇到畸形数据才会走进去,所以泄漏很慢,慢到你以为是"正常增长"。
一个容易被忽略的反面案例
另一个服务我也遇到过 RSS 暴涨,同样的流程跑下来,发现 malloc 和 free 是基本平衡的。罪魁祸首是 glibc 的 arena 碎片:多线程下每个线程各占一个 arena,释放的内存还回不去操作系统。
# 不改代码,一个环境变量就能验证
MALLOC_ARENA_MAX=2
加上之后内存曲线立刻平了。这类问题如果你一上来就认定"泄漏",会白白追几天。
聊点心里话
干了这么多年运维,我越来越觉得:
- 排查的本质是缩小范围,不是堆工具。 从 cgroup 到进程、到系统调用、到调用栈,每一步都在排除一大片可能性。
- eBPF 最大的价值是"无侵入"。 不重启、不改代码、不加 agent,生产环境上敢动手,这点比炫酷的火焰图重要得多。
- 别迷信单一指标。 RSS、mmap、page fault、memleak 互相印证,才不容易冤枉好人。
- "泄漏"和"涨"不是一回事。 缓存、碎片、延迟释放都会让曲线好看不到哪去,分清楚再动手。
最后说一句,工具永远是第二位的。真正值钱的是你面对一条诡异曲线时,脑子里那套"先问是什么涨、再问谁要的、最后问谁没还"的排查顺序。把这个顺序练熟了,换成别的工具也一样好使。
我是 Echo_Wish,下次再聊。