上周四下午快下班的时候,收到告警,线上一个订单服务的Pod频繁重启。
一开始以为是GC的问题,因为之前也遇到过类似的情况——堆内存不够,Full GC频繁,最后OOM。所以第一反应是去看Grafana上的JVM监控。
结果一看,堆内存使用率才60%左右,GC频率也正常。这就奇怪了。
然后去看Pod的events,发现重启原因写的是OOMKilled。
这就很迷惑了——JVM自己没报OOM,但是容器被kill了。
后来才反应过来,这是容器层面的OOM,不是JVM层面的。也就是说,整个Pod的内存使用超过了K8s配置的resources.limits.memory,被cgroup直接干掉了。
去查了一下这个服务的部署配置:
resources: limits: memory: 2Gi cpu: 1000m requests: memory: 1Gi cpu: 500m容器限制是2G。再看JVM启动参数:
-Xms1536m -Xmx1536m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m问题就出在这儿了。
堆设了1.5G,Metaspace最大512M,再加上线程栈、直接内存、JVM自身开销这些,实际进程占用的内存早就超过2G了。
之前一直觉得只要-Xmx不超过容器限制就没事,但实际上JVM进程的内存远不止堆这一块。
几个容易忽略的内存占用:
- 线程栈:默认每个线程1M(
-Xss),如果线程数多,这块占用不小。我们那个服务线程池配了200个核心线程,光线程栈就200M了 - 直接内存(Direct Memory):用了Netty做网络通信,直接内存默认最大等于
-Xmx,又是一大块 - Metaspace:加载的类多的话,这块也不小
- JVM自身:Code Cache、GC数据结构等,大概几十到上百M
所以一个粗略的估算公式:
JVM进程总内存 ≈ 堆内存 + Metaspace + 线程栈(线程数 × Xss) + 直接内存 + JVM自身开销按我们那个配置算一下:1536 + 512 + 200 + 1536(直接内存)+ ~100 ≈ 3.8G,远超容器的2G限制。
改法也很简单,两步:
- 把
-Xmx调小,给非堆内存留够空间 - 显式限制直接内存大小:
-XX:MaxDirectMemorySize=256m
改完之后的参数:
-Xms1024m -Xmx1024m \ -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=384m \ -XX:MaxDirectMemorySize=256m \ -XX:+UseG1GC \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/data/logs/heapdump.hprof重新算一下:1024 + 384 + 200 + 256 + ~100 ≈ 1.96G,在2G限制内,留了一点余量。
上线之后观察了两天,没再出现OOMKilled的情况。
踩坑总结:
- 容器环境下,
-Xmx绝对不能设到接近容器内存限制的值 - 建议
-Xmx不超过容器limits.memory的60%~70%,剩下的留给非堆和系统开销 - 如果用了NIO或者Netty,一定要显式设置
-XX:MaxDirectMemorySize - 排查容器OOM的时候,不要只看JVM的GC日志,还要看
/sys/fs/cgroup/memory/下面的统计信息
其实这个问题不算新,但每次换团队、换项目的时候,总能看到有人踩这个坑。写下来记录一下,也希望能帮到遇到同样问题的人。