制服大内存告警-单节点内存占用明显偏高问题解决及思路
适用场景:K8s / 类 Rancher 环境中,Java 应用「配了堆上限却像没生效」、监控内存虚高、少数节点内存长期偏高。
一、现象
- 管理平台已配置
-Xms/-Xmx(常见写在JAVA_OPTS),但业务进程内存表现与预期不符,或进程启动参数里看不到对应选项。 - 个别应用在监控上占用异常偏高(例如到三十多 G),远超所配堆上限,容易被当成内存泄漏。
- 集群整体 CPU 不高,但部分 worker 内存长期 70%~90%,另一些同类节点却明显空闲;同一类大应用 Pod 容易集中在少数节点上。
- 大应用 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
通用做法:
- 列出当前内存占用超过阈值(示例:>10Gi)的工作负载;
- 结合节点可分配内存,设定 request,使「单节点最多容纳 N 个大应用」(示例:节点约 60Gi+,request=16Gi → 大约最多 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 | 极高 | 明显回落 |
具体百分比因集群而异;关注趋势与是否仍单节点堆多个大应用。
五、可复用结论
- 先分清三件事: 参数有没有进 JVM、监控口径有没有把缓存算进去、调度有没有按 request 占位。
JAVA_OPTS≠ JVM 自动生效;直接java启动时优先JAVA_TOOL_OPTIONS(JDK 8)或改入口。- 验证看运行中进程的堆标志,不要只看
ps或环境变量。 - 大 JVM 必须设 memory request/limit:request 打散,limit 封顶;limit ≥ Xmx + 堆外余量,并参考近期峰值。
- 滚动观察用瞬时查询,避免阻塞命令拖住排障节奏。
- 过渡期新旧并存可能导致短暂更挤;以滚动结束后的分布为准。
六、涉及对象
| 对象 | 处理要点 |
|---|---|
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 注入 |