深度剖析由CI基础镜像切换引发的 OOM

同事团队在持续集成(CI)流程中将基础镜像从CentOSjava1.8切换至AlmaLinuxjava1.8_302后,发现部分应用出现OOM(Out Of Memory)异常。本文记录完整的问题排查过程与根因分析。

JDK OOM现场

问题现象

对比两种基础镜像环境观察到的异常现象:

特征项 CentOS 环境 AlmaLinux 环境
Cgroup版本 V1 V2
进程内存消耗 OldGen 最高 7.2GB OldGen 最高 20.9GB
Heap 配置 初始堆 576MB 初始堆 2GB
容器内存感知 正确识别 36GB 错误识别宿主机 128GB
OOM 发生率 无异常 高频出现

初步分析

  1. 进程与库文件分析

通过ps auxldd命令对比进程状态与动态链接库加载情况:

1
2
3
4
5
6
7
# Alma 环境进程内存示例
2PID %MEM VSZ RSS COMMAND
32176 20.9 44944296 7913380 java -javaagent...

# CentOS 环境进程内存示例
6PID %MEM VSZ RSS COMMAND
7258 7.2 15649124 2722068 java -javaagent...

动态链接库分析未发现异常so文件加载差异。

  1. JVM 配置对比

使用jinfo验证关键JVM参数:

1
2
3
4
5
6
# Metaspace 配置对比一致
$ jinfo -flag MetaspaceSize <PID>
-XX:MetaspaceSize=21807104 (20.79MB) # 两者一致

# Heap 配置存在差异
$ jmap -heap <PID>

关键配置对比表:

配置项 CentOS 值 AlmaLinux 值
MaxHeapSize 9.6GB 32.2GB
NewSize 192MB 682.5MB
MaxNewSize 3GB 10.7GB
OldSize 384MB 1.3GB

最终发现:CentOS java1.8.0_302 根据 Pod Limit (通过挂载/proc/meminfo)计算,AlmaLinux java1.8.0_302 虽然同样挂载/proc/meminfo,但是还是根据宿主机取值。

技术背景

Cgroup V1与V2 针对OOM场景差异说明及监控优化

在 Linux 内存管理中,cgroup v1 的 rsscgroup v2 的 anonfile 有一定的对应关系,但它们的内存统计方法和细节有所不同。以下是它们之间的关系及差异分析:

1. cgroup v1 的 rss

  • 定义:

    • rss 表示进程实际使用的物理内存,主要包括:
      • 匿名内存(anon memory):进程的堆、栈,以及其他未与文件关联的内存。
      • **活跃文件映射内存 **:由文件支持并被进程使用的内存(如共享库)。
    • 特性
      • 只统计进程当前正在使用的物理内存(驻留在 RAM 中),不包括可以回收的页面缓存(如未使用的文件缓存)。
      • 与进程的 RSS 概念一致,是不可回收的部分。
  • 主要组成:

    • rss = 匿名内存(anon) + 活跃的文件映射内存(file,但不包含页面缓存)。

2. cgroup v2 的 anonfile

  • 定义:
    • **anon**:匿名内存,包括进程的堆、栈,以及其他未映射到文件的内存区域。
    • **file**:文件映射内存,包括文件页面缓存和进程正在使用的文件内存。
    • 在 cgroup v2 中,内存统计进一步细化,将 anonfile 分开,分别表示匿名内存和文件映射内存的使用量。
  • 特性:
    • **anon**:始终表示 不可回收的匿名内存
    • **file**:可以包括两部分:
      • 活跃的文件映射内存:正在使用的文件支持内存(与 RSS 对应)。
      • 页面缓存:文件数据的缓存部分,通常是可回收的。

主要差异与优化

  1. 分类更细化:

    • cgroup v1 中,rss 直接合并了匿名内存和活跃文件映射内存,无法区分。
    • cgroup v2 中,将 anonfile 分开统计,明确区分匿名内存和文件相关内存。
  2. 页面缓存的处理:

    • 在 v1 中,页面缓存归入 cache,不算在 rss 中。
    • 在 v2 中,file 包含页面缓存,同时可以通过进一步分析分离出文件缓存部分。
  3. 统计方法改进:

    • cgroup v1 的 rss 偏粗粒度,无法具体判断匿名内存和文件映射内存的使用比例。
    • cgroup v2 提供了更精细的指标,便于用户优化不同类型内存的使用。

Cgroup OOM场景对比

  1. 在 Cgroup v1 中:

    • Cgroup的内存资源控制器限制每一个控制组的Page Cache和RSS物理内存.RSSCache 作为两种不同的内存使用类型,各自独立统计和管理。
    • OOM触发必要条件:如果RSS + Cache 超过了限制(memory.limit_in_bytes),会触发内存限制。
  2. 在 Cgroup v2 中:

    • anonfile 是两个主要的内存使用类别。
    • OOM触发必要条件:如果 anon + file内存(即当前内存使用量memory.current)使用超过了 memory.max 限制,会触发内存限制。

总结

  • 在 cgroup v1 中,rss 相当于 cgroup v2 的 anon + file(活跃部分),但 v1 无法区分匿名内存和文件映射内存的详细信息。
  • 在 cgroup v2 中,内存统计更清晰,anonfile 分别表示匿名内存和文件相关内存,后者还包括可回收的页面缓存。

JDK UseContainerSupport特性

早期 JDK 版本(< 8u191)无法感知容器资源限制:

  • 根据宿主机内存设置堆大小 → 易触发 OOM
  • 使用宿主机 CPU 核数 → 线程池配置不合理

核心功能

通过 -XX:+UseContainerSupport 启用(JDK 8u191+ / JDK 10+ 默认开启支持CgroupV1,JDK 8u372+ / JDK 11+ 支持CgroupV2):

  1. 内存适配

    • 自动检测容器内存限制(如 docker run -m
    • 结合 -XX:MaxRAMPercentage=NN 按百分比分配堆内存
    1
    2
    # 示例:使用容器内存的 50%
    java -XX:+UseContainerSupport -XX:MaxRAMPercentage=50 ...
  2. CPU 适配

    • 自动识别容器 CPU 配额(如 docker --cpus
    • 用于优化并行处理(ForkJoinPool、GC 线程数等)

通过这一特性,Java 应用在容器环境中能更精准地利用分配资源,避免因资源超限导致的稳定性问题。

Linux E820

E820 是 x86 架构中用于获取系统物理内存布局的 BIOS 中断调用方法,属于计算机启动阶段内存管理的关键机制。Linux 内核通过调用 BIOS的中断来访问它,方法是将EAX 寄存器设置为十六进制值 E820(eax=E820)。它会报告哪些内存地址范围可用,哪些保留供 BIOS 使用,传统 BIOS 时代的主流内存探测方式(现代 UEFI 系统已转向新机制),Linux 内核仍支持解析 E820 表(如通过 dmesg | grep e820 查看),主要有几点应用场景:

  • 操作系统启动时物理内存初始化
  • 内存热插拔检测
  • 虚拟化环境中客户机内存分配
  • 嵌入式系统硬件资源管理

数据结构

通过 ES:DI 寄存器传递 内存条目结构体

1
2
3
4
5
6
C1struct e820_entry {
2 uint64_t base_addr; // 内存区域起始地址
3 uint64_t length; // 区域长度
4 uint32_t type; // 类型标识
5 uint32_t acpi_attr; // ACPI 扩展属性
6};

类型码(Type)

  • 1:可用内存 (Usable RAM)
  • 2:保留区域 (Reserved, 如 BIOS 或硬件占用)
  • 3:ACPI 可回收内存
  • 4:ACPI NVS 内存
  • 5:坏内存 (Bad Memory)

工作流程

  1. 系统启动时:BIOS 初始化硬件后构建内存映射表
  2. 操作系统引导:通过 INT 15h, E820h 遍历所有内存区域
  3. 数据解析:内核筛选可用内存区域并建立页表

演变

  • UEFI 替代:通过 GetMemoryMap 服务函数取代传统 BIOS 中断
  • Linux 兼容:内核仍支持解析 E820 表(如通过 dmesg | grep e820 查看)
  • 虚拟化扩展:Hypervisor 可能虚拟化 E820 表供虚拟机使用

深入探究

JVM 内存计算机制

通过java -XX:+PrintFlagsFinal -version输出对比发现关键差异:

1
2
3
4
- uintx InitialHeapSize = 576MB       # CentOS
+ uintx InitialHeapSize = 2GB # AlmaLinux
- uintx MaxHeapSize = 9.6GB
+ uintx MaxHeapSize = 32.2GB

JVM 内存计算逻辑:

  1. 先判断是否容器内
    a. 判断是否支持UseContainerSupport
    b. 通过/proc/self/mountinfo判断各子系统是否存在,我们在这里退出
  2. 不在容器内,则调用Linux::physical_memory()获取物理内存
  3. 物理内存: _physical_memory = (julong)sysconf(_SC_PHYS_PAGES) * (julong)sysconf(_SC_PAGESIZE);

glibc 行为变更

关键 glibc PR(0ce657c5)变更:

1
2
3
4
5
// CentOS glibc 2.17
get_phys_pages() -> 读取 /proc/meminfo

// AlmaLinux glibc 2.34
get_phys_pages() -> 调用 sysinfo 系统调用

导致容器内进程直接获取宿主机内存信息而非/proc/meminfo值。

sysinfo系统调用

sysinfo

  1. _SC_PHYS_PAGES对应glibc函数为__get_phys_pages,其中使用glibc封装__sysinfo函数

  2. __sysinfo对应linux内核sys_sysinfo系统调用,返回sysinfo结构体

  3. sysinfo结构体中totalram字段为目标返回值:totalram_pages()

  4. totalram_pages()返回字段为全局页面数目 _totalram_pages 变量

  5. _totalram_pages变量在内核启动过程中memblock 内存页帧分配器填充,具体初始化细节如下:

    1
    2
    3
    4
    5
    6
    start_kernel()
    setup_arch()
    e820__memory_setup()
    memblock_set_current_limit(ISA_END_ADDRESS)
    e820__memblock_setup()
    ...

总结

CentOS使用2.17 glibc通过读取/proc/meminfo获取内存信息,而AlmaLinux的2.34 glibc改用sysinfo系统调用,直接获取宿主机内存,导致JVM根据错误的内存值计算堆大小,进而引发OOM。glibc获取内存从 /proc/memory 变更为 sysinfo系统调用,sysinfo获取的值为宿主机内存大小,这与我们通过lxcfs挂载/proc/memory的行为存在错位.

  1. 短期方案

    1
    2
    # 强制指定堆内存参数
    export JAVA_OPTS="-Xmx4g -Xms4g -XX:MaxRAMPercentage=75"
  2. 长期方案

    1. 升级JDK版本至8u372
    2. 确保基础镜像Glibc版本一致性

参考

  1. https://www.cnblogs.com/aozhejin/p/17217813.html
  2. http://www.wowotech.net/memory_management/meminfo_1.html
  3. https://richardweiyang-2.gitbook.io/kernel-exploring/nei-cun-guan-li/00-memory_a_bottom_up_view/01-e820_retrieve_memory_from_hw
  4. https://tinylab.org/riscv-memblock/
  5. https://github.com/torvalds/linux/blob/feffde684ac29a3b7aec82d2df850fbdbdee55e4/mm/show_mem.c#L77
  6. https://zhuanlan.zhihu.com/p/508597947
  7. https://hwguo.github.io/blog/2015/05/31/linux-freemem/
  8. https://github.com/bminor/glibc/blob/776938e8b8dcf2b59998979e91cc0f9db7d771a8/sysdeps/unix/sysv/linux/getsysstats.c#L249
  9. https://cloud.tencent.com/developer/article/2352781
  10. https://bugs.openjdk.org/browse/JDK-8272124
  11. https://bugs.openjdk.org/browse/JDK-8230305
  12. https://github.com/bminor/glibc/commit/0ce657c576bf1b2436c4e14a002eaf461897d82c#diff-18d264a00a00688651894bf5a7ef0d80b9d75b33054b24e5adb12086deb416b7R317