找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1538

积分

0

好友

193

主题
发表于 2025-12-25 12:01:11 | 查看: 30| 回复: 0

问题现象

将生产环境的JDK从8升级到JDK21后,系统出现多次内存溢出(OOM)告警,并伴有Pod重启的现象。

概念梳理

在深入分析之前,我们先明确几个关键概念,熟悉相关内容的读者可以直接跳转到【OOM分析】部分。

组件、Pod与容器的关系

在Kubernetes中,一个Deployment(组件)管理多个Pod实例,而一个Pod内可以运行多个容器。Pod可类比为房子,容器则是其中的房间。一个典型的包含Sidecar容器的Deployment配置如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-with-sidecar-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
      - name: main-nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
      - name: sidecar-monitor
        image: busybox:1.35

JVM堆内存、NMT与WorkingSet的关系

  1. 容器内存工作集(Workingset, WSS):操作系统维度,统计容器当前运行所需的所有活跃和近期被访问的内存页。
  2. NMT(Native Memory Tracking):JVM维度,追踪JVM自身(包括堆和堆外)向操作系统申请的内存。它分为reserved(预留地址空间)和committed(实际提交使用)两部分。
  3. JVM堆内内存:JVM管理的堆区域,是NMT统计的一部分,也直接影响WSS。

通常的关系为:Pod总内存 > Workingset > NMT > JVM堆内内存。但需注意,JVM的NMT不会统计Glibc内存分配器(Arena)的元数据开销。

K8S的OOM Killer决策机制

Kubernetes主要依据容器的WorkingSet是否接近其内存限制(memory limit)来决定是否终止容器。即使JVM自身内存(NMT)占用不高,只要WSS超标,就会触发OOMKilled

ZGC垃圾回收器的内存特性

ZGC为实现低延迟,会在启动时预分配大量堆外内存维护其数据结构。JDK21默认开启了-XX:+AlwaysPreTouch参数,导致堆内存物理页在启动时即被完全提交,增大了启动时的物理内存占用。若设置-Xms等于-Xmx,ZGC将隐式禁用内存归还功能,不利于内存回收。

Arena概念

Arena是Glibc内存分配器为缓解多线程分配竞争而设计的独立内存池。每个操作系统线程(pthread)可能拥有自己的Arena(默认最大数量可达CPU核心数的8倍),用于缓存malloc分配的内存。即使线程闲置,其Arena占用的内存也可能被内核记为inactive_anon(非活跃匿名页),而不会被立即释放,导致“幽灵内存”累积。

常见触发Arena分配的场景包括:Tomcat请求处理线程、JDBC驱动操作、Redis客户端缓冲区分配以及日志框架的堆外操作等。

OOM分析

现象与配置

服务Pod因OOMKilled重启。容器内存限制为4GB(4096M)。应用的JVM参数为 -Xms3g -Xmx3g

初步排查

通过监控和jcmd查看NMT,发现JVM堆内及自身管理的堆外内存(committed)稳定在约3.6GB,使用率不足30%,排除JVM内部内存泄漏的可能。

锁定问题:WSS异常增长

监控显示,容器的WorkingSet持续增长并逼近4GB限制。通过分析容器的memory.stat文件,发现增长主要来自inactive_anon(非活跃匿名内存)。

cat /sys/fs/cgroup/memory/memory.stat | awk '/^inactive_anon/ {tc=$2} /^active_anon/ {tr=$2} /^active_file/ {tif=$2} END {printf \"WSS Total: %.2f M\\n\", (tc + tr + tif)/(1024*1024)}'

对比不同时间点的数据,RSSinactive_anon同步大幅增长,而shmem(共享内存)未变,表明增长的是进程私有的匿名内存页。这与Glibc Arena的内存行为高度吻合:Arena分配的内存即使不再使用,也常作为inactive_anon滞留,不被迅速归还给操作系统。

使用pmap与NMT对比工具(如开源脚本memleak.sh),也证实了存在大量NMT未追踪的内存差异,进一步指向了JVM之外的系统堆外内存分配。

分析与解决方案

综合现象,根本原因是JDK21环境下,应用(可能是框架或驱动通过JNI调用)触发了Glibc创建过多的Arena,导致堆外内存(inactive_anon)在容器中不断累积,最终使WorkingSet超出K8s内存限制。

解决方案是限制每个进程可创建的Arena数量。通过设置环境变量MALLOC_ARENA_MAX,可以有效控制这部分内存开销。对于常规的JavaSpringBoot应用,设置为4是一个合理的起始值。

优化与结果

优化后的K8s容器配置

env:
  - name: MALLOC_ARENA_MAX
    value: “4”
  - name: JVM_OPTS
    value: -XX:+UseContainerSupport -XX:InitialRAMPercentage=50.0 -XX:MinRAMPercentage=50.0 -XX:MaxRAMPercentage=65.0

优化效果

  1. WSS稳定:设置后,容器WorkingSet稳定在3900M左右,不再持续增长。
  2. 内存差异减少pmap与NMT的对比显示未追踪的内存差异显著减少。
  3. 问题解决:应用未再发生OOMKilled重启。

其他JVM优化建议

  1. 避免固定堆大小-Xms-Xmx不要设置相同值,以便ZGC能向操作系统归还空闲内存。
  2. 使用容器感知参数:推荐使用-XX:+UseContainerSupport配合-XX:MaxRAMPercentage等参数,让JVM根据容器配额动态调整堆大小,这比写死的-Xmx参数更适应Kubernetes环境。
  3. 调整ZGC内存归还延迟:若内存非常紧张,可考虑调低-XX:ZUncommitDelay(默认300秒),但需注意这可能会增加GC频率,影响性能。

总结

Kubernetes中的容器发生OOM时,首先应通过NMT等工具排查JVM自身内存。若JVM内存稳定,则需转向分析容器WorkingSet,特别是memory.stat中的inactive_anon项。若其持续增长,很可能是Glibc Arena导致的操作系统堆外内存泄漏。通过合理设置MALLOC_ARENA_MAX环境变量,可以有效遏制此类内存增长,解决因升级JDK21(特别是使用ZGC)后出现的堆外OOM问题。




上一篇:基于AT32F405与SM4算法的安全加密U盘设计与实现方案
下一篇:技术专家转型管理者为何困难重重:懂技术与管理是两回事
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 18:32 , Processed in 0.297574 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表