我在工作中,曾经处理过一个 customer ticket,这个 ticket 花费了我几天的时间进行调查和处理,那几天真的是吃不好,睡不香。
问题解决之后,我想着一定要把问题的分析过程写下来,方便自己日后查看。也说不定可以帮助其他小伙伴们避免入坑呢?
技术环境
resources.requests.memory: 256Mi
,resources.limits.memory: 512Mi
,镜像里 JVM 未显式配置堆大小STATUS
为 OOMKilled
线上告警提示接口延迟飙升,kubectl get pods
看到服务实例反复重启,STATUS
栏位出现 OOMKilled
。kubectl describe pod
的 Last State
明确写着:
State: Terminated
Reason: OOMKilled
Exit Code: 137
Exit Code 137
代表进程被 SIGKILL
强杀,常见诱因是超出容器内存 limit
而触发 Linux OOM Killer,不过也需要结合 describe
的 Reason
来确认是否真是内存杀进程。(Stack Overflow)
应用日志里还能捞到一次完整的 Java OutOfMemoryError
堆栈(节选自实际可复现的同类报错形态,线程名用反引号标注):
Exception in thread `pool-2-thread-170` java.lang.OutOfMemoryError: Java heap space
at scala.meta.internal.semanticdb.SymbolOccurrence$.parseFrom(SymbolOccurrence.scala:118)
at scalapb.LiteParser$.readMessage(LiteParser.scala:24)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1623)
这类 Java heap space
异常的含义是:JVM 在堆上无法再分配对象,GC 也无法回收出足够空间,或堆已达上限。
1)先证实是 OOMKilled
而不是别的 137 场景
exit code 137
并不总是内存杀,也可能是被外部 SIGKILL
。对比 kubectl describe pod
的容器 State/Reason
字段,如果明确显示 Reason: OOMKilled
,即可坐实。
2)确认调度与 QoS 是否暗藏风险
requests
是调度参考线,limits
是上限。内存 limit
的执行是内核层的 cgroup OOMKill;当容器突破 limit
,在节点有内存压力时会被杀。若 request
远低于真实需求,调度器可能把过多 Pod 挪到同一节点,整体更易内存打架。
3)复盘 JVM 行为与容器限制的错位
现代 JVM 会感知 cgroup 限制,但若未合理设置 -XX:MaxRAMPercentage
/ -Xmx
,实际可用堆可能与 K8s limit
脱节:
limit: 512Mi
,而 JVM 默认可能在峰值时让总占用逼近甚至略超 limit
,被 OOM Killer 秒杀。业界常见实践是将堆上限设为 limit
的一部分,例如 60%~75%,留出 native/线程等开销。(Fairwinds)4)证据链
describe
:容器 Reason: OOMKilled
。137 = 128 + 9(SIGKILL)
,结合 Reason
可判定为 OOM。java.lang.OutOfMemoryError: Java heap space
堆栈。limit
后出现重启尖峰,属典型症状。OOMKilled
的 Java
容器Java 程序
import java.util.ArrayList;
import java.util.List;
public class MemoryEater {
public static void main(String[] args) throws Exception {
List<byte[]> bag = new ArrayList<>();
final int mb = 8;
while (true) {
bag.add(new byte[mb * 1024 * 1024]);
Thread.sleep(150);
}
}
}
Dockerfile(多阶段构建,尽量精简镜像体积):
# build
FROM eclipse-temurin:17-jdk-jammy AS build
WORKDIR /src
COPY MemoryEater.java .
RUN javac MemoryEater.java
# run
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY --from=build /src/MemoryEater.class /app/
# 不显式设置 Xmx,模拟很多老镜像里常见的默认配置问题
ENTRYPOINT java MemoryEater
触发 OOM 的清单(memory-eater-oom.yaml
):
apiVersion: apps/v1
kind: Deployment
metadata:
name: memory-eater
spec:
replicas: 1
selector:
matchLabels:
app: memory-eater
template:
metadata:
labels:
app: memory-eater
spec:
containers:
- name: eater
image: your-registry/memory-eater:demo
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 256Mi
cpu: 500m
部署后观察一段时间,你会看到 STATUS
变为 OOMKilled
,describe
里 Reason: OOMKilled
,退出码 137
。
limit
与 requests
真正对齐思路一:显式收敛 JVM 堆上限到容器 limit
的安全比例
JDK 10 以后默认支持容器感知,可用 -XX:MaxRAMPercentage
控制堆上限占比,并配合 -XX:+HeapDumpOnOutOfMemoryError
等参数提升可观测性。也可以增加容器 limit
,但在混部场景要避免一味加大导致节点资源紧张。
修复版清单(memory-eater-fixed.yaml
):
apiVersion: apps/v1
kind: Deployment
metadata:
name: memory-eater
spec:
replicas: 1
selector:
matchLabels:
app: memory-eater
template:
metadata:
labels:
app: memory-eater
spec:
containers:
- name: eater
image: your-registry/memory-eater:demo
env:
- name: JAVA_TOOL_OPTIONS
value: >-
-XX:MaxRAMPercentage=60
-XX:InitialRAMPercentage=60
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/heap/dump.hprof
-XX:+ExitOnOutOfMemoryError
volumeMounts:
- name: heap
mountPath: /heap
resources:
requests:
memory: 384Mi
cpu: 200m
limits:
memory: 512Mi
cpu: 1
volumes:
- name: heap
emptyDir: {}
MaxRAMPercentage=60
让堆保持在 limit
的 60% 左右,剩余 40% 给元空间、线程栈、直接内存与 JIT 等。ExitOnOutOfMemoryError
让 JVM 失败即退出,避免僵尸进程;HeapDumpOnOutOfMemoryError
保留现场用于定位。对此类参数,也可通过 -XX:OnOutOfMemoryError
搭配 jstack
自动抓取线程信息。思路二:把 requests
设成可信的基线
requests.memory
设为 P95
甚至 P99
的实测值。limits.memory
给出合理头寸(例如 requests
的 1.2~1.5 倍)。Guaranteed
(requests == limits
)以降低被驱逐的概率,但需要确保节点层面也留好缓冲。资源模型与内核 OOMKiller 行为在官方文档里有清晰解释。思路三:避免把 137 与 OOMKilled
生搬硬套
如果只看到 137
而没有 Reason: OOMKilled
,需要继续排查是否是探针超时、手动 kill -9
、节点压力等引发的 SIGKILL
。
kubectl describe
中的 Reason: OOMKilled
与 Exit Code: 137
是诊断锚点。Java heap space
堆栈如前文代码块所示,可作为模式匹配参考。OOMKilled
事件在监控/通知里的呈现方式(Slack 报警与内存曲线)。构建镜像
# 构建与推送
docker build -t your-registry/memory-eater:demo .
docker push your-registry/memory-eater:demo
部署与验证
# 先部署触发 OOM 的版本观察
kubectl apply -f memory-eater-oom.yaml
kubectl get pods -w
kubectl describe pod -l app=memory-eater
# 替换为修复版本
kubectl apply -f memory-eater-fixed.yaml
kubectl rollout status deploy/memory-eater
kubectl logs -l app=memory-eater --tail=100
Java 生态经常叠加 Netty 直接内存、线程池、Metaspace、ClassLoader 动态加载、JIT 代码缓存等多源开销;只把 Xmx
当作总内存的代名词,往往会在容器内被打脸。官方与行业资料对 OOMKilled
、OutOfMemoryError
有更系统的解释与处置建议,可用作学习清单:
JAVA_TOOL_OPTIONS
固化 -XX:MaxRAMPercentage
、-XX:+ExitOnOutOfMemoryError
、-XX:+HeapDumpOnOutOfMemoryError
,别把 Xmx
留成默认。requests
比实际基线低太多,也不要把 limits
卡得过死;根据真实流量的 P95/P99
去定数,而不是拍脑袋。exit code 137
不是判定 OOM 的全部证据,务必结合 Reason: OOMKilled
与事件时间线交叉验证。limit
;升级版本后留意曲线形态是否突变。Invisible OOM
:子进程被杀而 init
存活,K8s 侧不易察觉。为关键路径加守护与自检。OOMKill
事件、节点内存压力,配合日志侧 OutOfMemoryError
关键字聚合。OOMKilled
综述与排查步骤,包含典型命令与现象汇总。(Komodor)137
退出码与 SIGKILL
的关系与判读。(Groundcover)Java heap space
异常的成因与处理。(Oracle Documentation)这次事故的根源并不玄妙:当 JVM 的贪婪与 K8s 的刚性上限相遇,就会在曲线顶端撞一次墙。让 requests
、limits
与 JVM 的内存策略彼此知根知底,才是在容器里跑 Java 的底层秩序。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。