开放问答
K8s Java 17 容器被 OOMKill 但 JVM 日志显示堆未打满,cgroup v2 下该按什么顺序排查 native 内存?
该问题的正文、回答与评论。
提问者 苏星河 · 状态 open
KubernetesJVMOOMKillcgroup-v2内存排查容器化
背景
我在一个 12 节点(16C64G,Kubernetes 1.28.4,containerd 1.7.13,cgroup v2,内核 5.15)的生产集群上跑一套 Java 17(OpenJDK 17.0.9)的无状态订单服务。每个 Pod 规格是 8C32G(request=8C/32G,limit=8C/32G),容器内没有 sidecar,单容器。最近两周频繁出现 Pod 被 OOMKilled(kubectl describe pod 显示 Reason: OOMKilled, Exit Code: 137),但诡异的是:JVM 自己的 GC 日志里 heap 使用率最高只到 78%,-Xmx 设的是 18G,jmap -heap 在 OOM 前一刻抓的 dump 也显示 heap 还有 4G+ 余量。
现象
- 被 OOMKill 的 Pod 里,
/sys/fs/cgroup/memory.current在崩溃前几分钟能爬到 31.5G+,逼近 limit; - 但 JVM heap(
-Xmx 18G)只用了 ~14G,堆内还有 4G 没被 GC 回收(因为还没到 GC 触发点); - 也就是说 cgroup 视角的内存 ≈ 31.5G,JVM heap 视角 ≈ 14G,中间差出 ~17G——这部分我怀疑是 native 内存(Metaspace、线程栈、DirectByteBuffer、JNI、glibc arena 碎片),但没法定位到具体是哪一块;
- 同一个镜像在 1.24 节点(cgroup v1)上跑三个月没出过 OOM,迁移到 1.28 + cgroup v2 后开始频繁翻车,我怀疑和 cgroup v2 的 memory.oom.group / memory.high 语义变化有关,但不确定。
已尝试的排查与结果
- 把
-Xmx从 22G 降到 18G,-XX:MaxMetaspaceSize=1G、-XX:MaxDirectMemorySize=2G、-XX:ReservedCodeCacheSize=512m都显式设了,跑了三天还是 OOMKill,频率从每天 2 次降到每天 1 次,没根治; - 开了
-XX:NativeMemoryTracking=detail,OOM 前一刻jcmd <pid> VM.native_memory summary抓到的输出:Java Heap 13.8G、Metaspace 0.9G、Thread 1.2G(~600 个线程 × 1M 栈)、Code 0.4G、GC 0.8G、Internal 0.3G、Symbol 0.1G、Other 0.2G,合计 ~17.7G——和 cgroup 看到的 31.5G 还差 ~13G,NMT 没把这部分算进去; - 用
pmap -x <pid> | awk '{s+=$2} END {print s}'看 RSS,确实比 NMT 合计大不少,怀疑 glibc malloc arena 碎片 + DirectByteBuffer 的 off-heap 部分没被 NMT 覆盖; - 查了
-XX:+UseContainerSupport(Java 17 默认开启),jcmd VM.info里MaxHeapSize识别正确(18G),所以不是容器感知失效的问题; - 看了
dmesg,OOMKill 的调用栈是memory.oom_group触发,不是内核 OOM killer 单进程 kill,说明是 cgroup 级别整体超限,不是某个进程单独爆的。
期望得到的答案形态
- NMT 没覆盖到的那 ~13G 内存,最可能的来源排序是什么?(glibc arena 碎片 / DirectByteBuffer off-heap / JNI / 内核 page cache / 其他)怎么逐个验证?
- 有没有一套可复现的排查命令序列,能在 OOMKill 前 1–2 分钟抓到「cgroup 内存 vs JVM heap vs native 内存」三方对账?(比如用
memory.stat的file/anon/kernel字段拆分) -XX:MaxDirectMemorySize在 Java 17 下到底约束的是sun.nio.ch.DirectByteBuffer的 allocated 还是 reserved?如果业务用了 Netty 的PlatformDependent绕过了 DirectByteBuffer,这个参数还生效吗?- cgroup v2 下
memory.high(soft limit)和memory.max(hard limit)对 JVM 的行为差异?有没有人试过把memory.high设成 limit 的 90%,让内核在 OOM 前就把 page cache 压回去,避免直接 OOMKill? - 最后想问一个成本侧的问题:如果最终定位是 glibc arena 碎片,是换 jemalloc 划算,还是干脆把容器内存 limit 从 32G 提到 36G 让内核有缓冲更划算?有没有人做过这个 ROI 对比?
评论
暂无评论。