JVM 容器化内存溢出 (OOM) 治理随手记

8 阅读3分钟

JVM 容器化内存溢出 (OOM) 治理

在 Docker/Rancher 环境中,Java 应用的内存管理与传统虚拟机有巨大差异。为了防止容器被操作系统(OS OOM Killer)无情击杀,并确保 OOM 发生时能留下“临终遗言”(Dump 和告警),请遵循以下配置。

1. 内存分配核心逻辑

为什么不能设为 100%?

很多开发者错误地将容器 Limit 设为 4GB,然后把 JVM -Xmx 也设为 4GB。

Java 进程总占用 = Heap + Metaspace + CodeCache + DirectBuffer (NIO) + Thread Stack + JVM Overhead

如果堆占满了容器限制,哪怕只有一点点堆外内存请求,容器也会因超过 Cgroups 限制被 OS 直接 kill -9,此时连 Dump 都来不及生成。

经验设置公式:

  • -Xmx / MaxRAMPercentage = 容器内存 Limit 的 70% ~ 75%
  • 预留 25% ~ 30% = 给元空间、线程栈(每个线程默认 1MB)、Netty 直接内存及 OS 开销。

2. JVM 启动参数(推荐方案)

方案 A:现代优雅写法(推荐)

适用于 JDK 8u191+ / 11 / 17 / 21。能自动感知容器限制,无需手动计算内存数值。

Bash

java \
-XX:InitialRAMPercentage=75.0 \
-XX:MaxRAMPercentage=75.0 \
-XX:MaxMetaspaceSize=256m \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/app/logs/dump/ \
-XX:OnOutOfMemoryError="/app/scripts/oom_alarm.sh" \
-XX:+ExitOnOutOfMemoryError \
-jar app.jar

注意: -XX:+ExitOnOutOfMemoryError 必须放在 -XX:OnOutOfMemoryError 之后。JVM 会先尝试执行脚本,脚本执行完成后立即强制退出,确保 Rancher 能够感知到容器失败并触发重启。


3. 钉钉告警与现场保留脚本

脚本路径: /app/scripts/oom_alarm.sh

功能: 在 JVM 崩溃前,抓取瞬间的系统负载、内存状态,并向钉钉发送详细 Markdown 告警。

Bash

#!/bin/bash

# ==========================================
# 1. 核心配置
# ==========================================
DINGTALK_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=你的_TOKEN"
APP_NAME="SpringBoot-Production"
LOG_DIR="/app/logs/oom_status"
DATE_STR=$(date '+%Y%m%d_%H%M%S')
SYS_LOG_FILE="${LOG_DIR}/oom_info_${DATE_STR}.log"

mkdir -p "$LOG_DIR"

# 获取环境信息
CONTAINER_IP=$(hostname -i || echo "Unknown IP")
CONTAINER_ID=$(hostname)
CURRENT_TIME=$(date '+%Y-%m-%d %H:%M:%S')

# ==========================================
# 2. 收集快照 (防止进程退出太快导致命令失效)
# ==========================================
{
    echo "========== OOM 发生时间: $CURRENT_TIME =========="
    echo -e "\n[1. 内存状态]\n$(free -m || echo "free cmd fail")"
    echo -e "\n[2. 进程状态]\n$(top -b -n 1 | head -n 20 || echo "top cmd fail")"
    echo -e "\n[3. 磁盘空间]\n$(df -h || echo "df cmd fail")"
} > "$SYS_LOG_FILE" 2>&1

# ==========================================
# 3. 发送钉钉 Markdown 告警
# ==========================================
# 使用转义确保 JSON 安全
SAFE_APP_NAME=$(echo "$APP_NAME" | sed 's/"/\"/g')

JSON_PAYLOAD=$(cat <<EOF
{
    "msgtype": "markdown",
    "markdown": {
        "title": "OOM告警",
        "text": "### [OOM告警] 容器即将崩溃!\n\n---\n- **应用名称**: $SAFE_APP_NAME\n- **容器 IP**: $CONTAINER_IP\n- **容器 ID**: $CONTAINER_ID\n- **发生时间**: $CURRENT_TIME\n\n**说明**: JVM 发生 `OutOfMemoryError`。Heap Dump 已生成,容器即将退出并重启。\n\n**现场快照**: \n> `$SYS_LOG_FILE`\n\n请运维人员及时到宿主机挂载目录提取 .hprof 文件进行分析。"
    },
    "at": { "isAtAll": true }
}
EOF
)

curl -s -X POST "$DINGTALK_WEBHOOK" \
    -H "Content-Type: application/json" \
    -d "$JSON_PAYLOAD" >> "${LOG_DIR}/dingtalk_curl.log" 2>&1

4. 挂载与持久化建议

由于容器重启后内部文件会丢失,必须将 Dump 目录挂载到宿主机(Rancher 卷挂载):

容器内路径宿主机路径(示例)作用
/app/logs/dump//data/rancher_dumps/app_name/存储几百MB甚至数GB的 .hprof 文件
/app/logs/oom_status//data/rancher_logs/app_name/存储 OOM 发生时的系统负载日志

5. 常见问题检查清单 (Checklist)

  1. 权限确认: 在 Dockerfile 中确保脚本可执行:RUN chmod +x /app/scripts/oom_alarm.sh
  2. 工具检查: 基础镜像(如 Alpine)可能没有 curl。如果脚本无效,请在 Dockerfile 中安装:apk add --no-cache curlapt-get install -y curl
  3. Rancher 设置: Memory Limit 必须是硬限制(Hard Limit),不能只设 Reservation(预留)。
  4. Dump 清理: .hprof 文件很大,建议在宿主机配置定时任务(Crontab),清理 7 天以前的 Dump 文件。