-m 2g 反而更早 OOM:Docker 内存计数与宿主机空闲口径差异

本文摘要:一个 Spring Boot 文件服务用 docker run -m 2g --memory-swap 2g 部署,JVM 参数写的是 -Xmx2g。

一、问题与结论

压测持续到堆用量打印 1450 MB,进程被 SIGKILL,退出码 137;登录宿主机执行 free -h,available 列显示 48 GB。第一反应是"宿主机有内存,Docker 却杀了进程",排查方向就此被带偏。

结论是两套计数口径不同:-m 写入的是 cgroup 硬限制,计数覆盖匿名页与 page cache;宿主机 free 把 page cache 归入 buff/cache,视为可回收内存。-Xmx 只约束堆上限,与 -m 等值时 Metaspace、线程栈、Code cache 没有任何余量,几乎必然提前触发 cgroup 层的 OOM。

二、排查与选择依据

先判断 OOM 发生在哪一层。docker inspect 返回 OOMKilled: true、dmesg 出现 Memory cgroup out of memory,说明触发点在 cgroup 层,堆内的 OutOfMemoryError 没有机会抛出,进程直接被 SIGKILL。Java 堆 OOM 的特征是退出码非 137、日志出现 java.lang.OutOfMemoryError,两者不要混淆。cgroup v2 下进一步看计数构成:

bash 复制代码
cat /sys/fs/cgroup/memory.events
# oom_kill 计数
cat /sys/fs/cgroup/memory.stat | grep -E '^(anon|file)'
# anon 与 file 两类计数

file 非零说明 page cache 已经计入限额。监控指标应改为采集 memory.current(cgroup v1 对应 memory.usage_in_bytes),宿主机 free 的 available 不反映容器剩余额度。

再看 -Xmx 与 -XX:MaxRAMPercentage 的口径差异。-Xmx 是绝对字节数,只控制堆上限,非堆部分依旧占用同一批 cgroup 额度;-XX:MaxRAMPercentage 读取容器可见内存(cgroup memory.max)按百分比折算堆上限,JDK 11+ 的容器感知会自动读取并折算,无需把限额写进启动参数。两者二选一,不要叠加。

替代方案与取舍

方案 选择条件 代价 不该用的场景
-XX:MaxRAMPercentage=75 通用后端,可接受堆缩 25% 同限额下堆上限变小 堆值已按业务精确固定,必须绝对字节
放开 --memory-swap 批处理、非延迟敏感 换页抖动、尾延迟恶化 在线交易、SLA 严格
手动改 cgroup memory.high cgroup v2,可直接挂载 sysfs 脱离 Docker 参数,运维复杂 托管编排平台,无 cgroup 挂载权限

若堆值已按业务精确固定,直接下调 -Xmx 给非堆留空间,不要用 MaxRAMPercentage 覆盖既有 -Xmx。

三、关键原理

cgroup v2 的 memory.current 统计该 cgroup 下所有进程的匿名页、文件页缓存以及部分内核内存;宿主机 free 的 available 把可回收的 page cache 算作可用。同一时刻两个计数器可以分别显示 2 GB 与 48 GB,这不是统计错误,而是统计范围不同------前者是容器视角,后者是全局视角。

JVM 进程对 memory.current 的贡献可拆为:

text 复制代码
cgroup 计数 ≈ 堆(-Xmx) + Metaspace + 线程栈 + Code cache
              + Direct buffers + GC 工作区 + page cache

按 -Xmx2g 常见非堆合计 200 到 400 MB,文件读写再叠 page cache,总量即超 2 GB。-XX:MaxRAMPercentage=75 把堆上限定在约 1.5 GB,为非堆与缓存预留 500 MB,堆满时走 OutOfMemoryError 退出路径而非 SIGKILL,便于从日志定位问题。

四、可运行示例

环境:Linux 5.15+、Docker 24+、cgroup v2(stat -fc %T /sys/fs/cgroup/ 输出 cgroup2fs)。镜像基于 eclipse-temurin:17-jdk,入口命令为 java。

Dockerfile:

dockerfile 复制代码
FROM eclipse-temurin:17-jdk
WORKDIR /app
COPY OomTest.java /app/
RUN javac OomTest.java
ENTRYPOINT ["java"]

OomTest.java:

java 复制代码
import java.util.ArrayList;
import java.util.List;

public class OomTest {
  public static void main(String[] a) throws Exception {
    List<byte[]> l = new ArrayList<>();
    for (int i = 0; ; i++) {
      l.add(new byte[50 * 1024 * 1024]);
      System.out.printf("heap=%d MB, cgroup=%d MB%n", (i + 1) * 50, read());
      Thread.sleep(200);
    }
  }

  static long read() {
    for (String p : new String[]{"/sys/fs/cgroup/memory.current",
        "/sys/fs/cgroup/memory/memory.usage_in_bytes"}) {
      try {
        return Long.parseLong(
            java.nio.file.Files.readString(java.nio.file.Paths.get(p)).trim()) / 1048576;
      } catch (Exception ignored) {}
    }
    return -1;
  }
}

失败配置(不加 --rm,便于事后检查退出状态):

bash 复制代码
docker build -t oom-test .
docker run --name oom-run -m 2g --memory-swap 2g oom-test -Xmx2g OomTest

预期输出 :heap 打印到约 1.4 GB 量级时 cgroup= 已逼近 2 GB(差值来自 Metaspace、线程栈、Code cache 与 page cache,具体数值随 JDK 发行版而异),随后进程被 SIGKILL,退出码 137。

实际输出 :在宿主机执行 docker inspect --format '{``{.State.OOMKilled}} {``{.State.ExitCode}}' oom-run,输出 true 137 即确认 cgroup OOM;再执行 dmesg | grep -i oom 可见 Memory cgroup out of memory。若看到的是 java.lang.OutOfMemoryError 且退出码非 137,说明限额未按预期生效,检查 docker info 中 Cgroup Driver 与内核版本是否匹配。

常见失败:镜像内 JDK 早于 8u191,JVM 按宿主机物理内存设默认堆,heap= 打印远超 2 GB 仍未被杀。修复方法是改用 JDK 11+,或在 -Xmx / MaxRAMPercentage 中二选一。

清理与修正配置:

bash 复制代码
docker rm oom-run
docker run --rm -m 2g --memory-swap 2g oom-test -XX:MaxRAMPercentage=75 OomTest

此时堆上限约 1.5 GB,堆满时抛 OutOfMemoryError,退出码非 137。

五、验证结果与边界

按 MaxRAMPercentage=75 留余量适合多数无状态后端:堆缩 25% 换来可诊断的退出路径,运维能从日志区分 Java 堆 OOM 与 cgroup OOM。三条边界需要单独考虑:堆内数据本身贴近 2 GB 时应上调 -m 而不是调参;大文件 IO 服务的 page cache 占比高,即便堆有余量也易超限,需用 O_DIRECT 或减小读取块降低缓存压力;延迟敏感业务不要用 swap 兜底。参数折算的具体数值受 JDK 发行版影响,实施前用 java -XX:+PrintFlagsFinal -version | grep MaxHeapSize 核对实际堆上限。

参考资料

相关推荐
傲世仙尊1 小时前
线程池收尾-回调单例与可重入线程安全
linux·安全
风华同学1 小时前
Docker镜像换源
运维·docker·容器
芯次元玩家1 小时前
技术岗转物联网解决方案架构师,行业洞察、商业模式该从哪开始学?
java·开发语言
EatFan1 小时前
Spring Boot 4 升级避坑指南:依赖变化、Undertow 弃用、4.0.5 补丁与 Java 25 虚拟线程落地
java·spring boot·微服务·虚拟线程·spring boot 4
卓怡学长1 小时前
w213基于Spring Boot框架的Web安全学习系统的设计与实现
java·spring boot·spring·web安全·intellij-idea
Zhou1411361 小时前
MyBatisPlus_02_条件构造器与高级功能
java·开发语言·python
垚垚学技术_聚焦云原生2 小时前
Ansible Role 生产环境标准目录结构与规范化实践
java·elasticsearch·ansible
霸道流氓气质2 小时前
Spring AI Alibaba Graph Studio 入门指南:可视化Agent编排与调试平台
java·人工智能·spring
牢姐与蒯2 小时前
Linux信号(二).信号产生续 && 信号保存
linux·运维·服务器·ubuntu