igozhang

——

    制服大内存告警-单节点内存占用明显偏高问题解决及思路

    适用场景:K8s / 类 Rancher 环境中,Java 应用「配了堆上限却像没生效」、监控内存虚高、少数节点内存长期偏高。


    一、现象

    1. 管理平台已配置 -Xms/-Xmx(常见写在 JAVA_OPTS),但业务进程内存表现与预期不符,或进程启动参数里看不到对应选项。
    2. 个别应用在监控上占用异常偏高(例如到三十多 G),远超所配堆上限,容易被当成内存泄漏。
    3. 集群整体 CPU 不高,但部分 worker 内存长期 70%~90%,另一些同类节点却明显空闲;同一类大应用 Pod 容易集中在少数节点上。
    4. 大应用 Deployment 普遍未配置 memory request / limit,调度侧几乎无内存占位约束。

    二、原因

    1. 配置写了,启动路径没接上

    管理界面里常见:

    • 键:JAVA_OPTS
    • 值:-Xms16g -Xmx16g ...

    期望:启动时堆大约限制在 16G。

    若镜像是直接 java -jar ...,中间没有脚本去拼 $JAVA_OPTS,则环境变量在、进程参数里没有。
    JAVA_OPTS 只是运维约定名,JVM 默认不会读它 → 看起来「配了」,实际「没进进程」。

    2. 监控偏大 ≠ 一定泄漏

    某应用监控一度到三十多 G,容易误判泄漏。

    拆开看往往是:

    • 进程真实常驻(RSS)只有一部分(例如约十 G 量级);
    • 其余大量是文件页缓存(读盘加速,内存紧时内核可回收)。

    监控常用「容器工作集」口径,容易把缓存算进去 → 数字虚高
    另:-Xmx 只约束堆;进程总占用通常还会多出堆外(类元数据、线程、Direct 等),比 Xmx 多出约 2G 很常见,需与「虚高的缓存」区分。

    3. 不申报内存占位 → 大应用挤到少数节点

    大应用若不写 memory request/limit,调度会按「几乎不占内存」来排。
    结果:部分节点 70%~90%,部分节点很空——不是单机坏了,是没按真实体量占座


    三、解决方案

    1. 让堆参数真正进 JVM

    在「直接 java -jar、又不想改镜像入口」时:把环境变量改为 JVM 会自动读取的名字,例如 JDK 8 用 JAVA_TOOL_OPTIONS(值可仍是 -Xms/-Xmx/...)。

    也可改启动脚本 / Deployment command 显式展开 $JAVA_OPTS,或升级镜像入口。
    改完需重建 Pod,并用正在跑的进程验证堆上限(不要只看环境变量是否存在)。

    注意:JAVA_TOOL_OPTIONS不会出现在 ps 命令行里,需用 jinfo / 启动日志(如 Picked up JAVA_TOOL_OPTIONS)确认。

    2. 按实测给大应用补 request / limit

    通用做法:

    1. 列出当前内存占用超过阈值(示例:>10Gi)的工作负载;
    2. 结合节点可分配内存,设定 request,使「单节点最多容纳 N 个大应用」(示例:节点约 60Gi+,request=16Gi → 大约最多 3 个);
    3. limit 略高于「Xmx + 堆外余量」及近期峰值(示例:峰值近 19Gi 则 limit 用 22Gi,其它可用 20Gi)。
    应用 request(示例) limit(示例)
    mes-02 16Gi 22Gi
    mes-01 16Gi 22Gi
    mdm-01 16Gi 20Gi
    qms-01 16Gi 20Gi
    wms-01 16Gi 20Gi
    • request:决定调度密度(打散);
    • limit:防止单容器无限涨(封顶)。

    3. 滚动重建,让分布按新 request 重排

    只改 YAML 不会自动挪走旧 Pod。滚动后调度器按新 request 选节点。
    过渡期会新旧并存,个别节点可能短暂更高,属预期;旧 Pod 退出后应回落。


    四、通用排查与执行骨架

    在可执行 kubectl 的机器上操作;命名空间与应用名按环境替换。
    说明与命令分开;变更步骤单独标明。

    4.1 摸清:谁大、有没有占位、参数进没进进程

    列出占用偏高的 Pod(阈值可改):

    kubectl top pods -A --no-headers | awk '
    $4 ~ /Gi$/ { v=$4; sub(/Gi$/,"",v); if (v+0>=10) printf "%s\t%s\t%s\t%s\n",$1,$2,$3,$4 }
    ' | sort -k4 -h -r
    

    看 Deployment 是否已设 request/limit:

    kubectl -n ns-app get deploy mes-01 mes-02 mdm-01 qms-01 wms-01 \
      -o custom-columns=NAME:.metadata.name,REQ:.spec.template.spec.containers[0].resources.requests.memory,LIM:.spec.template.spec.containers[0].resources.limits.memory,REPLICAS:.spec.replicas
    

    抽查某个应用:环境变量 vs 进程参数:

    POD=$(kubectl -n ns-app get pod -l app=qms-01 -o jsonpath='{.items[0].metadata.name}')
    echo "POD=$POD"
    kubectl -n ns-app exec "$POD" -- printenv JAVA_OPTS JAVA_TOOL_OPTIONS 2>/dev/null
    kubectl -n ns-app exec "$POD" -- ps -o pid,args -p 1
    

    若需区分「进程 RSS」与「容器缓存虚高」:

    POD=<pod-name>
    kubectl -n ns-app exec "$POD" -- sh -c 'awk "/^VmRSS|^VmHWM/ {print}" /proc/1/status; grep -E "^(cache|rss|inactive_file|active_file) " /sys/fs/cgroup/memory/memory.stat'
    

    节点维度:

    kubectl top nodes
    

    思路小结: Top 大 → 看 request 是否为空 → 看 cmdline / 堆标志是否含 Xmx → 必要时拆 RSS vs cache → 再决定是「参数未注入」「口径问题」还是「调度挤兑」。

    4.2 变更:注入方式(平台或 YAML)

    变更说明: 改环境变量并重建 Pod。

    • 平台:将 JAVA_OPTS 改为 JAVA_TOOL_OPTIONS(或等价地改入口脚本)。
    • 验证(路径按镜像内 JDK 调整):
    POD=$(kubectl -n ns-app get pod -l app=qms-01 -o jsonpath='{.items[0].metadata.name}')
    kubectl -n ns-app exec "$POD" -- printenv JAVA_TOOL_OPTIONS
    kubectl -n ns-app exec "$POD" -- sh -c 'JINFO=$(ls /usr/local/jdk*/bin/jinfo 2>/dev/null | head -n 1); "$JINFO" -flag MaxHeapSize 1; "$JINFO" -flag InitialHeapSize 1'
    

    期望:MaxHeapSize / InitialHeapSize 与目标堆一致(例如 16Gi ≈ 17179869184)。

    4.3 变更:批量写入 memory request / limit

    变更说明: 修改 Deployment,触发滚动重启,需业务窗口。

    NS=ns-app
    for item in \
      'mes-02:16Gi:22Gi' \
      'mes-01:16Gi:22Gi' \
      'mdm-01:16Gi:20Gi' \
      'qms-01:16Gi:20Gi' \
      'wms-01:16Gi:20Gi'
    do
      DEP=${item%%:*}; REST=${item#*:}; REQ=${REST%%:*}; LIM=${REST#*:}
      CNAME=$(kubectl -n "$NS" get deploy "$DEP" -o jsonpath='{.spec.template.spec.containers[0].name}')
      echo "PATCH $DEP container=$CNAME request=$REQ limit=$LIM"
      kubectl -n "$NS" patch deploy "$DEP" --type=strategic -p \
        "{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"$CNAME\",\"resources\":{\"requests\":{\"memory\":\"$REQ\"},\"limits\":{\"memory\":\"$LIM\"}}}]}}}}"
    done
    

    核对:

    kubectl -n ns-app get deploy mes-01 mes-02 mdm-01 qms-01 wms-01 \
      -o custom-columns=NAME:.metadata.name,REQ:.spec.template.spec.containers[0].resources.requests.memory,LIM:.spec.template.spec.containers[0].resources.limits.memory,READY:.status.readyReplicas,DESIRED:.spec.replicas
    

    4.4 观察滚动(避免长时间阻塞)

    不要用无超时的 kubectl rollout status 死等。用瞬时查询:

    NS=ns-app
    kubectl -n "$NS" get deploy mes-01 mes-02 mdm-01 qms-01 wms-01 \
      -o custom-columns=NAME:.metadata.name,DESIRED:.spec.replicas,UPDATED:.status.updatedReplicas,READY:.status.readyReplicas,AVAILABLE:.status.availableReplicas
    
    kubectl -n ns-app get pods --no-headers | grep -E 'mes-01-|mes-02-|mdm-01-|qms-01-|wms-01-' | awk '{print $1,$3,$5}' | sort
    
    kubectl -n ns-app get pods --no-headers | grep -E 'mes-01-|mes-02-|mdm-01-|qms-01-|wms-01-' | grep -Ev 'Running|Completed' | head -n 20
    
    kubectl top nodes
    

    完成标准:

    • 各 Deployment:UPDATED / READY / AVAILABLE == DESIRED
    • 仅剩新版本 Pod 且均为 Running
    • 无长期 Pending / CrashLoopBackOff
    • 节点内存分布相对拉平(原先打满的节点回落,空闲节点承接部分大应用)

    4.5 结果形态

    滚动结束后,副本进度齐套;节点由「少数很高、少数很空」变为更均衡。例如:

    节点 变更前内存 变更后内存
    node-a 中高
    node-b
    node-c
    node-d 中低(已接到大应用)
    node-e
    node-f 极高 明显回落

    具体百分比因集群而异;关注趋势与是否仍单节点堆多个大应用


    五、可复用结论

    1. 先分清三件事: 参数有没有进 JVM、监控口径有没有把缓存算进去、调度有没有按 request 占位。
    2. JAVA_OPTS ≠ JVM 自动生效;直接 java 启动时优先 JAVA_TOOL_OPTIONS(JDK 8)或改入口。
    3. 验证看运行中进程的堆标志,不要只看 ps 或环境变量。
    4. 大 JVM 必须设 memory request/limit:request 打散,limit 封顶;limit ≥ Xmx + 堆外余量,并参考近期峰值。
    5. 滚动观察用瞬时查询,避免阻塞命令拖住排障节奏。
    6. 过渡期新旧并存可能导致短暂更挤;以滚动结束后的分布为准。

    六、涉及对象

    对象 处理要点
    Deployment/mes-02 request/limit;确认 JVM 注入
    Deployment/mes-01 request/limit;确认 JVM 注入
    Deployment/mdm-01 request/limit;确认 JVM 注入
    Deployment/qms-01 JAVA_TOOL_OPTIONS + request/limit
    Deployment/wms-01 request/limit;确认 JVM 注入

    MP3