作为一名云原生时代的运维工程师,我们每天都在与 Kubernetes (K8s) 和各种应用打交道。我们深信,现代化的可观测性体系是我们保障系统稳定性的“眼睛”。然而,当这双“眼睛”看到的数据与我们的“常识”相悖时,困惑便油然而生。

一、核心问题现象

9月30日,也就是放假的前一天的下午,用我们平台的业务工程师过来问我“我这边有个疑问,就是jvm有垃圾回收机制,比如说我申请的最大内存为10G,然后就是我有一次使用了8.2G,但是我使用完了jvm会进行垃圾回收,回收后内存肯定都有降低,但是从电子云来看一直是使用了8.2G,从平台监控看Pod 的内存占用率始终维持在一个很高的水平(例如 80%),并持续触发告警

二、根本原因:监控视角与 JVM 内存机制的错位

监控视角不同

  • K8s/云平台监控:监控的是整个 Pod (容器) 的内存使用量,这是从操作系统 (OS) 的视角看的。

  • JVM GC:管理的是 JVM 堆内存 (Heap) 的内部使用情况,这是从Java 虚拟机的视角看的。

JVM 的内存预占机制

  • Java 应用启动时,通过 -Xmx 参数(如 -Xmx8g)指定的堆内存,会立即向操作系统“预定”或“提交”这块虚拟内存空间。

  • 这块被预定的内存,在 JVM 的整个生命周期中,通常不会被归还给操作系统。

  • JVM 的垃圾回收 (GC),只是在这块已经预定好的“专属区域”内进行清理和整理,把空间腾出来供自己后续使用,而不是把内存还给系统。

三、一个生动的比喻:租仓库

  • Pod limit: 10g = 物业给你划了块 10G 的地皮。

  • JVM -Xmx8g = 你在这块地皮上,建了个 8G 的大仓库。

  • JVM GC = 工人在仓库内部整理货物。

  • K8s 监控 = 天上的卫星,它只能看到你 8G 的仓库一直占着地,看不到仓库里面是满是空。

  • Pod 总内存 ≈ 8G 的仓库 (堆内存) + 仓库外的小办公室 (堆外内存)。所以总占用会略高于 8G,轻易就触发了 10G * 80% 的告警线。

也就是说当 Pod 的内存 limit 是 10G,并且用 -Xmx8g 启动 Java 应用时,从监控上看,这个 Pod 的内存占用率会立刻飙升到 80% 左右并长期维持在这个水平,即使 JVM 内部通过 GC 回收了大量堆内存。需要注意的是Pod 总内存占用 ≈ JVM 堆内存 (-Xmx) + JVM 堆外内存 + 其他开销

四、解决方案与最佳实践

  1. 治标方案:调大 Pod limit

    • 做法:保持 -Xmx8g 不变,将 Pod 的 limit 从 10G 调大到 12G。

    • 原理:总地皮变大了,8G 仓库的占用百分比就降低了,不再触发告警。这为堆外内存留出了足够的缓冲空间。

  2. 治本方案:使用 JVM 动态内存参数

    • 做法:移除 -Xmx,改用 -XX:MaxRAMPercentage=75.0 这样的参数。

    • 原理:让 JVM 自动根据 Pod 的 limit 来计算并设置自己的最大堆内存。例如,limit 是 10G,JVM 会自动设置堆大小为 10G * 75% = 7.5G,天然地为堆外内存留出了 25% 的空间。这是云原生环境下的推荐做法。

  3. K8s 最佳实践:设置 requests == limits

    • 做法:将 Pod 的 spec.containers.resources 中的 requests.memory 和 limits.memory 设置成相等的值。

    • 原理:这样做会使 Pod 的服务质量 (QoS) 等级变为 Guaranteed。

    • 好处:在节点资源紧张时,K8s 会优先“驱逐”其他低优先级的 Pod,而最后才会动 Guaranteed 级别的 Pod,极大地保障了核心应用的稳定性。