💡Tip

服务在虚拟机上跑得好好的,容器化上 K8s 后却频繁触发内存超限告警,甚至 OOM Kill?
堆内存才用了不到 10%,Pod 内存却飙到 80% 以上,到底是什么情况?
本文将层层拆解,揭开容器内存暴涨的真相。

诡异的现象:JVM 很健康,Pod 却要“撑爆”了

Java 服务容器化部署到 Kubernetes 后,Pod 内存使用率持续缓慢上升,频繁超过 80%,甚至触发 OOM 导致 Pod 重启。

资源配置如下:

1
2
3
4
5
6
7
8
9
resources:
requests:
memory: 2200Mi
cpu: 1000m
limits:
memory: 3000Mi
cpu: 3000m

JAVA_OPTS='-Xmx2000m'

按理说,JVM 堆内存只占 2000Mi,Pod limit 给了 3000Mi,还有近 1G 的空间给堆外、Metaspace 等,应该足够富裕。可为什么内存会超限呢?

排查过程:堆内存正常,堆外也正常,那内存去哪了?

首先排查 JVM 堆内存

进入容器,用 jhsdb jmap --heap 查看堆使用情况:

1
2
3
4
5
6
Heap Usage:
G1 Heap:
regions = 2000
capacity = 2097152000 (2000.0MB)
used = 168122616 (160.33MB)
8.01% used

堆内存仅用了 160MB,占比 8%,完全正常。GC 也健康,没有内存泄漏迹象。

排查堆外内存

jstat -gc 显示 Metaspace 使用很小,代码中也没有 DirectByteBuffer 等直接内存操作。堆外内存嫌疑暂时排除。

排查 Pod 整体内存

查看 cgroup 的内存统计文件

1
cat /sys/fs/cgroup/memory/memory.stat

关键输出:

1
2
cache 741126144      # 706 MB
rss 2392379392 # 2281 MB

Pod 实际物理内存 RSS 约 2.28GB,而 cache(页缓存)竟然占用了 706MB
Pod 内存总量 = RSS + cache ≈ 3GB,已经接近 3GB 的 limit,所以触发告警。

根源分析:谁制造了这么多 cache?

容器中日志先写入本地文件,再由采集器(SLS)读取发送。

Linux 的 Page Cache 用来缓存文件读写,频繁的文件写入和读取,导致大量文件数据被缓存在 Page Cache 中,而这些 cache 会计入容器的内存使用(cgroup v1 行为),最终撑爆 Pod 内存。

关键点

  • 宿主机内存充足,内核不会主动回收 Page Cache。

  • 容器内无法执行 drop_caches(只读文件系统)。

  • 即使 RSS 只有 2.3GB,加上 0.7GB cache 后刚好超限,引发 OOM。

我们尝试在宿主机上清空 cache(echo 3 > /proc/sys/vm/drop_caches),Pod 内存瞬间下降 700MB,证实了 cache 就是元凶。

解决方案:三种方法根治“cache 暴涨”

日志直接输出到标准输出(最佳实践)

原理:应用日志不写本地文件,直接 console 输出,由容器运行时(docker/containerd)捕获并转发给日志中心(如 SLS、ELK)。
优点

  • 完全避免 Page Cache 产生,内存占用只来自 RSS。
  • 符合云原生 12 要素,K8s 原生支持。
  • 无需改造日志采集系统。

保留本地日志时的缓解措施

如果因合规要求必须保留日志文件,可采用以下方法:

  1. 优化日志策略:异步写入、调高日志级别、限制日志量。
  2. 调整内核参数(宿主机操作,会影响所有POD慎重操作,不建议):
    1
    2
    3
    sysctl -w vm.vfs_cache_pressure=200   # 积极回收 cache
    sysctl -w vm.dirty_ratio=10 # 降低脏页比例
    sysctl -w vm.dirty_background_ratio=5

启用 cgroup v2(K8s 1.20+ 推荐)

cgroup v2 改进了内存回收机制,当 Pod 内存超过 memory.high(默认 limit 的 80%)时,内核会主动回收 Page Cache,无需人工干预。

K8s 集群启用 cgroup v2 后,只需配置 Pod 的 limits.memory,kubelet 自动设置 memory.high,即可自动抑制 cache 增长。
检查是否生效:

1
cat /sys/fs/cgroup/memory.high   # 应该等于 limit*0.8

容器化不是简单的“打包扔上去”,内存管理涉及 JVM、cgroup、内核缓存三层。希望本文的排查思路和解决方案能帮你避开“日志写爆 cache”的大坑。