一次生产环境 OOMKilled 告警引发的深度排查:从 JVM 内存到代码漏洞的全链路分析
"在分布式系统的复杂世界里,每一个看似无害的告警,都可能是一条通往问题核心的线索。”
Intro - 新的挑战
几个月前,秉着对 backend engineering 的热爱,我加入了公司的 chatbot 团队,负责一个 Java 项目 bot-gateway 的开发。有意思的是,我们的服务在过去几个月里一直不稳定,每天总有 Kubernetes Pods 时不时地重启。不过由于业务繁忙,这个问题一直被搁置。更有意思的是,作为一名新加入的 Java 小兵,在狂奔开发业务功能两个月后,终于赶上了 Engineering Excellence Sprint. 怀着对技术问题的好奇心和解谜的热情,我主动请缨深入调查这个问题。
排查过程充满挑战,但最终的结果令我非常满意。可以说这次从业务代码到 JVM 内存模型,从 k8s pod 管理到线上监控告警,都详细捋了一遍。在此将整个过程记录下来,与大家分享。
一 / 突如其来的 Pod 重启
一切都始于一个看似平静的夜晚,我们的告警系统突然被触发:
[FIRING:1] Container has been restarted. Reason: OOMKilled
这个告警来自我们的核心服务之一 bot-gateway
。
OOMKilled
这个词对任何 SRE 或开发工程师来说都意味着麻烦。研究了下,它表示容器因内存耗尽而被其宿主(Kubernetes 节点)无情地杀死。而且更恼火的是,这一切发生得如此突然且暴力, 应用层根本来不及留下任何有价值的“遗言”,没有应用层日志,而容器日志中也只有寥寥数语:
Task ... ran out of memory
... deleted with exit code 137
exit code 137
表示进程收到了SIGKILL
信号而被终止。
服务一次次地重启,影响了用户体验,也给团队带来了巨大压力。于是我开始了深入调查之旅。
二 / 理论基础准备
之前对 JVM 并不熟悉,正好借此机会,夯实我的理论基础。
这里两个核心概念的区分至关重要:
OOMKilled
vs java.lang.OutOfMemoryError
OutOfMemoryError
(OOM Error): 这是 JVM 的“内部矛盾”。JVM发现自己的**堆内存(Heap)**不足,会主动抛出异常。这是一个相对“优雅”的失败方式。OOMKilled
: 这是容器的“外部冲突”。当 Kubernetes 发现整个容器的总内存占用(堆内存 Heap + 非堆内存 Non-Heap 和原生内存 Native memory 等)超过了设定的上限 (limits.memory
),为了保护整个节点的稳定,操作系统会像一个无情的城管,会强制将这个失控的进程(容器)驱离。
JVM内存模型简介
- 堆内存 (Heap Memory):存储代码中
new
出来的所有对象实例,被分为新生代 New Gen 和老年代 Old Gen。对象在新生代中诞生,经历数次垃圾回收(GC)后依然存活的,会被晋升到老年代。如果老年代持续增长且无法回收通常意味着内存泄漏 - 非堆内存 (Non-Heap Memory):在 Java 8 及以后版本中,主要指元空间(Metaspace),存储类的定义、方法、字段等元数据。元空间持续增长通常表明类加载器泄漏(ClassLoader Leak)
- (TODO 补链接到另一篇 JVM 内存 blog)
当然,我一开始也并不清楚到底是容器配置有问题(比如这个 Java 应用就是需要这么大的内存,申请的机器内存 1G 过小),还是咱的应用代码有问题,只能硬着头皮开始淌水。