K8sPodOOM排查:从limits设置到JVM调优

admin 2026-06-30 06:24:25 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该文档系统阐述Kubernetes环境中Java应用Pod出现OOMKilled的完整排查框架,涵盖容器cgroup机制、节点驱逐策略与JVM内存模型三层关联分析。核心指出需区分容器OOM与节点驱逐事件,详解JVM堆内外内存组成及增长特性,提供基于cgroup文件、kubectltop、NMT等工具的实操诊断方法,并给出limits设置原则与MaxRAMPercentage等JVM参数调优建议。 综合评分: 85 文章分类: 云安全,安全运维,实战经验,解决方案,应用安全


cover_image

K8s Pod OOM 排查:从 limits 设置到 JVM 调优

点击关注 👉 点击关注 👉

马哥Linux运维

2026年6月24日 20:44 广东

在小说阅读器读本章

去阅读

K8s Pod OOM 排查:从 limits 设置到 JVM 调优

一、问题背景

在 Kubernetes 上跑 Java 服务,最让人头疼的故障之一就是 Pod 被频繁 OOMKilled。表现通常是这样的:服务跑着跑着突然重启,kubectl get pod 看到重启次数一直在涨,kubectl describe pod 里赫然写着 Last State: Terminated, Reason: OOMKilled, Exit Code: 137。业务侧的反馈是接口偶发超时、长连接掉线、定时任务没跑完就被掐断,甚至整批 Pod 同时被杀导致服务雪崩。

这种问题的难处理之处在于,它往往不是单纯的”内存配少了”。很多同学第一反应是把 limits.memory 往上调,结果调到 8Gi、16Gi 还是 OOM,甚至越调越频繁。原因在于 OOM 的根因可能在三个完全不同的层面:

  • 容器层:cgroup 内存上限被突破,内核 OOM Killer 杀掉进程,Kubernetes 把它标记为 OOMKilled
  • 节点层:节点内存压力大,kubelet 触发按 QoS 等级的驱逐(Eviction),Pod 被驱逐而非容器 OOM。
  • 应用层:JVM 堆内、堆外、Metaspace、直接内存、线程栈、GC 开销任意一项吃满了容器配额,但堆可能并没有满。

把这三层混在一起谈,是排查最容易跑偏的地方。本文把容器内存机制、Kubernetes 资源管理、JVM 内存模型三条线串起来,给一套能照着执行的排查闭环:从看到 OOMKilled 到定位到具体是哪类内存吃满,再到给出对应的 limits 设置和 JVM 参数调优方案,最后给回滚和验证方法。

二、适用场景

本文适用于以下情况:

  • 在 Kubernetes 集群(1.20 及以上版本,cgroup v1 或 cgroup v2 均涉及)上运行 Java 应用(JDK 8u191+ 或 JDK 11/17/21)。
  • Pod 出现 OOMKilled137 退出码、CrashLoopBackOff 或节点驱逐相关告警。
  • 需要从零建立一套 Pod 内存监控与 OOM 排查流程的运维或 DevOps 工程师。
  • 需要为 Java 服务制定合理的 requests/limits 和 JVM 内存参数规范的团队。

不适用的情况:

  • 非 Java 应用(Go、Node、Python 等)的内存泄漏排查,原理部分(cgroup、Kubernetes 驱逐)仍可参考,但 JVM 调优部分不适用。
  • 裸机/虚拟机上 JVM 调优,没有容器 cgroup 约束时,资源模型不同,本文的 limits 部分不直接适用。

前置条件(排查前应具备,否则效率大打折扣):

  • 集群已部署 metrics-server(kubectl top 可用)和 cAdvisor + Prometheus(历史指标可查)。
  • 业务镜像含 JDK 完整工具链或预装 Arthas,能 kubectl exec 进容器跑 jcmd
  • 关键服务已开 NativeMemoryTracking=summary 和 HeapDumpOnOutOfMemoryError
  • 有 node-problem-detector 或等价的宿主 OOM 事件采集。
  • 有可用的 dump 持久卷或足够节点磁盘空间。
  • 团队有 MAT/async-profiler 离线分析能力。

这些前置条件本身就是 OOM 可观测性的基线。如果都不具备,第一件事不是排查某个 OOM,而是先把这套观测能力建起来,否则每次 OOM 都只能靠猜。

三、核心知识点

3.1 容器 OOM 与节点驱逐是两件事

容器被杀有两种来源,必须先分清:

  • 容器 OOM(Container OOMKilled):容器进程内存用量超过 cgroup 的内存上限,由内核 OOM Killer 选中该 cgroup 内得分最高的进程杀掉。kubectl describe pod 会显示 Reason: OOMKilled,退出码 137(128 + SIGKILL 9)。
  • 节点驱逐(Node Eviction):节点整体内存紧张,kubelet 的驱逐管理器根据 --eviction-hard(如 memory.available<100Mi)触发,按 QoS 等级和超出 requests 的程度挑选 Pod 驱逐。被驱逐的 Pod 事件里是 Status: Evicted,而不是 OOMKilled

判断依据:看 kubectl describe pod <pod> -n <ns> 的 Events 和 Last State。OOMKilled 走容器内存这条线;Evicted 且节点上有 MemoryPressure 条件走节点内存这条线。

3.2 cgroup v1 与 cgroup v2 的内存文件

容器内存上限由 cgroup 实现,Kubernetes 1.25 起默认 cgroup v2(取决于节点配置),不同版本文件路径不同:

  • cgroup v1:

  • 上限:/sys/fs/cgroup/memory/memory.limit_in_bytes

  • 当前用量:/sys/fs/cgroup/memory/memory.usage_in_bytes

  • RSS:/sys/fs/cgroup/memory/memory.stat 里的 rsscacheswap 等字段

  • OOM 控制:/sys/fs/cgroup/memory/memory.oom_control

  • cgroup v2:

  • 上限:/sys/fs/cgroup/memory.max

  • 当前用量:/sys/fs/cgroup/memory.current

  • 明细:/sys/fs/cgroup/memory.stat(字段如 anonfileslab 等,对应 v1 的 rss/cache)

  • OOM 事件计数:/sys/fs/cgroup/memory.events(字段 oomoom_kill

  • 峰值:/sys/fs/cgroup/memory.peak

在容器里 cat 这些文件可以拿到内核视角的真实数据,比 JVM 自己算的更权威。不同发行版、不同 K8s 版本挂载路径可能略有差异,重点看 /sys/fs/cgroup/ 下是否存在 memory.max(v2)还是 memory/ 目录(v1)。

3.3 kubectl top 用的是 working set

kubectl top pod 显示的内存是 container_memory_working_set_bytes,不是 container_memory_usage_bytes。两者的关系在 cgroup v1 下大致是:

working_set = usage_in_bytes - total_inactive_file(不可回收的文件缓存部分)

working set 更接近”内核认为不能轻易释放、OOM 时算账的那部分内存”。所以在排查时盯 kubectl top 和 Prometheus 的 container_memory_working_set_bytes 是对的。需要注意,不同 exporter、不同 cgroup 版本下这两个指标的换算口径可能略有差异,以实际 exporter 指标为准,重点观察其相对变化趋势。

3.4 QoS 等级与 oom_score_adj

Kubernetes 根据 requests 和 limits 的关系给 Pod 划分 QoS:

  • Guaranteed:每个容器 requests.memory == limits.memory 且 CPU 也相等。oom_score_adj 为 -997,最不容易被节点 OOM Killer 选中。
  • Burstable:至少有一个容器设置了 requests,但不满足 Guaranteed。oom_score_adj 在 2 到 1000 之间按比例计算。
  • BestEffort:没设置任何 requests/limits。oom_score_adj 为 1000,节点内存紧张时最先被杀。

QoS 影响的是节点级 OOM Killer 的优先级和驱逐顺序,不影响容器自身 cgroup 上限是否被突破。也就是说,Guaranteed 的 Pod 一样会 OOMKilled,只要它自己吃满了自己的 limits。

3.5 JVM 在容器里的内存组成

这是排查 Java Pod OOM 最关键的知识点。一个 JVM 进程在容器里占用的内存远不止堆:

  • 堆(Heap):-Xmx 控制的部分,GC 管理的对象。
  • Metaspace:类元数据,-XX:MaxMetaspaceSize 控制,堆外。
  • 直接内存(Direct Buffer):NIO/Netty 用,-XX:MaxDirectMemorySize 控制,堆外。
  • 线程栈:每线程 -Xss,线程数 × Xss,堆外。
  • Code Cache:JIT 编译后的代码,-XX:ReservedCodeCacheSize,堆外。
  • GC 自身开销:G1/Parallel/ZGC 的数据结构和线程占用的本地内存。
  • JNI 本地内存:native 库分配,JVM 之外但同进程内。
  • JVM 自身:运行时数据结构、C 堆分配等。

这就是”-Xmx 设成等于 limits.memory 必然 OOM”的根本原因:cgroup 限制的是整个进程的内存,而 -Xmx 只管堆。堆外的 Metaspace、直接内存、线程栈、Code Cache、GC 开销加起来可能占几百 MiB 到 1Gi 以上,留给堆的”安全垫”一旦没有,堆还没满进程就被杀了。

3.6 UseContainerSupport 与 MaxRAMPercentage

  • JDK 8u191 之前,JVM 在容器里会按宿主机内存算堆,导致 -Xmx 自动推导远超 limits,极易 OOM。8u191 引入 UseContainerSupport(默认开启),JVM 识别 cgroup 限制。
  • 不显式设 -Xmx 时,JVM 按 cgroup 内存上限的百分比自动算最大堆,默认 MaxRAMPercentage=25(Java 8/11 早期部分版本默认 25,Server 模式下部分版本为 25;不同版本默认值可能略有差异,重点以你实际 JDK 的默认值为准,建议显式指定)。
  • 推荐显式用 -XX:MaxRAMPercentage 控制堆占比,而不是依赖默认值。

3.7 退出码 137 的含义

退出码 137 = 128 + 9(SIGKILL)。容器进程被 SIGKILL 强杀,OOM 是最常见原因,但并非唯一:节点驱逐、kubectl delete pod、cgroup 冻结、宿主机 OOM Killer 都可能产生 137。所以看到 137 不要直接断言是容器 OOM,要结合 Reason 字段和节点事件一起判断。

3.8 JVM 内存模型的完整图景

要排查堆外 OOM,必须先在脑子里建立 JVM 进程内存的完整图景。一个 JVM 进程在操作系统眼里就是一段虚拟地址空间,cgroup 限制的是这段空间里实际落实成物理页(RSS)的部分。JVM 把这段空间划分成若干区域:

  • Java Heap:-Xms/-Xmx 控制,分新生代(Eden + Survivor)/老年代,GC 主战场。这部分在 NMT 里归 Java Heap
  • Metaspace:存储类元数据(Klass、方法、常量池等),位于 native 内存,-XX:MaxMetaspaceSize 兜底。NMT 归 Class
  • Code Cache:JIT 编译产物,-XX:ReservedCodeCacheSize。NMT 归 Code
  • Thread Stack:每个 Java 线程一份,-Xss 控制单线程大小,线程数 × Xss 是总量。NMT 归 Thread
  • Direct Memory:NIO ByteBuffer.allocateDirect 分配的堆外缓冲,Netty PooledByteBufAllocator 也走这里,-XX:MaxDirectMemorySize 兜底。NMT 归 Direct(部分实现归 Internal,不同版本口径可能略有差异)。
  • GC 数据结构:G1 的 Remembered Set、Card Table、标记位图等,ZGC 的着色指针元数据。NMT 归 GC
  • Internal:JVM 自身的 C 堆分配、Unsafe 分配等。NMT 归 Internal
  • JNI/GC native:第三方 native 库(如 RocksDB、压缩库、图像处理)直接 malloc 的内存,可能不在 NMT 统计内,是排查盲区。

关键认知:-Xmx 只管 Java Heap 这一项。剩下七项加起来,在没有兜底参数时是可以无限增长的,这就是堆外 OOM 的来源。容器 limits 限制的是上述所有项的物理页总和。

3.9 内存各部分的增长特性

不同内存区域的增长模式不同,识别模式有助于快速定位:

  • Heap:受 -Xmx 硬约束,正常不会超过,超过会先抛 OutOfMemoryError: Java heap space(除非 -XX:+ExitOnOutOfMemoryError 才转 SIGKILL)。
  • Metaspace:随加载的类数量增长。动态生成类(CGLIB 代理、Groovy 脚本、JSP 重编译、反射 MethodHandle)会导致持续增长,是泄漏高发区。
  • Direct Memory:随未释放的 ByteBuf 增长。Netty 池化分配但忘记 release、连接泄漏、响应体未消费都会导致直接内存只涨不降。
  • Thread Stack:随线程数增长。线程池无上限、异步框架无并发限制、每次请求 new 线程都会导致线程栈爆炸。
  • Code Cache:相对稳定,JIT 编译达到稳态后基本不再涨,除非频繁加载新类触发大量编译。
  • GC 数据结构:随堆大小和 Region 数量定,G1 在大堆下 Remembered Set 占用可观,是”堆没满但 GC 占用大”的一个原因。

排查时根据增长模式反推:线性增长看 Metaspace 和 Direct;阶跃式增长看 Thread;与流量同步波动看 Heap 和 Direct;稳定高位不降看是否安全垫不足。

3.10 cgroup v2 的内存统计字段对照

cgroup v2 的 memory.stat 字段与 v1 不同,排查时要知道对应关系:

| cgroup v2 字段 | 含义 | 近似 v1 字段 | | — | — | — | | anon | 匿名页(堆、堆外 malloc 落地) | rss | | file | 文件页(缓存) | cache | | slab | 内核 slab(可回收/不可回收) | slab | | sock | socket 缓冲 | sock | | shmem | 共享内存 | mapped_file /shmem | | pgfault /pgmajfault | 页错误 | 同名 | | oom /oom_kill(在 memory.events) | OOM 事件计数 | oom_control 里的 under_oom/oom_kill |

排查容器 OOM 时,anon 持续增长最可疑(对应进程实际占用的堆+堆外 native),file 增长一般是缓存可回收。不同内核版本字段集合可能略有差异,重点看 memory.currentmemory.peakmemory.events 三个文件。

3.11 Kubernetes 内存配额的两个维度

除了 Pod 级别的 requests/limits,还有两个维度会影响 OOM 行为:

  • Namespace ResourceQuota:限制命名空间总内存申请量和使用量。requests.memory 超过 Quota 的 Pod 创建会被拒绝(Pending),不是 OOM,但容易和资源不足混淆。
  • LimitRange:给没有显式设置 resources 的容器设默认 requests/limits,避免 BestEffort 满天飞。

排查”Pod 创建失败”时,先 kubectl describe namespace <ns> 看 ResourceQuota 使用情况,区分是配额超限还是节点资源不足,再决定是调 Quota 还是扩容,不要误当成 OOM 处理。

四、整体排查思路

遇到 Pod OOM,按这个顺序走,不要一上来就改 JVM 参数:

  1. 确认是容器 OOM 还是节点驱逐:kubectl describe pod 看 Reason 和 Events,kubectl describe node 看是否 MemoryPressure
  2. 确认是哪个容器:Pod 可能多容器,定位到具体 container 的 Last State
  3. 确认内存增长曲线:看 Prometheus container_memory_working_set_bytes 近 24 小时趋势,是缓慢爬升(泄漏)还是尖峰(突发流量/大查询)还是稳态后突杀(堆外或 limits 留得太小)。
  4. 确认是堆内还是堆外:OOM 时堆是否真的满了,决定走堆排查还是 native memory 排查。
  5. 定位根因:根据上面四步收敛到具体原因(limits 配错、堆泄漏、直接内存泄漏、Metaspace、线程爆炸、GC 失败、节点压力)。
  6. 修复:按根因给对应方案,而不是无脑加内存。
  7. 验证:复现压力、观察指标、确认重启次数不再增长。
  8. 回滚预案:保留旧配置和镜像,灰度可退。

排查路径示意(脑内决策树):

Pod 重启 / 137
&nbsp; ├─ describe pod Reason=OOMKilled → 容器 OOM
&nbsp; │ &nbsp; &nbsp;├─ working_set 平稳后突杀,堆没满 → 堆外/limits 安全垫不足
&nbsp; │ &nbsp; &nbsp;├─ working_set 缓慢爬升 → 泄漏(堆 or 堆外)
&nbsp; │ &nbsp; &nbsp;└─ working_set 尖峰 → 突发流量/大对象/大查询
&nbsp; └─ describe pod Status=Evicted / node MemoryPressure → 节点内存
&nbsp; &nbsp; &nbsp; &nbsp;└─ 节点超卖严重 / requests 设置过低 / BestEffort 太多

4.1 排查前的环境信息收集

动手前先把环境信息收集齐,避免排查中途反复确认:

  • 集群版本:kubectl version --short 或 kubectl version(cgroup v2 默认从 1.25 起,影响内存文件路径)。
  • 节点 cgroup 版本:在节点上 stat -fc %T /sys/fs/cgroup/,输出 cgroup2fs 为 v2,tmpfs 为 v1。
  • Pod 的 QoS:kubectl get pod <pod> -n <ns> -o jsonpath='{.status.qosClass}'
  • JDK 版本:容器内 java -version,确认是否 ≥ 8u191(容器感知)、是否支持 -XX:+ExitOnOutOfMemoryError(8u92+)和 -Xlog:gc*(9+)。
  • 是否开 NMT:jcmd <pid> VM.native_memory summary 能跑通即开启。
  • 监控是否就绪:确认 container_memory_working_set_bytes 等指标有数据。
  • 是否有 dump 卷:kubectl get pvc -n <ns> 看是否配了 dump 持久卷。

环境信息收集清单(核对表):

| 项 | 命令 | 用途 | | — | — | — | | 集群版本 | kubectl version | 判断 cgroup 默认版本、API 兼容性 | | cgroup 版本 | stat -fc %T /sys/fs/cgroup/ (节点) | 决定看哪个内存文件 | | Pod QoS | kubectl get pod -o jsonpath=...qosClass | 判断节点驱逐优先级 | | JDK 版本 | java -version (容器) | 判断可用 JVM 参数 | | NMT 状态 | jcmd <pid> VM.native_memory summary | 堆外排查前提 | | 监控数据 | Prometheus 查询 | 趋势分析 | | dump 卷 | kubectl get pvc | dump 落盘位置 |

信息齐全再动手,能避免排查到一半发现”NMT 没开只能靠排除法”或”cgroup 是 v2 却在 cat v1 路径”这类返工。

五、实战步骤

下面以一个真实风格的案例串起来。业务侧反馈:order-service 在 prod 命名空间,最近两天每隔几小时重启一次,告警里是 Pod restarted

5.1 第一步:确认重启原因

目的:分清容器 OOM 还是节点驱逐。

命令:

bash

kubectl get pod -n prod -l app=order-service -o wide

预期输出:READY 列可能正常,但 RESTARTS 列大于 0 且在增长。

命令:

bash

kubectl describe pod <pod-name> -n prod

重点看两段:

  • Containers:

    下每个容器的 Last State

Last State: &nbsp; &nbsp;Terminated
&nbsp; Reason: &nbsp; &nbsp; &nbsp;OOMKilled
&nbsp; Exit Code: &nbsp; 137
&nbsp; Started: &nbsp; &nbsp; Mon Jun 23 10:00:00 2026
&nbsp; Finished: &nbsp; &nbsp;Mon Jun 23 12:30:00 2026
  • Events:

    段是否有 Evicted 或节点相关事件。

判断逻辑:

  • Reason: OOMKilled

    → 走容器内存排查(5.3 起)。

  • Status: Evicted

    或 Events 里有 node was low on resource → 走节点内存排查(5.2)。

  • 两者都没有,只是 137 → 看是否人为 delete 或健康检查失败导致的强杀,先排除 Liveness/Startup Probe 失败。

异常表现:Last State 里 Reason 为空但 Exit Code 137,且 Events 里有 Liveness probe failed。这其实是探针失败后被 kill,不是 OOM,不要按 OOM 处理。

下一步动作:根据 Reason 进入对应分支。

5.2 分支 A:节点内存压力导致的驱逐

目的:判断是不是节点层面的问题。

命令:

bash

kubectl describe node <node-name>

重点看:

  • Conditions:

    里 MemoryPressure 是否为 True

  • Allocatable:

    的 memory 与 Allocated resources 的内存申请量对比,看是否超卖严重。

  • Events:

    段是否有 Evicted 记录。

命令:

bash

kubectl get pod -n prod -o wide --field-selector spec.nodeName=<node-name> | \
&nbsp; awk&nbsp;'{print $1, $2}'&nbsp;|&nbsp;head

辅助看节点上跑了哪些 Pod。

判断逻辑:

  • MemoryPressure=True

    或节点 memory.available 长期接近 --eviction-hard 阈值 → 节点内存不足。

  • 节点上 Burstable/BestEffort Pod 多,且 requests.memory 设得很低但实际用得多 → 超卖,被驱逐的是最软的柿子。

常用命令看节点实时内存(在节点上执行,需要主机权限):

bash

free -h
bash

cat&nbsp;/proc/meminfo | grep -E&nbsp;"MemTotal|MemFree|MemAvailable|Buffers|Cached"

预期输出重点关注 MemAvailable,kubelet 用这个值和 eviction-hard 阈值比较。

修复方向(节点侧):

  • 给关键服务用 Guaranteed QoS(requests == limits),降低被驱逐概率。
  • 扩节点或迁移 Pod,降低单节点内存超卖。
  • 检查是否 requests.memory 设得过低(如 256Mi 但实际用 2Gi),把它调到接近真实用量。

风险提醒:调整 requests 会触发 Pod 重新调度,可能影响滚动更新和 PDB。生产环境先在一个副本上验证,避免批量重建。

下一步动作:如果排除节点压力,回到容器 OOM 分支(5.3)。

5.3 第二步:定位是哪个容器、内存增长形态

目的:多容器 Pod 要锁定到具体容器,并判断内存是泄漏型、突发型还是稳态突杀型。

命令:

bash

kubectl top pod <pod-name> -n prod --containers

预期输出(示意):

POD &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;NAME &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;CPU(cores) &nbsp; MEMORY(bytes)
order-service-xxx &nbsp;order-app &nbsp; &nbsp; 120m &nbsp; &nbsp; &nbsp; &nbsp; 1850Mi
order-service-xxx &nbsp;sidecar-log &nbsp; 10m &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;80Mi

判断逻辑:锁定到 order-app 容器,内存 1850Mi。

命令(看历史趋势,依赖 Prometheus 已部署 cAdvisor metrics):

bash

# 示例思路:通过 PromQL 查询近 6 小时 working set 趋势
# 实际用 Grafana Explore 或 promtool query 比手敲 PromQL 更稳

PromQL(以实际 exporter 指标为准):

container_memory_working_set_bytes{namespace="prod",pod=~"order-service-.*",container="order-app"}

判断内存形态:

  • 缓慢单调爬升,每次重启后归零再爬 → 典型泄漏(堆 or 堆外),走 5.4、5.5。
  • 平稳在一个值附近,突然尖峰后重启 → 突发流量/大查询/大对象,走 5.6。
  • 平稳在接近 limits 的值,无明显增长,运行一段时间后被杀 → limits 安全垫不足或堆外吃满,走 5.7。

下一步动作:按形态进入对应子步骤。

5.4 第三步:在容器内确认 cgroup 上限与实际用量

目的:用内核视角核实 limits 生效情况,排除”limits 没生效 / 被覆盖”。

命令(进入容器,需要确认 Pod 还活着或在新副本里执行):

bash

kubectl&nbsp;exec&nbsp;-it <pod-name> -c order-app -n prod -- sh

容器内执行:

bash

# cgroup v2
cat&nbsp;/sys/fs/cgroup/memory.max
cat&nbsp;/sys/fs/cgroup/memory.current
cat&nbsp;/sys/fs/cgroup/memory.peak
cat&nbsp;/sys/fs/cgroup/memory.events
bash

# cgroup v1
cat&nbsp;/sys/fs/cgroup/memory/memory.limit_in_bytes
cat&nbsp;/sys/fs/cgroup/memory/memory.usage_in_bytes

预期输出(cgroup v2 示意):

2147483648 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# memory.max = 2Gi,对应 limits.memory: 2Gi
2107637760 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# memory.current 已接近上限
2147483648 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# memory.peak 等于上限,说明顶到过
oom 3
oom_kill 3 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 已发生 3 次 cgroup OOM

判断逻辑:

  • memory.max

    应等于 limits.memory。如果不等,说明 limits 没正确下发或被运行时覆盖,去查 Deployment/Pod spec 和运行时类(如 containerd 配置)。

  • memory.peak

    等于或非常接近 memory.max,说明确实顶到上限被杀,确认是容器 OOM。

  • memory.events

    的 oom_kill 计数与重启次数能对上。

下一步动作:确认 limits 生效后,判断堆内还是堆外。

5.5 第四步:判断是堆内还是堆外

目的:决定走堆泄漏排查还是 native memory 排查。

命令(容器内,JDK 自带工具):

bash

# 拿到 JVM 进程 PID
jps
# 或
pgrep -f java
bash

# 堆使用概况
jcmd <pid> GC.heap_info
# 或
jstat -gc <pid> 1000 5

jcmd <pid> GC.heap_info 预期输出(示意):

&nbsp;garbage-first heap &nbsp; total &nbsp;1048576K, used 880000K [0x..., 0x..., 0x...)
&nbsp; region size 2048K, 512 young (1048576K), 0 survivors (0K)
&nbsp;Metaspace &nbsp; &nbsp; &nbsp; used 120000K, capacity 130000K, committed 132000K, reserved 1146880K
&nbsp; class space &nbsp; &nbsp;used 13000K, capacity 14000K, committed 14000K, reserved 1048576K

判断逻辑:

  • 堆 used 远小于 Xmx(如 Xmx 1.5Gi 但 used 才 880Mi),而容器 memory.current 已接近 memory.max → 堆外内存吃满,走 5.7。
  • 堆 used 接近 Xmx 且 GC 后降不下来 → 堆泄漏或大对象驻留,走 5.6。
  • Metaspace used 接近 MaxMetaspaceSize → Metaspace 泄漏(常见于动态类生成、Groovy 脚本、反射代理),走 5.7。

开启 Native Memory Tracking(需要重启 JVM 加参数,不能热开,除非已预先开启):

-XX:NativeMemoryTracking=summary

开启后命令:

bash

jcmd <pid> VM.native_memory summary

预期输出会分类给出 Reserved/Committed:Java HeapClass(Metaspace)、ThreadCodeGCInternalDirect 等。重点关注哪一类 Committed 异常大。

异常表现:Direct 或 Internal 的 Committed 持续增长但堆不涨 → 直接内存泄漏(Netty、NIO 场景常见)。

下一步动作:堆内问题走 5.6,堆外问题走 5.7。

5.6 分支 B:堆内泄漏或大对象

目的:定位是哪类对象占住堆。

命令:

bash

# 看 GC 情况,关注 Full GC 频率和各代占用
jstat -gcutil <pid> 2000 10

预期输出(示意):

&nbsp; S0 &nbsp; &nbsp; S1 &nbsp; &nbsp; E &nbsp; &nbsp; &nbsp;O &nbsp; &nbsp; &nbsp;M &nbsp; &nbsp; CCS &nbsp; &nbsp;YGC &nbsp; &nbsp;YGCT &nbsp; &nbsp;FGC &nbsp; &nbsp;FGCT &nbsp; &nbsp;GCT
&nbsp; 0.00 &nbsp;98.00 &nbsp;60.00 &nbsp;95.00 &nbsp;92.00 &nbsp;88.00 &nbsp; 120 &nbsp; 15.000 &nbsp; 12 &nbsp; &nbsp;8.500 &nbsp;23.500

判断逻辑:

  • O

    (Old 区)长期 90%+ 且 FGC 频繁、FGCT 累计时间长但 O 降不下来 → 老年代驻留大量对象,典型泄漏。

  • YGC

    后 O 持续上涨只下不来 → 对象晋升后无法回收。

命令(抓堆 dump,注意风险):

bash

# 抓堆 dump,会触发一次 STW,生产慎用,建议在低峰或副本上抓
jcmd <pid> GC.heap_dump /tmp/heap.hprof

或者更推荐用 Arthas 在线看,开销更小:

bash

# 下载并启动 arthas(按官方方式),attach 到目标 JVM
# arthas 里执行:
dashboard
heapdump /tmp/heap.hprof

拿到 hprof 后用 MAT(Eclipse Memory Analyzer)或 jolokia/async-profiler 配合分析,重点看 Dominator Tree 和 Leak Suspects 报告,定位哪类对象、哪条引用链占住内存。

风险提醒:GC.heap_dump 抓全堆会 STW 并产生与堆等大的临时文件,生产环境务必在副本或低峰执行,dump 文件及时清理避免占满磁盘。

下一步动作:定位泄漏对象后改代码或配置,再走 5.8 的验证。

5.7 分支 C:堆外内存 / limits 安全垫不足

目的:分清是”真的堆外泄漏”还是”limits 留的安全垫不够”。

先看是不是配置问题。计算容器内存预算:

容器内存预算(limits.memory) =
&nbsp; -Xmx(堆)
+ -XX:MaxMetaspaceSize
+ -XX:MaxDirectMemorySize(直接内存上限)
+ 线程数 × -Xss(线程栈)
+ -XX:ReservedCodeCacheSize
+ GC 开销(G1 一般预留堆的 10%~20%,ZGC 更多)
+ JVM 自身 + 安全垫(建议 25%~30%)

举例:limits.memory: 2Gi,若 -Xmx 设成 1.8Gi,几乎没有安全垫,Metaspace + 线程栈 + 直接内存 + GC 一上来就把 2Gi 撑爆,堆还没满就 OOM。这是最常见的”配置型 OOM”。

判断逻辑:

  • 用 5.5 的 VM.native_memory summary 看哪一类堆外大。
  • 如果是直接内存(Direct)持续增长 → Netty/NIO 场景,检查是否未释放的 ByteBuf、连接池泄漏。可加 -XX:MaxDirectMemorySize=512m 兜底,并用 jcmd <pid> VM.native_memory baseline + detail 对比增长。
  • 如果是 Metaspace 增长 → 检查是否有动态类生成未被回收,加 -XX:MaxMetaspaceSize=256m 兜底并定位生成源。
  • 如果是线程数爆炸(Thread 类大)→ 查线程数,jcmd <pid> Thread.print 或 kubectl exec -- jstack <pid>,看是否线程池没设上限或异步框架创建了海量线程。
  • 如果配置预算算下来本来就不够 → 走”正确设置 limits + JVM 参数”方案(第六、七节)。

命令(看线程数):

bash

# 容器内
jcmd <pid> Thread.print&nbsp;| grep&nbsp;"java.lang.Thread.State"&nbsp;|&nbsp;wc&nbsp;-l
# 或看进程线程数
cat&nbsp;/proc/<pid>/status | grep Threads

判断逻辑:线程数远超预期(如几万)→ 线程泄漏或池未限流,先治线程,别加内存。

下一步动作:堆外泄漏走代码修复 + 兜底参数;配置型走参数规范。

5.8 第五步:修复方案落地

按根因分场景给方案,下面是最常见的两类。

场景一:配置型 OOM(limits 安全垫不足)。

修复要点:

  • limits.memory

    设为应用稳态峰值 × 1.3 左右,留 30% 安全垫。

  • -Xmx

    设为 limits.memory 的 60%~70%,而不是等于 limits。

  • 显式设置堆外各部分上限。

  • 用 -XX:MaxRAMPercentage 时,给堆外的部分要单独再留。

场景二:堆外泄漏(直接内存)。

修复要点:

  • 加 -XX:MaxDirectMemorySize 兜底,避免无限增长。
  • 代码侧定位未释放的 ByteBuf(Netty 用 PooledByteBufAllocator 的 leakDetector,加 -Dio.netty.leakDetection.level=ADVANCED)。
  • 修复后灰度发布验证。

修改 Deployment 示例见第七节。

5.9 第六步:验证

目的:确认修复后 OOM 不再发生。

命令(持续观察重启次数):

bash

kubectl get pod -n prod -l app=order-service -w

观察 RESTARTS 列在预期压力下是否保持不增长。

命令(看实时内存):

bash

kubectl top pod -n prod -l app=order-service --containers

Prometheus 看趋势(以实际 exporter 指标为准):

container_memory_working_set_bytes{namespace="prod",container="order-app"}

验证标准:

  • 在业务高峰(压测或真实流量)下,working_set 峰值低于 limits.memory 的 80%~85%。
  • 连续观察 24~48 小时,重启次数为 0。
  • 容器内 memory.events 的 oom_kill 计数不再增长。

5.10 第七步:复盘

复盘要点:

  • 记录根因、修复方案、验证结果、影响范围。
  • 沉淀监控告警阈值(见第八节)。
  • 把”limits + JVM 参数”规范写入团队基线,避免同类问题在其他服务复现。
  • 如果是泄漏类,跟踪代码修复的 PR 和回归测试。

5.11 分支 D:线程泄漏与 native 线程上限

目的:线程数异常增长会把线程栈总占用顶上去,每线程默认 -Xss1m,几千线程就是几 GiB 堆外,直接吃满容器内存,而堆可能完全没动。

命令(容器内看线程数):

bash

pgrep -f java |&nbsp;head&nbsp;-1 | xargs -I{}&nbsp;cat&nbsp;/proc/{}/status | grep Threads

预期输出(示意):

Threads: 8432

判断逻辑:业务正常线程数应该是个稳定的小数量级(几十到几百)。Threads: 8432 显然异常,按 -Xss1m 算线程栈理论占用 8.4GiB,远超 2Gi limits,必然 OOM。

命令(看线程在干什么):

bash

jcmd <pid> Thread.print&nbsp;> /tmp/t.txt
grep -E&nbsp;"java.lang.Thread.State"&nbsp;/tmp/t.txt |&nbsp;sort&nbsp;|&nbsp;uniq&nbsp;-c |&nbsp;sort&nbsp;-rn |&nbsp;head

预期输出(示意):

&nbsp; &nbsp;8400 RUNNABLE
&nbsp; &nbsp; &nbsp;12 TIMED_WAITING (on object monitor)
&nbsp; &nbsp; &nbsp;20 WAITING (parking)

判断逻辑:8400 个 RUNNABLE 线程说明线程池没有上限或异步框架(如未限制并发度的响应式/线程池)创建了海量线程。

命令(找创建线程的栈):

bash

jcmd <pid> Thread.print&nbsp;| grep -A 30&nbsp;"Thread-"&nbsp;|&nbsp;head&nbsp;-60

异常表现:大量线程栈顶停在某个连接池获取、某个 new Thread 调用、某个 CompletableFuture 链路上。

还要检查系统级线程上限(容器内):

bash

cat&nbsp;/proc/sys/kernel/threads-max
ulimit&nbsp;-u

判断逻辑:如果 Threads 计数已经接近 threads-max 或 ulimit -u,会先抛 unable to create new native thread 而不是直接 OOM。两种情况都可能发生,看日志区分:

  • 日志有 OutOfMemoryError: unable to create new native thread → 线程数触顶,先治线程。
  • 日志没有该错误,但进程被 SIGKILL → 线程栈总占用吃满 cgroup,按 OOM 处理。

修复方向:

  • 代码侧给线程池设上限(ThreadPoolExecutor 的 maximumPoolSize、队列容量),响应式框架限制并发度(如 flatMap 的 concurrency 参数)。
  • 临时止血可降低 -Xss(如 -Xss512k,但要看栈深度是否够),或调高 limits 并尽快定位泄漏源。
  • 不要靠调大 threads-max 解决线程泄漏,那是把崩溃时间推迟。

风险提醒:抓 Thread.print 时线程数极高的情况下本身有开销,避开高峰;导出文件及时清理。

下一步动作:定位泄漏源后修复并灰度,走 5.8 修复与 5.9 验证。

5.12 分支 E:GC overhead 与堆泄漏的区分

目的:GC overhead limit exceeded 和真正的堆泄漏在表象上接近,但处理方向不同。

命令:

bash

jstat -gcutil <pid> 2000 20

预期输出(示意,连续 20 次采样):

&nbsp; S0 &nbsp; &nbsp; S1 &nbsp; &nbsp; E &nbsp; &nbsp; &nbsp;O &nbsp; &nbsp; &nbsp;M &nbsp; &nbsp; CCS &nbsp; &nbsp;YGC &nbsp; YGCT &nbsp; &nbsp;FGC &nbsp; FGCT &nbsp; &nbsp; GCT
&nbsp; 0.00 &nbsp;98.00 &nbsp;80.00 &nbsp;92.00 &nbsp;92.00 &nbsp;88.00 &nbsp;130 &nbsp;16.000 &nbsp; &nbsp;5 &nbsp; 4.000 &nbsp;20.000
&nbsp; 0.00 &nbsp;98.00 &nbsp;80.00 &nbsp;93.00 &nbsp;92.00 &nbsp;88.00 &nbsp;132 &nbsp;16.200 &nbsp; &nbsp;6 &nbsp; 4.800 &nbsp;20.800
&nbsp; 0.00 &nbsp;98.00 &nbsp;80.00 &nbsp;94.00 &nbsp;92.00 &nbsp;88.00 &nbsp;135 &nbsp;16.500 &nbsp; &nbsp;7 &nbsp; 5.600 &nbsp;22.100
&nbsp; 0.00 &nbsp;98.00 &nbsp;80.00 &nbsp;94.00 &nbsp;92.00 &nbsp;88.00 &nbsp;138 &nbsp;16.900 &nbsp; &nbsp;8 &nbsp; 6.400 &nbsp;23.300

判断逻辑:

  • FGC

    (Full GC 次数)短时间内快速上涨,O(老年代)持续 92%~94% 降不下来,FGCT(Full GC 累计耗时)线性增长 → 老年代驻留大量无法回收对象,典型堆泄漏。

  • 如果 O 在 Full GC 后能短暂回落但很快又涨满,且伴随业务大流量 → 可能是”瞬时大对象”而非稳定泄漏,需结合业务看是不是大查询/大结果集。

区分方法:在低峰期(流量小)持续观察 O。低峰仍单调爬升 → 泄漏;低峰回落、高峰才涨 → 突发大对象。

修复方向:

  • 泄漏:抓 dump 用 MAT 看 Dominator Tree 和 Path to GC Roots(排除弱/软引用),定位持住大对象的引用链,改代码。
  • 突发大对象:业务侧限制单次查询返回行数、分页、流式处理,或对大结果集加缓存上限。

5.13 分支 F:突发流量与大查询导致的尖峰 OOM

目的:内存形态是平稳基线 + 偶发尖峰,重启时间点与某个业务动作(大查询、报表导出、批量推送)对齐。

判断依据(多指标交叉,不要只看一个):

  • working_set

    折线在某个时刻出现陡峭尖峰,尖峰顶部即重启点。

  • 业务日志在该时刻有大量”导出”“查询””批量”类请求。

  • 重启后 working_set 立刻回落到基线,不是缓慢爬升。

这类不是泄漏,是单次操作分配了大量临时对象或加载了大结果集到内存。

命令(看 GC 在尖峰时刻的表现,需开启 GC 日志):

bash

# 如果启动参数里有 -Xlog:gc*(JDK 11+)或 -XX:+PrintGCDetails(JDK 8)
# 在容器内找 GC 日志
ls&nbsp;-lh /var/log/ | grep gc
# 或通过 jcmd 查看 GC 日志配置
jcmd <pid> VM.log&nbsp;list

修复方向(不靠加内存):

  • 大查询分页、流式游标(MyBatis 的 ResultHandler、JPA 的 Stream)。
  • 报表导出改异步 + 分批写文件/对象存储,不在内存里攒全量。
  • 对单请求内存设上限(如限制导出最大行数),超过走拒绝或排队。
  • 限流:对触发大内存操作接口限并发,避免多个大请求同时打满内存。

加内存只是兜底,根因在业务逻辑,否则流量一大就复发。

5.14 完整案例时间线

把前面的步骤串成一个真实复盘时间线,便于对照自己的排查节奏。

  • T0:监控告警 Pod restartedorder-service 重启计数 +1。
  • T+2min:kubectl describe pod 看到 Reason: OOMKilled, Exit Code: 137,确认容器 OOM,排除节点驱逐(node 无 MemoryPressure)。
  • T+5min:kubectl top pod --containers 锁定 order-app 容器,内存 1850Mi / limits 2Gi。
  • T+10min:Prometheus 看 24h 趋势,working_set 平稳在 1.8Gi 附近偶发尖峰到 2Gi 后重启,判断不是缓慢泄漏,是稳态接近上限 + 尖峰。
  • T+15min:进入新副本容器,cat /sys/fs/cgroup/memory.max = 2147483648(2Gi),memory.peak = 2147483648,确认顶到上限。
  • T+20min:jcmd <pid> GC.heap_info,堆 used 才 880Mi,远小于 MaxHeapSize 1.5Gi(原配置 -Xmx 设成了 1.8Gi,实际堆没满)。
  • T+25min:判断堆外吃满。jcmd <pid> VM.native_memory summary(已开 NMT),Direct 类 Committed 约 600Mi 且持续增长,定位直接内存。
  • T+30min:日志搜 Direct buffer memory,确认 Netty 场景直接内存泄漏。加 -XX:MaxDirectMemorySize=512m 兜底,并开 -Dio.netty.leakDetection.level=ADVANCED
  • T+40min:核算内存预算,把 -Xmx 从 1.8Gi 调到 1.2Gi(MaxRAMPercentage=60),Metaspace 256m,Direct 512m,留安全垫。
  • T+50min:改 Deployment,灰度 1 副本 kubectl apply,观察 1 小时,重启次数为 0,working_set 峰值 1.6Gi。
  • T+1h:全量滚动更新,连续观察 48h,重启计数 0,oom_kill 计数不增长。
  • T+48h:复盘,把”netty 泄漏检测 + MaxDirectMemorySize 兜底 + 堆按 limits 60%”写入团队基线,配置告警 PodMemoryNearLimit 85%。

这条时间线说明:从告警到定位根因约 30 分钟,前提是监控指标和 NMT 都就位。没有 NMT 时定位堆外要靠排除法,时间会拉长到数小时,所以建议关键服务默认开 NativeMemoryTracking=summary

六、常用命令速查

bash

# 看 Pod 状态和重启次数
kubectl get pod -n <ns> -o wide
kubectl get pod -n <ns> -l <selector>

# 看重启原因(重点)
kubectl describe pod <pod> -n <ns>

# 看容器级别资源占用
kubectl top pod <pod> -n <ns> --containers
kubectl top node

# 进入容器
kubectl&nbsp;exec&nbsp;-it <pod> -c <container> -n <ns> -- sh

# 容器内 cgroup v2
cat&nbsp;/sys/fs/cgroup/memory.max
cat&nbsp;/sys/fs/cgroup/memory.current
cat&nbsp;/sys/fs/cgroup/memory.peak
cat&nbsp;/sys/fs/cgroup/memory.events

# 容器内 cgroup v1
cat&nbsp;/sys/fs/cgroup/memory/memory.limit_in_bytes
cat&nbsp;/sys/fs/cgroup/memory/memory.usage_in_bytes
cat&nbsp;/sys/fs/cgroup/memory/memory.stat

# 节点内存
kubectl describe node <node>
# 节点上执行
free -h
cat&nbsp;/proc/meminfo

# 看 dmesg 的 OOM 记录(需要宿主机权限,容器内看不到宿主 dmesg)
dmesg -T | grep -i -E&nbsp;"out of memory|oom|killed process"

# JVM 诊断(容器内)
jps
pgrep -f java
jcmd <pid> GC.heap_info
jstat -gc <pid> 1000 10
jstat -gcutil <pid> 2000 10
jcmd <pid> VM.native_memory summary
jcmd <pid> Thread.print
jcmd <pid> VM.flags
jcmd <pid> GC.heap_dump /tmp/heap.hprof

# 看 Pod 资源声明
kubectl get pod <pod> -n <ns> -o jsonpath='{.spec.containers[*].resources}'&nbsp;| jq .

说明:dmesg 在容器内通常看不到宿主机的内核日志,看宿主 OOM 记录需要在节点上以特权方式执行,或通过日志采集(如 node-problem-detector)把 dmesg 里的 OOM 行采集到中心日志。node-problem-detector 会把宿主 OOM 事件转成 Kubernetes Node 事件和告警,是生产环境推荐做法。

6.1 命令输出关键字段速查表

kubectl describe pod 关键字段对照:

| 字段位置 | 字段名 | 含义 | 判断 | | — | — | — | — | | Containers.lastState | Reason | 终止原因 | OOMKilled =容器OOM;空+Evicted=节点驱逐 | | Containers.lastState | Exit Code | 退出码 | 137 =SIGKILL;143=SIGTERM(正常停止) | | Containers.lastState | Finished | 终止时间 | 与监控尖峰对齐确认因果关系 | | Containers.restartCount | 计数 | 重启次数 | 趋势增长=反复 OOM | | Containers.state | running/waiting | 当前状态 | CrashLoopBackOff =反复崩溃退避 | | Events | Back-off | 退避 | 配合 restartCount 判断崩溃频率 |

jstat -gcutil 列含义对照:

| 列 | 含义 | 关注 | | — | — | — | | S0/S1 | Survivor 0/1 使用率 | 正常交替 | | E | Eden 使用率 | YGC 后应回落 | | O | 老年代使用率 | 长期 90%+ 降不下=泄漏 | | M | Metaspace 使用率 | 接近 MaxMetaspaceSize=元数据泄漏 | | YGC/YGCT | YGC 次数/耗时 | 频繁 YGC=新生代太小或分配过快 | | FGC/FGCT | Full GC 次数/耗时 | 频繁 FGC 且 O 降不下=老年代驻留 | | GCT | GC 总耗时 | 与运行时间比看 GC 占比 |

jcmd <pid> VM.native_memory summary 分类对照:

| 分类 | 对应内存 | 增长异常时的方向 | | — | — | — | | Java Heap | 堆 | 查堆泄漏/dump | | Class | Metaspace | 查动态类生成 | | Thread | 线程栈 | 查线程泄漏 | | Code | Code Cache | 一般稳定,看是否频繁加载新类 | | GC | GC 数据结构 | 大堆下 G1 RS 占用,评估换 GC | | Internal | JVM 内部/Unsafe | 查 native 分配 | | Direct | 直接内存 | 查 Netty/NIO 释放 |

6.2 一次性诊断脚本

把常用诊断命令打包成一个脚本,进容器后一键采集,避免手敲遗漏。文件路径:scripts/oom-snapshot.sh

bash

#!/usr/bin/env bash
# 用途:在容器内一次性采集 JVM 内存诊断快照,输出到 /tmp/oom-snapshot
# 只读采集,不修改任何配置,不触发 dump(避免 STW),可安全运行
set&nbsp;-uo pipefail

OUT="/tmp/oom-snapshot"
mkdir&nbsp;-p&nbsp;"$OUT"

echo&nbsp;"[1] cgroup v2"
if&nbsp;[ -f /sys/fs/cgroup/memory.max ];&nbsp;then
&nbsp;&nbsp;cat&nbsp;/sys/fs/cgroup/memory.max >&nbsp;"$OUT/memory.max"
&nbsp;&nbsp;cat&nbsp;/sys/fs/cgroup/memory.current >&nbsp;"$OUT/memory.current"
&nbsp;&nbsp;cat&nbsp;/sys/fs/cgroup/memory.peak >&nbsp;"$OUT/memory.peak"
&nbsp;&nbsp;cat&nbsp;/sys/fs/cgroup/memory.events >&nbsp;"$OUT/memory.events"
else
&nbsp;&nbsp;echo&nbsp;"cgroup v1 detected"
&nbsp;&nbsp;cat&nbsp;/sys/fs/cgroup/memory/memory.limit_in_bytes >&nbsp;"$OUT/limit_in_bytes"
&nbsp;&nbsp;cat&nbsp;/sys/fs/cgroup/memory/memory.usage_in_bytes >&nbsp;"$OUT/usage_in_bytes"
fi

PID="$(pgrep -f java | head -1)"
if&nbsp;[ -z&nbsp;"$PID"&nbsp;];&nbsp;then
&nbsp;&nbsp;echo&nbsp;"no java process found"&nbsp;>&2
&nbsp;&nbsp;exit&nbsp;1
fi
echo&nbsp;"java pid:&nbsp;$PID"&nbsp;>&nbsp;"$OUT/pid"

echo&nbsp;"[2] heap info"
jcmd&nbsp;"$PID"&nbsp;GC.heap_info >&nbsp;"$OUT/heap_info"&nbsp;2>&1

echo&nbsp;"[3] gcutil"
jstat -gcutil&nbsp;"$PID"&nbsp;1000 10 >&nbsp;"$OUT/gcutil"&nbsp;2>&1

echo&nbsp;"[4] native memory"
jcmd&nbsp;"$PID"&nbsp;VM.native_memory summary >&nbsp;"$OUT/nmt"&nbsp;2>&1

echo&nbsp;"[5] vm flags"
jcmd&nbsp;"$PID"&nbsp;VM.flags >&nbsp;"$OUT/vmflags"&nbsp;2>&1

echo&nbsp;"[6] thread count"
cat&nbsp;/proc/"$PID"/status | grep Threads >&nbsp;"$OUT/threads"&nbsp;2>&1

echo&nbsp;"[7] meminfo"
cat&nbsp;/proc/meminfo >&nbsp;"$OUT/meminfo"

echo&nbsp;"done, snapshots in&nbsp;$OUT"
ls&nbsp;-lh&nbsp;"$OUT"

脚本说明:

  • 全程只读,不抓 dump(避免 STW),适合生产环境快速采集现场。

  • 输出落到 /tmp/oom-snapshot,可 kubectl cp 拉到本地分析。

  • pgrep -f java | head -1

    取第一个 java 进程,多 JVM 容器需手动指定 PID。

  • set -uo pipefail

    :未定义变量报错、管道失败传递,但不用 -e(某条命令失败不影响后续采集)。

  • 采集后记得清理 /tmp/oom-snapshot,避免残留。

6.3 拉取诊断快照到本地

bash

# 把容器内的诊断快照拷到本地(按实际 pod/namespace 替换)
kubectl&nbsp;cp&nbsp;prod/<pod>:/tmp/oom-snapshot ./oom-snapshot -c order-app

注意:kubectl cp 依赖容器内有 tar,distroless 镜像可能没有,需要换镜像或用 kubectl exec ... cat 逐个文件拉取。

6.4 常用 PromQL 速查

排查时高频用到的 PromQL(以实际 exporter 指标为准,字段名可能因版本略有差异):

# 容器内存使用率(working_set / limits)
container_memory_working_set_bytes{namespace="prod",container!=""}
/
container_spec_memory_limit_bytes{namespace="prod",container!=""}

# 容器 RSS(近似堆外 native 占用参考)
container_memory_rss{namespace="prod",container!=""}

# 容器文件缓存(可回收部分,单独看避免误判)
container_memory_cache{namespace="prod",container!=""}

# 近 5 分钟 OOM 事件
increase(container_oom_events_total{namespace="prod"}[5m])

# 近 1 小时重启次数
increase(kube_pod_container_status_restarts_total{namespace="prod"}[1h])

# 节点可用内存
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes

# 节点内存使用率(Allocatable 维度)
1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)

使用建议:这些 PromQL 在 Grafana Explore 或 kubectl get --raw "/api/v1/namespaces/monitoring/services/prometheus:9090/proxy/api/v1/query?query=..." 里验证。指标名以集群实际部署的 exporter 为准,cAdvisor 与某些自定义 exporter 的字段名存在差异,先用 {__name__=~"container_memory.*"} 查实际可用指标再套用。

七、配置示例

7.1 推荐的 Deployment 资源声明

文件路径:deploy/order-service.yaml(按实际仓库结构放置)。

yaml

apiVersion:&nbsp;apps/v1
kind:&nbsp;Deployment
metadata:
&nbsp;&nbsp;name:&nbsp;order-service
&nbsp;&nbsp;namespace:&nbsp;prod
&nbsp;&nbsp;labels:
&nbsp; &nbsp;&nbsp;app:&nbsp;order-service
spec:
&nbsp;&nbsp;replicas:&nbsp;3
&nbsp;&nbsp;selector:
&nbsp; &nbsp;&nbsp;matchLabels:
&nbsp; &nbsp; &nbsp;&nbsp;app:&nbsp;order-service
&nbsp;&nbsp;template:
&nbsp; &nbsp;&nbsp;metadata:
&nbsp; &nbsp; &nbsp;&nbsp;labels:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;app:&nbsp;order-service
&nbsp; &nbsp;&nbsp;spec:
&nbsp; &nbsp; &nbsp;&nbsp;containers:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;name:&nbsp;order-app
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;image:&nbsp;registry.example.com/order-service:1.2.3
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;resources:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;requests:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;cpu:&nbsp;"500m"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;memory:&nbsp;"2Gi"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;limits:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;cpu:&nbsp;"1000m"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;memory:&nbsp;"2Gi"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;env:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;name:&nbsp;JAVA_TOOL_OPTIONS
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;value:&nbsp;>-
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -XX:MaxRAMPercentage=60
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -XX:MaxMetaspaceSize=256m
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -XX:MaxDirectMemorySize=512m
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -XX:+HeapDumpOnOutOfMemoryError
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -XX:HeapDumpPath=/dump/heap.hprof
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -XX:NativeMemoryTracking=summary
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -XX:+ExitOnOutOfMemoryError
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;volumeMounts:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;name:&nbsp;dump
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;mountPath:&nbsp;/dump
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;readinessProbe:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;httpGet:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;path:&nbsp;/actuator/health/readiness
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;port:&nbsp;8080
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;initialDelaySeconds:&nbsp;30
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;periodSeconds:&nbsp;10
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;livenessProbe:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;httpGet:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;path:&nbsp;/actuator/health/liveness
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;port:&nbsp;8080
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;initialDelaySeconds:&nbsp;60
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;periodSeconds:&nbsp;15
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;failureThreshold:&nbsp;5
&nbsp; &nbsp; &nbsp;&nbsp;volumes:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;name:&nbsp;dump
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;emptyDir:&nbsp;{}

配置说明:

  • requests == limits

    (均为 2Gi),构成 Guaranteed QoS,节点内存紧张时不易被驱逐,也避免被调度到内存不足的节点后又被赶走。

  • -XX:MaxRAMPercentage=60

    :堆按 limits.memory 的 60% 算,约 1.2Gi,给堆外留约 800Mi 安全垫。

  • -XX:MaxMetaspaceSize=256m

    :兜底 Metaspace,防止动态类生成无限涨。

  • -XX:MaxDirectMemorySize=512m

    :兜底直接内存,Netty/NIO 场景必须设。

  • -XX:+HeapDumpOnOutOfMemoryError

    HeapDumpPath:堆 OOM 时自动 dump,便于事后分析。注意 dump 落到 emptyDir,会占节点磁盘,需配合定时清理或挂持久卷。

  • -XX:NativeMemoryTracking=summary

    :开 NMT,便于 VM.native_memory 排查,有约 5% 性能开销,权衡使用;生产可只在排查期开启。

  • -XX:+ExitOnOutOfMemoryError

    :堆 OOM 时让进程直接退出而非继续带病运行,配合 K8s 重启更快恢复,避免半死状态。该参数在 JDK 8u92+ 可用,部分早期版本不支持,使用前确认 JDK 版本。

  • Liveness failureThreshold: 5:避免单次抖动误杀,给 GC 停顿留余地。

生效方式:kubectl apply -f deploy/order-service.yaml。修改 resources 或 env 会触发滚动更新,Pod 逐个重建。

7.2 JVM 内存预算核算表

以 limits.memory: 2Gi 为例,推荐分配:

| 组成 | 推荐值 | 说明 | | — | — | — | | 堆 -Xmx | 约 1.2Gi(MaxRAMPercentage=60) | 主要对象存储 | | Metaspace | 256m | 类元数据兜底 | | 直接内存 | 512m | NIO/Netty 堆外 | | Code Cache | 默认(约 240m) | JIT 代码,一般不动 | | 线程栈 | 线程数 × 1m | 默认 -Xss1m,200 线程约 200m | | GC 开销 | 约 150m | G1 数据结构和线程 | | 安全垫 | 剩余部分 | 兜底波动,建议留 200m+ |

合计约 2Gi,留有余量。MaxRAMPercentage 与显式 -Xmx 二选一,不要同时设,否则以 -Xmx 为准可能导致混乱。

7.3 cgroup v2 下确认 limits 生效

部署后在容器内验证:

bash

cat&nbsp;/sys/fs/cgroup/memory.max
# 预期:2147483648(2Gi)
bash

# 确认 JVM 看到的内存上限
jcmd <pid> VM.flags | grep -i -E&nbsp;"MaxHeapSize|MaxRAMPercentage|UseContainerSupport"

预期看到 MaxHeapSize 约为 1.2Gi 量级,UseContainerSupport 为 true。若 MaxHeapSize 远大于 limits(如按宿主机内存算),说明容器感知未生效,检查 JDK 版本是否 ≥ 8u191。

7.4 告警规则示例

Prometheus 告警(指标名以实际 cAdvisor/exporter 为准):

yaml

groups:
&nbsp;&nbsp;-&nbsp;name:&nbsp;pod-memory
&nbsp; &nbsp;&nbsp;rules:
&nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;alert:&nbsp;PodMemoryNearLimit
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;expr:&nbsp;|
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sum(container_memory_working_set_bytes{namespace!="",container!=""})
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; by (namespace, pod, container)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sum(container_spec_memory_limit_bytes{namespace!="",container!=""})
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; by (namespace, pod, container)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; > 0.85
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for:&nbsp;10m
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;labels:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;severity:&nbsp;warning
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;annotations:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;summary:&nbsp;"{{ $labels.pod }}&nbsp;内存使用超过 limits 的 85%"

&nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;alert:&nbsp;PodOOMKilled
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;expr:&nbsp;|
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; increase(container_oom_events_total{namespace!=""}[5m]) > 0
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;labels:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;severity:&nbsp;critical
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;annotations:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;summary:&nbsp;"{{ $labels.pod }}&nbsp;发生 OOMKilled"

阈值说明:> 0.85 不是绝对标准,需结合业务基线调整。读写密集、有缓存的服务常态可能就高,要按服务分组设阈值,不要全局一刀切。

7.5 不同 GC 在容器里的取舍

JVM 参数里 GC 选择直接影响堆外开销和停顿,进而影响 limits 设置。

  • Parallel GC(-XX:+UseParallelGC):吞吐优先,老年代连续,堆外开销相对小,但 Full GC 停顿大,大堆下不适合对延迟敏感的服务。
  • G1 GC(-XX:+UseG1GC,JDK 9+ 默认):Region 化,可控停顿目标 -XX:MaxGCPauseMillis=200,堆外开销中等(Remembered Set、Card Table 等),2Gi~8Gi 堆的通用选择。
  • ZGC(-XX:+UseZGC,JDK 15+ 生产可用):低延迟,停顿 <1ms 量级,但堆外开销和元数据占用比 G1 大,需要更大安全垫;大堆(几十 GiB)且延迟敏感场景才值得。
  • Shenandoah(OpenJDK 部分发行版):低延迟,开销类似 ZGC,选用前确认你的 JDK 发行版支持。

容器内存预算影响:ZGC/Shenandoah 的堆外开销更明显,limits.memory 要多留安全垫(堆占比可降到 50%~55%),否则容易堆没满、GC 元数据把容器顶爆。G1 用 60%~70% 比较稳。

判断逻辑:选 GC 不是看哪个”更先进”,是看你的堆大小、延迟要求、容器内存预算三者匹配。2Gi 容器跑普通业务用 G1,别盲目上 ZGC。

7.6 堆 dump 与 dump 卷配置

HeapDumpOnOutOfMemoryError 产生的 dump 文件不能随便落。推荐挂独立持久卷或带保留策略的目录,避免占满节点磁盘触发磁盘压力 eviction。

yaml

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;volumeMounts:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;name:&nbsp;dump
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;mountPath:&nbsp;/dump
&nbsp; &nbsp; &nbsp;&nbsp;volumes:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;name:&nbsp;dump
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;persistentVolumeClaim:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;claimName:&nbsp;order-service-dump

PVC 示例(按实际 StorageClass 调整):

yaml

apiVersion:&nbsp;v1
kind:&nbsp;PersistentVolumeClaim
metadata:
&nbsp;&nbsp;name:&nbsp;order-service-dump
&nbsp;&nbsp;namespace:&nbsp;prod
spec:
&nbsp;&nbsp;accessModes:
&nbsp; &nbsp;&nbsp;-&nbsp;ReadWriteOnce
&nbsp;&nbsp;resources:
&nbsp; &nbsp;&nbsp;requests:
&nbsp; &nbsp; &nbsp;&nbsp;storage:&nbsp;10Gi
&nbsp;&nbsp;storageClassName:&nbsp;standard

配置说明:

  • dump 文件大小约等于堆,2Gi 堆就给 PVC 留 10Gi 以上容纳多次 dump。
  • 配合 sidecar 或 CronJob 定时清理 dump(保留最近 N 个),否则迟早写满。
  • 不要把 dump 落到 emptyDir 又不清理,emptyDir 占的是节点可写层磁盘,写满会触发节点磁盘压力 eviction,影响同节点所有 Pod。

7.7 JVM 启动参数完整模板(容器场景)

把常用容器场景 JVM 参数整理成一个可直接套用的模板(通过 JAVA_TOOL_OPTIONS 注入,避免改镜像):

-XX:MaxRAMPercentage=60
-XX:InitialRAMPercentage=60
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/dump/heap.hprof
-XX:NativeMemoryTracking=summary
-XX:+ExitOnOutOfMemoryError
-Xlog:gc*:file=/var/log/gc/gc.log:time,uptime,level,tags:filecount=5,filesize=20m

逐项说明:

  • InitialRAMPercentage=60

    :初始堆也按比例设,避免启动后堆立刻扩容造成抖动。

  • MaxGCPauseMillis=200

    :G1 停顿目标,按业务延迟要求调,不是越小越好(太小会增加 GC 频率)。

  • -Xlog:gc*

    是 JDK 9+ 统一日志格式(JDK 8 用 -XX:+PrintGCDetails -Xloggc:/var/log/gc/gc.log),落文件并滚动,便于事后用 GCViewer/gceasy 分析。

  • filecount=5,filesize=20m

    :GC 日志滚动保留 5 个 20MiB 文件,避免无限增长。

注意:-Xlog:gc* 是 JDK 9+ 语法,JDK 8 不支持,会启动失败。跨版本镜像务必按 JDK 版本选 GC 日志参数,不要混用。

7.8 内存预算核算脚本

人工算内存预算容易漏项,用一个 Shell 脚本(支持 dry-run,只输出建议不修改)辅助核算。文件路径:scripts/mem-budget.sh

bash

#!/usr/bin/env bash
# 用途:根据 limits.memory 和 JVM 参数核算内存预算,给出是否安全垫不足的判断
# 用法:./mem-budget.sh <limits_memory_mib> <xmx_mib> <metaspace_mib> <direct_mib> <thread_count> <xss_kb>
# 示例:./mem-budget.sh 2048 1228 256 512 300 1024
# 注意:本脚本只做只读核算,不修改任何配置,可安全在生产外运行

set&nbsp;-euo pipefail

if&nbsp;[&nbsp;"$#"&nbsp;-ne 6 ];&nbsp;then
&nbsp;&nbsp;echo&nbsp;"Usage:&nbsp;$0&nbsp;<limits_mib> <xmx_mib> <metaspace_mib> <direct_mib> <thread_count> <xss_kb>"&nbsp;>&2
&nbsp;&nbsp;exit&nbsp;2
fi

LIMITS_MIB="$1"
XMX_MIB="$2"
META_MIB="$3"
DIRECT_MIB="$4"
THREAD_COUNT="$5"
XSS_KB="$6"

# 线程栈总量(MiB)
THREAD_STACK_MIB=$(( THREAD_COUNT * XSS_KB /&nbsp;1024&nbsp;))
# Code Cache 预估(默认 240MiB)
CODE_CACHE_MIB=240
# GC 开销预估(G1,约堆的 12%)
GC_OVERHEAD_MIB=$(( XMX_MIB *&nbsp;12&nbsp;/&nbsp;100&nbsp;))
# JVM 自身 + 安全垫预留
JVM_BASE_MIB=128

TOTAL=$(( XMX_MIB + META_MIB + DIRECT_MIB + THREAD_STACK_MIB + CODE_CACHE_MIB + GC_OVERHEAD_MIB + JVM_BASE_MIB ))
HEADROOM=$(( LIMITS_MIB - TOTAL ))
HEADROOM_RATIO=$(( HEADROOM *&nbsp;100&nbsp;/ LIMITS_MIB ))

echo&nbsp;"limits.memory &nbsp; &nbsp; &nbsp; :&nbsp;${LIMITS_MIB}&nbsp;MiB"
echo&nbsp;" &nbsp;- Xmx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :&nbsp;${XMX_MIB}&nbsp;MiB"
echo&nbsp;" &nbsp;- Metaspace &nbsp; &nbsp; &nbsp; :&nbsp;${META_MIB}&nbsp;MiB"
echo&nbsp;" &nbsp;- Direct &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;:&nbsp;${DIRECT_MIB}&nbsp;MiB"
echo&nbsp;" &nbsp;- ThreadStack &nbsp; &nbsp; :&nbsp;${THREAD_STACK_MIB}&nbsp;MiB (${THREAD_COUNT}&nbsp;threads x&nbsp;${XSS_KB}&nbsp;KB)"
echo&nbsp;" &nbsp;- CodeCache &nbsp; &nbsp; &nbsp; :&nbsp;${CODE_CACHE_MIB}&nbsp;MiB"
echo&nbsp;" &nbsp;- GC overhead &nbsp; &nbsp; :&nbsp;${GC_OVERHEAD_MIB}&nbsp;MiB"
echo&nbsp;" &nbsp;- JVM base &nbsp; &nbsp; &nbsp; &nbsp;:&nbsp;${JVM_BASE_MIB}&nbsp;MiB"
echo&nbsp;"estimated total &nbsp; &nbsp; :&nbsp;${TOTAL}&nbsp;MiB"
echo&nbsp;"headroom &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;:&nbsp;${HEADROOM}&nbsp;MiB (${HEADROOM_RATIO}% of limits)"

if&nbsp;[&nbsp;"${HEADROOM_RATIO}"&nbsp;-lt 20 ];&nbsp;then
&nbsp;&nbsp;echo&nbsp;"[WARN] 安全垫不足 20%,堆外波动极易触发容器 OOM,建议降低 MaxRAMPercentage 或提高 limits"
&nbsp;&nbsp;exit&nbsp;1
elif&nbsp;[&nbsp;"${HEADROOM_RATIO}"&nbsp;-lt 30 ];&nbsp;then
&nbsp;&nbsp;echo&nbsp;"[NOTE] 安全垫 20%~30%,可接受但偏紧,高峰需观察"
else
&nbsp;&nbsp;echo&nbsp;"[OK] 安全垫 >= 30%"
fi

脚本说明:

  • set -euo pipefail

    :出错即停、未定义变量报错、管道失败传递,避免静默错误。

  • 所有算术用整数运算,避免浮点依赖。

  • 脚本只读输出,不写文件不连集群,可在任何环境安全运行(dry-run 思想)。

  • GC 开销按 G1 的 12% 估算,用 ZGC 应调高到 20%~25%,对应改脚本里的系数。

  • 阈值(20%/30%)不是绝对标准,按业务波动调整。脚本给出的是判断依据,不是金科玉律。

执行权限与运行:

bash

chmod&nbsp;+x scripts/mem-budget.sh
./mem-budget.sh 2048 1228 256 512 300 1024

预期输出:

limits.memory &nbsp; &nbsp; &nbsp; : 2048 MiB
&nbsp; - Xmx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; : 1228 MiB
&nbsp; ...
estimated total &nbsp; &nbsp; : 1638 MiB
headroom &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: 410 MiB (20% of limits)
[NOTE] 安全垫 20%~30%,可接受但偏紧,高峰需观察

7.9 async-profiler 定位堆外/native 分配

当 NMT 不够精确(JNI/native 库分配可能不在 NMT 统计内)时,用 async-profiler 采样 native 内存分配,定位到具体调用栈。这是堆外泄漏排查的利器。

用法(在容器内,需要下载 async-profiler,按官方 release 取版本):

bash

# 采样 native 分配,生成火焰图(示例思路,路径按实际放置)
./profiler.sh --alloc --malloc -d 60 -f /tmp/malloc.html <pid>

参数说明:

  • --malloc

    :采样 malloc 调用,定位 native 堆外分配热点。

  • --alloc

    :采样 Java 对象分配。

  • -d 60

    :采样 60 秒。

  • -f /tmp/malloc.html

    :输出火焰图 HTML。

判断逻辑:火焰图里占比最大的栈顶就是分配热点。如果是某个 native 库(RocksDB、压缩、图像)的栈帧占大头,说明是 JNI native 内存泄漏,NMT 看不到,只有 profiler 能定位。

风险提醒:async-profiler 采样有性能开销(约 1%~3%),生产环境短时采样(30~60 秒)可接受,避开极端高峰,采样文件及时清理。

7.10 Netty 直接内存泄漏检测配置

Netty 场景的直接内存泄漏,开 leakDetector 是最快定位手段。在 JAVA_TOOL_OPTIONS 或启动参数加:

-Dio.netty.leakDetection.level=ADVANCED

等级说明(按 Netty 官方):

  • DISABLED

    :关闭。

  • SIMPLE

    :默认,1% 采样率,开销极小。

  • ADVANCED

    :1% 采样率,但泄漏时打印完整访问栈,定位用。

  • PARANOID

    :100% 采样,开销大,仅排查期用。

判断逻辑:开了 ADVANCED 后,应用日志里若出现 LEAK: ByteBuf.release() was not called before it's garbage-collected,并附带分配栈,直接定位到未 release 的 ByteBuf 创建点。

注意:PARANOID 生产常开会显著拖慢,确认修复后回退到 SIMPLE。泄漏检测本身有开销,是排查期手段,不是常驻配置。

八、日志与指标观察方法

8.1 关键指标清单(以实际 exporter 指标为准)

| 指标 | 含义 | 关注点 | | — | — | — | | container_memory_working_set_bytes | 工作集,kubectl top 来源 | 与 limits 比值,OOM 直接相关 | | container_memory_rss | 匿名内存,近似进程 RSS | 堆外/native 内存参考 | | container_memory_cache | 文件缓存 | 可回收部分,单独看别误判 | | container_memory_usage_bytes | 总用量(含 cache) | 看 cache 占比时用 | | container_spec_memory_limit_bytes | limits.memory | 做比值的分母 | | container_spec_memory_request_bytes | requests.memory | 看调度和超卖 | | container_oom_events_total | OOM 事件计数 | 告警直接用 | | kube_pod_container_status_last_terminated_reason | 上次终止原因 | 含 OOMKilled 标签 | | kube_pod_container_status_restarts_total | 重启计数 | 趋势监控 | | node_memory_MemAvailable_bytes | 节点可用内存 | 节点驱逐判断 |

不同 exporter(cAdvisor、kube-state-metrics、node-exporter)版本下部分指标字段名可能略有差异,以实际环境 kubectl get --raw "/metrics" 或 Prometheus __name__ 查询为准。

8.2 日志观察

  • 容器标准输出:kubectl logs <pod> -n <ns> --previous(注意 --previous 看上次崩溃前的日志)。

  • 重点搜 OutOfMemoryErrorGC overhead limit exceededDirect buffer memoryunable to create new native thread

  • java.lang.OutOfMemoryError: Java heap space

    → 堆内。

  • OutOfMemoryError: Direct buffer memory

    → 直接内存。

  • OutOfMemoryError: Metaspace

    → Metaspace。

  • OutOfMemoryError: unable to create new native thread

    → 线程数/系统限制。

  • GC overhead limit exceeded

    → GC 回收跟不上分配,常伴随堆泄漏。

  • 节点 dmesg:通过 node-problem-detector 采集,搜 Out of memory: Killed process,能看到宿主 OOM Killer 杀的具体进程和得分。

bash

kubectl logs <pod> -n prod --previous | grep -i -E&nbsp;"OutOfMemory|GC overhead|Direct buffer|native thread"

8.3 观察方法

  • 趋势看 24~48 小时 working_set 折线,识别缓慢爬升(泄漏)还是尖峰(突发)。
  • 重启时间点与指标尖峰对齐,确认因果关系。
  • 多指标交叉:不要只看 working_set,结合 rsscache、重启次数、OOM 事件计数一起判断。例如 working_set 高但 cache 占大头,可能是文件缓存可回收,不一定是真泄漏。

8.4 Grafana 面板配置思路

OOM 排查大盘建议放四组面板,按实际数据源调整字段。

第一组:容器内存总览。

  • Panel 1:working_set 与 spec_memory_limit 双线,看是否长期贴着 limits 走。

  • PromQL(以实际 exporter 指标为准):

  container_memory_working_set_bytes{namespace="$namespace",pod=~"$pod",container!=""}

叠加 limits:

  container_spec_memory_limit_bytes{namespace="$namespace",pod=~"$pod",container!=""}
  • Panel 2:working_set / limit 比率,阈值线 0.85,超过标红。

第二组:内存构成拆解。

  • Panel 3:堆叠图,把 rsscacheswap(如有)叠在一起,看 cache 占比,避免把文件缓存误判成泄漏。

  • 注意不同 cgroup 版本/exporter 下 rss/cache 字段口径可能不同,以实际指标为准。

第三组:重启与 OOM 事件。

  • Panel 4:increase(container_oom_events_total[5m]),看 OOM 突发。
  • Panel 5:kube_pod_container_status_restarts_total,看重启趋势。

第四组:节点内存。

  • Panel 6:node_memory_MemAvailable_bytes 与 node_memory_MemTotal_bytes,标 eviction-hard 阈值线。
  • Panel 7:节点上所有 Pod 的 working_set 汇总,与节点 Allocatable 对比,看超卖。

变量设置:$namespace$pod 用 Grafana 变量,namespace 查询 label_values(kube_pod_container_status_restarts_total, namespace)pod 联动 label_values(container_memory_working_set_bytes{namespace="$namespace"}, pod)。面板字段依赖具体数据源,需按实际环境调整 label 名。

8.5 告警阈值调优方法

阈值不能拍脑袋,要按业务基线动态调整。方法:

  1. 采集稳态基线:连续 7 天记录 working_set 的 P50/P95/P99 和峰值,以及对应的业务流量。
  2. 区分服务分组:缓存型服务(如 Redis-like、本地缓存大的)常态内存高,计算型服务波动小,分组设阈值。
  3. 设阈值:
  • PodMemoryNearLimit

    :稳态 P99 的 1.1 倍与 limits × 0.85 取较小者,for: 10m 避免瞬时尖峰误报。

  • PodOOMKilled

    increase(container_oom_events_total[5m]) > 0,critical,必报。

  • PodRestartAnomaly

    increase(kube_pod_container_status_restarts_total[1h]) > 2,warning,频繁重启告警。

  1. 节点级:node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.1 对应 eviction-hard,提前告警。

阈值是起点不是终点,上线后按误报/漏报持续调整。宁可前期误报多一些,也不要漏报导致业务被 OOM 打了才知道。

8.6 多指标交叉判断速查

OOM 排查最忌讳凭单一指标下结论。下表给出常见组合判断:

| working_set 趋势 | 堆 used | Metaspace | 线程数 | Direct | 判断 | | — | — | — | — | — | — | | 缓慢单调爬升 | 接近 Xmx | 正常 | 正常 | 正常 | 堆泄漏 | | 缓慢单调爬升 | 远小于 Xmx | 正常 | 正常 | 增长 | 直接内存泄漏 | | 缓慢单调爬升 | 远小于 Xmx | 接近上限 | 正常 | 正常 | Metaspace 泄漏 | | 缓慢单调爬升 | 远小于 Xmx | 正常 | 增长 | 正常 | 线程泄漏 | | 平稳高位 | 远小于 Xmx | 正常 | 正常 | 正常 | 安全垫不足/配置型 | | 偶发尖峰 | 尖峰时接近 Xmx | 正常 | 正常 | 正常 | 突发大对象/大查询 | | 平稳但节点 MemoryPressure | – | – | – | – | 节点超卖/驱逐 |

用这张表对照 5.5 采集到的数据,能快速收敛到根因分支。前提是 NMT 开启、jstat 能跑、指标采集正常,这三样是排查的基础设施,没准备好就不要急着排 OOM。

8.7 node-problem-detector 采集宿主 OOM

容器内看不到宿主 dmesg 的 OOM 记录,生产环境用 node-problem-detector 把宿主内核事件转成 Node 事件和告警。

部署方式(DaemonSet,按官方 manifest 调整):

yaml

apiVersion:&nbsp;apps/v1
kind:&nbsp;DaemonSet
metadata:
&nbsp;&nbsp;name:&nbsp;node-problem-detector
&nbsp;&nbsp;namespace:&nbsp;kube-system
spec:
&nbsp;&nbsp;selector:
&nbsp; &nbsp;&nbsp;matchLabels:
&nbsp; &nbsp; &nbsp;&nbsp;app:&nbsp;node-problem-detector
&nbsp;&nbsp;template:
&nbsp; &nbsp;&nbsp;metadata:
&nbsp; &nbsp; &nbsp;&nbsp;labels:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;app:&nbsp;node-problem-detector
&nbsp; &nbsp;&nbsp;spec:
&nbsp; &nbsp; &nbsp;&nbsp;hostNetwork:&nbsp;true
&nbsp; &nbsp; &nbsp;&nbsp;containers:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;name:&nbsp;node-problem-detector
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;image:&nbsp;registry.k8s.io/node-problem-detector/node-problem-detector:v0.8.20
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;command:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;"/node-problem-detector"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;"--logtostderr"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;"--config.system-log-monitor=/config/kernel-monitor.json"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;volumeMounts:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;name:&nbsp;log
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;mountPath:&nbsp;/var/log
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;name:&nbsp;kmsg
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;mountPath:&nbsp;/dev/kmsg
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;name:&nbsp;config
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;mountPath:&nbsp;/config
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;readOnly:&nbsp;true
&nbsp; &nbsp; &nbsp;&nbsp;volumes:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;name:&nbsp;log
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;hostPath:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;path:&nbsp;/var/log
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;name:&nbsp;kmsg
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;hostPath:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;path:&nbsp;/dev/kmsg
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;name:&nbsp;config
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;configMap:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;name:&nbsp;node-problem-detector-config

配置说明:镜像 tag 按实际可用版本调整;kernel-monitor.json 里定义匹配 Out of memory: Killed process 等内核日志的规则,命中后上报为 Node Condition 或 Event。生产部署需结合集群版本和镜像仓库调整,以实际可用 manifest 为准。

看到 Node 事件里有 KernelDeadlock 或 OOMKilling 类条件,再结合节点上哪个 Pod 被杀,就能把宿主 OOM 和容器 OOM 区分清楚。

九、排查路径汇总

完整闭环一图流(文字版):

现象:Pod 重启 / 137 / 重启计数告警
&nbsp; │
&nbsp; ├─ kubectl describe pod
&nbsp; │ &nbsp; &nbsp; │
&nbsp; │ &nbsp; &nbsp; ├─ Reason=OOMKilled ──────────────── 容器 OOM 分支
&nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │
&nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; ├─ kubectl top --containers / Prom 趋势
&nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │
&nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; ├─ 缓慢爬升 → 泄漏
&nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; ├─ jstat/jcmd 看堆 → 堆满 → 抓 dump → MAT 分析 → 改代码
&nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; └─ 堆没满 → NMT summary → 堆外哪类大 → 对症
&nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │
&nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; ├─ 尖峰 → 突发流量/大查询 → 限流/分批/缓存优化
&nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │
&nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; └─ 稳态突杀 → limits 安全垫不足
&nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; └─ 核算内存预算 → 调 MaxRAMPercentage/Metaspace/Direct/limits
&nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; │
&nbsp; │ &nbsp; &nbsp; │ &nbsp; &nbsp; └─ 容器内 cat memory.max/current/events 确认 limits 生效
&nbsp; │ &nbsp; &nbsp; │
&nbsp; │ &nbsp; &nbsp; └─ Status=Evicted / node MemoryPressure ── 节点内存分支
&nbsp; │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; └─ describe node + free/meminfo
&nbsp; │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ├─ 节点超卖 → 扩容 / 调 requests / 给关键服务 Guaranteed QoS
&nbsp; │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; └─ BestEffort 太多 → 补齐 requests/limits
&nbsp; │
&nbsp; ├─ 修复 → apply Deployment(灰度)
&nbsp; ├─ 验证 → 重启计数不涨 / working_set 峰值 < 85% limits / oom_kill 不涨
&nbsp; ├─ 回滚 → 回退镜像 tag 或 apply 旧 yaml
&nbsp; └─ 复盘 → 沉淀告警阈值 + JVM 参数基线

9.1 快速分诊三连命令

刚收到告警、来不及细查时,用三条命令快速分诊,决定走哪条排查线:

bash

# 1. 看重启原因(OOMKilled / Evicted / 探针失败)
kubectl describe pod <pod> -n <ns> | grep -A5 -E&nbsp;"Last State|Reason|Exit Code"
bash

# 2. 看节点是否内存压力
kubectl describe node <node> | grep -A3 -E&nbsp;"MemoryPressure|Allocated resources"
bash

# 3. 看容器实时内存与 limits 对比
kubectl top pod <pod> -n <ns> --containers

分诊结论:

  • 命令 1 显示 OOMKilled + 命令 2 无 MemoryPressure → 容器 OOM,走第五节。
  • 命令 1 显示 Evicted 或命令 2 显示 MemoryPressure=True → 节点内存,走 5.2。
  • 命令 1 显示探针失败、无 OOM → 探针问题,先查 Liveness/Startup,别按 OOM 处理。
  • 命令 3 显示内存远低于 limits 却重启 → 可能是探针/异常退出,不是内存问题。

这三条命令 30 秒内出结果,能避免一上来就钻进 JVM 细节却走错方向。分诊对了,后面所有步骤才有效率。

十、常见误区与反模式

排查 OOM 时最容易踩的几个坑,单列出来对照自查。

  • 反模式 1:一看到 OOM 就加 limits.memory。加内存只是抬高天花板,泄漏类问题加多少都迟早撑爆,还会加剧节点超卖。先定位是泄漏还是配置,再决定动不动 limits。
  • 反模式 2:把 -Xmx 设成等于 limits.memory。堆外一点安全垫都不留,Metaspace/线程栈/直接内存/GC 一上来就顶满,堆还没满就被杀。这是最高频的配置错误。
  • 反模式 3:只看 kubectl top 一个数判断。working_set 高也可能是文件缓存可回收部分,要看 rss/cache 拆解和重启时间点对齐,多指标交叉。
  • 反模式 4:把节点驱逐当容器 OOM 治。节点内存不足时给单个 Pod 加 limits 无济于事,要去查节点超卖和 QoS 分布,给关键服务 Guaranteed 并扩容。
  • 反模式 5:在生产 Pod 直接抓全堆 dump。GC.heap_dump STW 且产大文件,高峰抓会拖垮服务,emptyDir 落盘可能写满节点磁盘。应在副本/影子/低峰抓。
  • 反模式 6:用 -XX:+ExitOnOutOfMemoryError 但不配 Liveness。进程退出后靠 K8s 重启,没问题;但如果同时把 Liveness failureThreshold 设成 1,GC 停顿几秒就被探针杀掉,会和 OOM 混在一起,难以分清是 OOM 还是探针误杀。failureThreshold 至少 5,给 GC 留余地。
  • 反模式 7:盲目换 ZGC 以为更省内存。ZGC 低延迟但堆外开销更大,2Gi 小容器上反而更容易因 GC 元数据吃满而 OOM。GC 选择要匹配堆大小和容器预算。
  • 反模式 8:抓了 dump 不分析就归因为”业务正常波动”。dump 一定要用 MAT 看 Leak Suspects 和 Dominator Tree,否则等于没抓。
  • 反模式 9:改完不验证就关工。必须连续观察 24~48h 重启计数和 oom_kill 不增长才算闭环,不要看重启一次没发生就判定修复。
  • 反模式 10:把 requests 设得很低、limits 设得很高。Burstable QoS 在节点内存紧张时容易被驱逐,且调度时按 requests 算,实际用得多会造成节点超卖。关键服务用 requests == limits

10.11 排查工具链与镜像准备

排查 OOM 依赖一组工具,临时找工具会耽误黄金时间。建议在基础镜像里预装,或准备一个诊断 sidecar 镜像:

  • JDK 自带:jcmdjstatjpsjmapjstack(需 JDK 完整镜像,JRE 镜像没有)。
  • Arthas:在线诊断,开销小,适合生产实时看。
  • async-profiler:native 采样,定位堆外热点。
  • MAT:离线分析 dump,本地运行。

镜像选择影响排查能力:

  • 用 JRE slim 镜像省空间,但缺 jcmd 等工具,OOM 时无法现场诊断。建议生产用 JDK 镜像或预装 Arthas 的镜像,体积换可诊断性。
  • distroless 镜像连 sh 都没有,kubectl exec 进不去,只能靠 sidecar 或 kubectl debug 临时注入调试容器。生产用 distroless 要配套 kubectl debug 流程。

kubectl debug 临时注入诊断容器(不修改原 Pod,按实际集群版本支持情况使用):

bash

# 给目标 Pod 注入一个带诊断工具的 ephemeral container
kubectl debug -it <pod> -n prod --image=registry.example.com/diag-tools:latest --target=order-app

注入后在 ephemeral container 里用 nsenter 或直接访问 /proc/<pid> 诊断目标容器进程。这种方式不重启业务 Pod,适合生产现场排查。不同 K8s 版本对 ephemeral container 支持程度不同,使用前确认集群版本。

10.12 容量规划与超卖控制

OOM 很多时候根因是节点超卖。容量规划建议:

  • 节点 Allocatable 内存 = Capacity – 系统预留(kube-reserved + system-reserved + eviction-threshold)。
  • 调度时按 requests.memory 之和 ≤ Allocatable 判断,不超这一层。
  • 实际用量(limits 或真实 working_set)可能远超 requests,造成运行时超卖。控制 limits/requests 比值,关键服务比值接近 1(Guaranteed),避免 Burstable 大量超卖。
  • 监控节点 working_set 总和与 Allocatable 的比值,超过 80% 预警,提前扩容而非等驱逐。

节点预留配置(kubelet 参数,按节点角色调整):

--kube-reserved=memory=1Gi
--system-reserved=memory=1Gi
--eviction-hard=memory.available<100Mi

这些参数影响节点可调度量,调整需重启 kubelet,生产变更窗口操作,并在一个节点验证后再批量推。

十一、风险提醒

  • kubectl exec

    进入生产容器执行诊断命令时,jcmd GC.heap_dumpjstack -Farthas dashboard 都有 STW 或额外开销,避开业务高峰,优先在副本或影子实例上做。

  • GC.heap_dump

    产生的文件与堆等大,落到 emptyDir 会占节点磁盘,可能间接导致节点磁盘压力 eviction,务必配置定时清理或挂独立持久卷并设保留策略。

  • 修改 resources.limits/requests 会触发 Pod 重建,相当于滚动更新。务必走灰度,配合 maxUnavailable/maxSurge 和 PDB 控制爆炸半径。

  • -XX:+ExitOnOutOfMemoryError

    让进程快速退出,但若你的业务对”半死不活”有自愈逻辑(如内部重连),需评估是否冲突。该参数部分 JDK 版本不支持,使用前确认。

  • NativeMemoryTracking

    有约 5% 性能开销,常开需评估;建议作为排查期临时开启,排查完关闭。

  • 节点上 dmesgfreecat /proc/meminfo 需要主机权限,不要在生产节点上随意执行其他破坏性命令,限定为只读观察。

  • 把 -Xmx 设成等于 limits.memory 是最常见的踩坑配置,务必留安全垫。

  • 堆外泄漏加 -XX:MaxDirectMemorySize 兜底只是止血,真正要修的是未释放的 ByteBuf/连接,否则只是把 OOM 时间推迟。

  • 调 threads-max/ulimit -u 解决线程泄漏是把崩溃推迟,治标不治本,且可能掩盖真正的无界线程池问题,必须配合代码层修复。

  • kubectl debug

    注入 ephemeral container 不重启业务 Pod,但仍会占用节点资源,注入后及时退出清理,避免遗留调试容器占内存。

  • 节点 kubelet 预留参数(kube-reserved/system-reserved/eviction-hard)调整需重启 kubelet,影响节点可调度量,生产务必单节点验证后批量推,留回滚(恢复旧参数)窗口。

  • ResourceQuota 调整影响整个命名空间可分配量,调大需确认集群总容量,调小可能导致已有 Pod 无法更新,变更前评估影响范围。

十二、验证方式

修复后按以下顺序验证,全部通过才算闭环:

  1. 配置生效验证:容器内 cat /sys/fs/cgroup/memory.max 等于新 limits;jcmd <pid> VM.flags 中 MaxHeapSize 符合预期。
  2. 压力验证:用与生产相近的流量或压测工具(如 wrk、JMeter)打一段高峰,观察 working_set 峰值是否低于 limits 的 85%。
  3. 稳定性验证:连续观察 24~48 小时,RESTARTS 不增长,container_oom_events_total 不增长。
  4. 告警验证:人为构造接近 limits 的内存压力(如压测),确认 PodMemoryNearLimit 告警触发且能被收敛处理。
  5. dump 可用性验证:人为触发一次堆 OOM(测试环境),确认 /dump/heap.hprof 生成且可被 MAT 打开,证明 dump 链路通。
bash

# 稳定性验证脚本示例思路(伪代码,按实际监控平台调整)
# 每 5 分钟记录一次重启次数与 oom_kill,连续 48 小时
# while true; do
# &nbsp; ts=$(date +%s)
# &nbsp; restarts=$(kubectl get pod -n prod <pod> -o jsonpath='{.status.containerStatuses[0].restartCount}')
# &nbsp; echo "$ts $restarts" >> /tmp/restart_trace.log
# &nbsp; sleep 300
# done

12.6 验证 Checklist

修复完成后逐项打勾,全部通过才算闭环:

  • [ ] 容器内 cat /sys/fs/cgroup/memory.max 等于新 limits。
  • [ ] jcmd <pid> VM.flags 中 MaxHeapSizeMaxMetaspaceSizeMaxDirectMemorySize 符合预期。
  • [ ] UseContainerSupport=trueMaxHeapSize 按 cgroup 上限算(非宿主机内存)。
  • [ ] 压测或高峰流量下 working_set 峰值低于 limits 的 85%。
  • [ ] 连续 24~48h RESTARTS 不增长。
  • [ ] 连续 24~48h container_oom_events_total 不增长。
  • [ ] memory.peak 低于 memory.max(没顶到上限)。
  • [ ] 告警链路通:人为构造接近 limits 的内存压力,PodMemoryNearLimit 能触发。
  • [ ] dump 链路通:测试环境触发堆 OOM,/dump/heap.hprof 生成且 MAT 可打开。
  • [ ] 回滚预案就绪:旧镜像 tag 和旧 yaml 已保存,rollout undo 验证过可执行。
  • [ ] 监控大盘四组面板(容器内存总览/内存构成/重启OOM/节点内存)数据正常。
  • [ ] JVM 参数基线已更新到团队规范文档。

12.7 不同场景的验证侧重

不同根因修复后,验证侧重点不同:

  • 配置型 OOM:重点验证安全垫,压测到峰值看 working_set 是否稳在 85% 以下,memory.peak 是否远离 memory.max
  • 堆泄漏:重点验证长周期(48h+)O 区是否能回落,FGC 频率是否恢复正常,不靠加内存。
  • 直接内存泄漏:重点验证 Netty leakDetector 日志不再出现 LEAKDirect 类 Committed 稳定不增。
  • 线程泄漏:重点验证线程数稳定在预期范围,不再随时间/流量增长。
  • 突发大对象:重点验证高峰场景下 working_set 不再出现陡峭尖峰,限流/分页生效。
  • 节点超卖:重点验证节点 MemAvailable 长期高于 eviction-hard 阈值,MemoryPressure 不再出现。

十三、回滚方案

任何修改都要有可回滚的路径。建议两种回滚方式都备好:

方式一:镜像 tag 回滚(推荐,最快)。

bash

# 把 Deployment 镜像回退到上一个稳定 tag
kubectl&nbsp;set&nbsp;image deployment/order-service order-app=registry.example.com/order-service:1.2.2 -n prod

或:

bash

kubectl rollout undo deployment/order-service -n prod

方式二:yaml 回滚(改了 resources/env 时用)。

bash

# 保留上一版 yaml,直接 apply 旧版本
kubectl apply -f deploy/order-service.yaml.prev

回滚注意:

  • rollout undo

    默认回退到上一个 revision,若改了多次需要 --to-revision=N 指定。

  • 回滚同样是滚动更新,仍受 PDB 和 maxUnavailable 约束,不会瞬间全切。

  • 回滚后同样要验证重启计数和内存指标,确认问题确实回到可接受状态。

  • 如果是泄漏类问题且回滚到旧版本仍有泄漏,说明泄漏在更早的版本就存在,需要继续往回追溯定位引入版本。

13.5 滚动更新策略与爆炸半径控制

修改 resources/env 触发滚动更新,要控制爆炸半径:

yaml

spec:
&nbsp;&nbsp;strategy:
&nbsp; &nbsp;&nbsp;type:&nbsp;RollingUpdate
&nbsp; &nbsp;&nbsp;rollingUpdate:
&nbsp; &nbsp; &nbsp;&nbsp;maxSurge:&nbsp;1
&nbsp; &nbsp; &nbsp;&nbsp;maxUnavailable:&nbsp;0
  • maxUnavailable: 0

    :滚动期间不减少可用副本,保证 SLA。

  • maxSurge: 1

    :最多多起 1 个新副本,控制资源占用峰值,避免新副本把节点内存打爆。

  • 配合 PDB(PodDisruptionBudget)minAvailable 保证主动驱逐时也留足副本。

yaml

apiVersion:&nbsp;policy/v1
kind:&nbsp;PodDisruptionBudget
metadata:
&nbsp;&nbsp;name:&nbsp;order-service-pdb
&nbsp;&nbsp;namespace:&nbsp;prod
spec:
&nbsp;&nbsp;minAvailable:&nbsp;2
&nbsp;&nbsp;selector:
&nbsp; &nbsp;&nbsp;matchLabels:
&nbsp; &nbsp; &nbsp;&nbsp;app:&nbsp;order-service

灰度顺序建议:先改 1 个副本观察一个完整业务周期(含高峰),确认无 OOM 再全量。可用 kubectl patch 临时把某个副本的镜像/参数指向新版本做金丝雀,或用 ArgoCD 的渐进式发布。

十四、生产环境注意事项

  • 灰度发布:内存相关变更先在 1 个副本验证,观察一个完整业务周期(含高峰)再全量。配合 maxSurge: 1, maxUnavailable: 0 滚动策略。
  • 执行窗口:JVM 参数和 limits 变更尽量选低峰,滚动更新期间会有 Pod 重建,可能短时降低可用副本数。
  • 影响范围评估:改 limits 可能影响调度(节点可分配内存变化),提前确认目标节点有足够 Allocatable 内存,避免 Pod 变成 Pending。
  • 备份与可追溯:每次变更保留旧 yaml 和镜像 tag,便于 rollout undo。变更走 GitOps(如 ArgoCD)留审计记录。
  • 权限控制:生产 kubectl apply 通过 CI/CD 或受控运维通道执行,避免个人直接操作集群;关键命名空间用 RBAC 限制写权限。
  • 监控先行:上线任何内存变更前,确保 container_memory_working_set_bytescontainer_oom_events_total、重启计数这三个指标有告警和大盘,否则改完没有观测等于盲改。
  • 不要在生产 Pod 上抓全堆 dump:优先在预发或影子流量副本抓;如必须生产抓,选低峰、单副本、抓完立即清理 dump 文件。
  • 安全垫规范团队统一:把”堆 = limits × 60%~70%、Metaspace/Direct 显式兜底、requests == limits”写进服务基线,新服务默认套用,避免逐个踩坑。
  • 容器内存与宿主内核版本相关:cgroup v2 下部分内存统计口径与 v1 不同,迁移节点时(如从 v1 节点迁到 v2 节点)要重新观察指标基线,不要假设完全一致。
  • JVM 版本统一:团队内统一 JDK 小版本(如统一 8u3xx 或 17.0.x),避免不同小版本的容器感知和默认参数差异导致行为不一致。

14.1 JVM 参数团队基线规范

把一次性排查沉淀成可复用的基线,新服务默认套用,避免逐个踩坑。推荐基线(按服务类型分档):

| 服务档位 | limits.memory | MaxRAMPercentage | MaxMetaspaceSize | MaxDirectMemorySize | GC | 说明 | | — | — | — | — | — | — | — | | 轻量服务 | 512Mi~1Gi | 60 | 128m | 128m | G1 | 纯计算、无堆外 | | 常规服务 | 2Gi~4Gi | 60 | 256m | 256m | G1 | 多数业务 | | IO 密集/Netty | 2Gi~4Gi | 55 | 256m | 512m | G1 | 直接内存大 | | 大堆低延迟 | 8Gi+ | 50 | 256m | 512m | ZGC | 延迟敏感大堆 |

通用必备参数(所有档位):

  • -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dump/heap.hprof

  • -XX:+ExitOnOutOfMemoryError

    (确认 JDK 版本支持)

  • -Xlog:gc*

    (JDK 9+)或 -XX:+PrintGCDetails(JDK 8)

  • -XX:NativeMemoryTracking=summary

    (关键服务常开,普通服务排查期开)

基线是起点,每个服务上线后根据真实指标微调,但安全垫(堆占比 ≤ 70%、堆外显式兜底、requests == limits)是硬约束,不能破。

14.2 监控大盘与告警就绪标准

任何内存相关变更上线前,确认以下观测就绪,否则等于盲改:

  • container_memory_working_set_bytes

    大盘有折线,能看 24h+ 趋势。

  • container_spec_memory_limit_bytes

    在同一图上叠加,肉眼能看是否贴顶。

  • container_oom_events_total

    有告警(increase > 0 即 critical)。

  • kube_pod_container_status_restarts_total

    有告警(increase[1h] > 2)。

  • 节点 node_memory_MemAvailable_bytes 有大盘和告警(低于阈值)。

  • node-problem-detector 部署,宿主 OOM 能转成 Node 事件。

这六项是 OOM 可观测的底线,缺一不可。没有可观测性就改内存配置,出了问题无法定位也无法验证修复是否生效。

14.3 团队协作与变更流程

OOM 排查不是运维一个人的事,需要研发和运维协作:

  • 运维负责:监控告警、容器/cgroup 层面排查、limits 与 JVM 参数基线、灰度与回滚。
  • 研发负责:dump 分析、代码层泄漏修复、业务侧大查询/限流改造、线程池与并发控制。
  • 变更流程:所有 limits/JVM 参数变更走 GitOps(yaml 提 PR → review → CI 校验 → ArgoCD 同步),留审计;生产 kubectl apply 不允许个人直接执行,通过受控通道。
  • 复盘机制:每次生产 OOM 事件出复盘文档,记录根因/修复/验证/影响/改进项,归档到团队知识库,避免同类问题重复发生。

十五、总结

K8s Pod OOM 排查的核心是把三件事分清楚:是容器 OOM 还是节点驱逐、是堆内还是堆外、是泄漏还是配置不足。这三组判断做对,后面的修复方向才不会跑偏。

实操上记住几条主线:

  • 看到 137 先 kubectl describe pod 看 Reason,不要直接断言 OOM。

  • 容器内 cat /sys/fs/cgroup/memory.max 和 memory.events 是内核视角的权威证据,比 JVM 自算更可信。

  • 堆没满却 OOM,去查堆外:Metaspace、直接内存、线程栈、GC 开销,用 jcmd VM.native_memory summary 拆解。

  • -Xmx

    永远不要等于 limits.memory,堆按 limits 的 60%~70% 设,堆外各部分显式兜底,留 25%~30% 安全垫。

  • requests == limits

    走 Guaranteed QoS,既防节点驱逐又防调度后被赶走。

  • 修复走灰度,验证看重启计数和 working_set 峰值,回滚靠镜像 tag 或旧 yaml。

  • 沉淀告警阈值和 JVM 参数基线,把一次性排查变成团队可复用的规范。

OOM 不是靠加内存解决的,是靠定位到具体哪类内存、为什么涨、谁来兜底来解决的。把这套闭环跑顺,后续遇到任何 Java Pod OOM 都能在一个可控的时间内收敛到根因并给出有依据的修复方案。

最后给一组可直接落地的行动清单,按优先级执行:

  1. 把所有 Java 服务的 -Xmx 与 limits.memory 关系排查一遍,凡 -Xmx >= limits.memory × 0.8 的,立即按 60%~70% 重设,留安全垫。
  2. 关键服务默认开 -XX:NativeMemoryTracking=summary-XX:+HeapDumpOnOutOfMemoryError-XX:MaxDirectMemorySize-XX:MaxMetaspaceSize
  3. 关键服务统一 requests == limits 走 Guaranteed QoS。
  4. 部署 node-problem-detector,把宿主 OOM 事件接进监控。
  5. 建四组监控面板 + 三条告警(near limit / oom kill / restart anomaly),阈值按 7 天基线调。
  6. 把”内存预算核算脚本”和”诊断快照脚本”纳入运维工具箱,OOM 时一键采集现场。
  7. 团队基线文档化,新服务默认套用分档 JVM 参数规范。

做完这七步,Java Pod OOM 就从”偶发事故”变成”可控的、有预案的、能快速闭环的常规问题”。

文末福利

今天给大家分享一份超级牛掰的Linux学习笔记,足足有1456页!是一位Linux运维大佬整理分享的,分享是获得大佬同意的,大家有需要的尽管收藏起来!

笔记介绍

这份笔记非常全面且详细,从Linux基础到shell脚本,再到防火墙、数据库、日志服务管理、Nginx、高可用集群、Redis、虚拟化、Docker等等,与其说Linux学习笔记,不如说是涵盖了运维各个核心知识。

并且图文并茂,代码清晰,每一章下面都有更具体详细的内容,十分适合Linux运维学习参考!

笔记展示

笔记下载

扫描下方二维码,回复暗号“1456页Linux笔记“,即可100%免费领取成功


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:马哥Linux运维 点击关注 👉 点击关注 👉《K8s Pod OOM 排查:从 limits 设置到 JVM 调优》

[PWN]Voidexec 网络安全文章

[PWN]Voidexec

文章总结: 该文档分析PWN题目Voidexec的解题思路,核心是通过mprotect修改内存权限并采用自修改shellcode技术绕过forbidden函数对
评论:0   参与:  0