K8s 集群里看到 Pod 反复重启,状态 Last State: Terminated, Reason: OOMKilled, Exit Code: 137。重启 5 次后 CrashLoopBackOff,告警群里炸开。本文是我们这两年遇到的 6 种 OOMKilled 真实原因,每种都附诊断命令和修法 —— 不是泛泛而谈"加内存",是真正告诉你内存被谁吃了。
第一件事:OOMKilled 是什么
Exit Code 137 = 128 + 9 = SIGKILL,Pod 进程被强制杀死。但 K8s 里有两种 OOMKilled,完全是两个故事:
- 容器内存超 limits:K8s cgroup 把容器内的某个进程 OOM 杀掉。Pod 状态显示
OOMKilled - 节点内存不足:Linux 内核全局 OOM Killer 出动,杀掉评分最高的进程。可能杀的是你的容器,也可能是其他容器
先用这几条命令区分:
# 1. 看 Pod 上次终止原因
kubectl describe pod my-app-pod-xxx | grep -A 5 'Last State'
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137
# 2. 看具体被杀的进程时刻
kubectl logs my-app-pod-xxx --previous --tail=200
# 3. 看节点系统日志(确认是 cgroup OOM 还是系统 OOM)
kubectl debug node/<node-name> -it --image=busybox -- sh
# 进入后:
cat /var/log/messages | grep -i 'oom\|killed'
# 4. 用 events 看 Pod 死亡时刻是不是和节点压力相关
kubectl get events --field-selector reason=OOMKilled --sort-by='.lastTimestamp'
区分清楚之后再对症下药。下面是 6 种最常见的原因。
原因 1:Java 堆 + Metaspace + Direct Memory 加起来超 limits
这是 Java 应用上 K8s 最经典的坑。开发以为 -Xmx2g 就保证最大 2GB,结果容器 limits 设了 2.5GB,跑两小时就被 OOMKilled。
JVM 实际占用内存 = 堆(Heap)+ Metaspace + Code Cache + Direct Buffer + JIT 缓冲 + 线程栈 + GC 缓冲。一个跑 200 线程的 Web 应用,堆 2GB 时,总内存可能去到 3GB。
# 看 JVM 各内存区域实际占用(在容器内)
kubectl exec -it my-java-pod -- jcmd 1 VM.native_memory summary
# 输出片段:
# Native Memory Tracking:
# Total: reserved=4521MB, committed=2891MB
# - Java Heap (reserved=2048MB, committed=2048MB)
# - Class (reserved=1052MB, committed=255MB) ← Metaspace
# - Thread (reserved=210MB, committed=210MB) ← 线程栈
# - Code (reserved=246MB, committed=92MB) ← JIT
# - GC (reserved=89MB, committed=89MB)
# - Internal (reserved=12MB, committed=12MB)
# - Symbol (reserved=20MB, committed=20MB)
# - Native Memory Tracking (reserved=8MB, committed=8MB)
# - Arena Chunk (reserved=1MB, committed=1MB)
# - Unknown (reserved=835MB, committed=164MB) ← Direct Memory + 第三方
修法:
# Deployment 里这样配 JVM:
spec:
template:
spec:
containers:
- name: app
image: my-java-app:1.0
env:
- name: JAVA_OPTS
value: >-
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=70.0
-XX:MaxMetaspaceSize=256m
-XX:CompressedClassSpaceSize=128m
-XX:ReservedCodeCacheSize=240m
-XX:MaxDirectMemorySize=512m
-Xss512k
-XX:NativeMemoryTracking=summary
resources:
requests:
memory: 2Gi
limits:
memory: 3Gi # 留 30% buffer 给堆外
关键参数:
+UseContainerSupport:JVM 自动读 cgroup limits(JDK 8u191+ 默认开)MaxRAMPercentage=70:堆最多用容器内存的 70%,留 30% 给堆外MaxMetaspaceSize:类元数据上限,默认无限,会无限增长MaxDirectMemorySize:Netty / NIO 的 Direct Memory 上限
原因 2:Go 程序的 GOGC + 容器 limits
Go 1.19 之前,Go 不知道容器 limits,GC 触发阈值按物理机内存算。容器 limits 2GB 但物理机 64GB,Go 觉得"还有大把内存可用",直到 OOM 才反应过来。
// 1.19+ 用 GOMEMLIMIT 告诉 Go 容器实际能用多少
// 在 main 里读 cgroup 自动算,或者直接环境变量
package main
import (
"os"
"runtime/debug"
)
func main() {
// 方法 1:环境变量 GOMEMLIMIT=1750MiB
// 方法 2:代码里设置(留 limits 的 87% 给 Go)
debug.SetMemoryLimit(int64(1.75 * 1024 * 1024 * 1024))
// GOGC 调小一点,频繁 GC 换内存
debug.SetGCPercent(50) // 默认 100
// 你的业务...
}
# 或者直接环境变量
env:
- name: GOMEMLIMIT
value: "1750MiB" # 容器 limits 2GB,留 250MB 缓冲
- name: GOGC
value: "50" # 默认 100,调小让 GC 更激进
用 automaxprocs 这个库自动读 cgroup CPU limits 设 GOMAXPROCS:
import _ "go.uber.org/automaxprocs"
// 启动时自动 SetMaxProcs(cgroup_cpu_quota / cgroup_cpu_period)
原因 3:Python 多进程 fork 后 RSS 翻倍
Python web app 通常用 gunicorn 多 worker。fork 之后 RSS 不会翻倍(copy-on-write),但只要任何 worker 写了对象,内存就开始分裂。
# 进容器看实际占用
kubectl exec -it my-py-pod -- ps -ef | grep gunicorn
# UID PID PPID C STIME TTY TIME CMD
# root 1 0 0 10:00 ? 00:00:01 python -m gunicorn -w 8 app:app
# root 8 1 5 10:00 ? 00:01:23 python -m gunicorn worker
# ... 8 个 worker
# 看每个 worker 实际 RSS
kubectl exec -it my-py-pod -- ps -o pid,rss,cmd -p 8,9,10,11,12,13,14,15
# PID RSS CMD
# 8 412384 gunicorn worker # 400MB
# 9 408232 gunicorn worker
# ... 8 个加起来约 3.2GB
修法:
- worker 数 = (CPU limits * 2) + 1(经验值,不要超过 8)
- 定期 reload:用
--max-requests 1000 --max-requests-jitter 100,每个 worker 处理 1000 请求后自动重启,释放内存 - preload 模式:
--preload让 worker 共享父进程加载的大对象(libraries / models)
gunicorn app:app \
--workers 5 \
--worker-class uvicorn.workers.UvicornWorker \
--max-requests 1000 \
--max-requests-jitter 200 \
--preload \
--bind 0.0.0.0:8000
原因 4:Node.js 没设 --max-old-space-size
Node.js V8 引擎默认堆上限是 1.5GB(64 位)或 700MB(32 位)。即使容器 limits 给了 4GB,Node 也只用其中 1.5GB,堆满了就 OOM。
env:
- name: NODE_OPTIONS
value: "--max-old-space-size=3072" # 容器 limits 4GB,给 V8 堆 3GB
但更常见的问题是 Node 应用缓存了大量数据在内存里,且没有上限。
// 错:无限增长的缓存
const cache = new Map();
function getCachedUser(uid) {
if (!cache.has(uid)) {
cache.set(uid, fetchUser(uid)); // 永远不淘汰
}
return cache.get(uid);
}
// 对:用 LRU 限制大小
const LRU = require('lru-cache');
const userCache = new LRU({
max: 10000, // 最多 1 万个对象
ttl: 1000 * 60 * 5, // 5 分钟过期
updateAgeOnGet: true,
});
async function getCachedUser(uid) {
let user = userCache.get(uid);
if (!user) {
user = await fetchUser(uid);
userCache.set(uid, user);
}
return user;
}
原因 5:容器内多进程,某个子进程暴涨
有些镜像里跑了 supervisord / s6 / dumb-init 加多个进程。主进程内存正常,但某个子进程(比如日志收集 sidecar / cron 任务)突然吃 1GB,触发 cgroup OOM 杀的可能就是它。
# 进容器看所有进程
kubectl exec -it my-pod -- ps -eo pid,rss,cmd --sort=-rss | head
# PID RSS CMD
# 1 45000 supervisord
# 12 982423 /usr/bin/php-fpm ← 主进程
# 45 812334 /usr/local/bin/log-collector ← 这货也快 1GB
# 88 12345 nginx: master process
# 看哪个进程被杀过(dmesg)
kubectl debug node/<node> -it --image=busybox -- sh -c "dmesg | grep -i 'killed process'"
# Out of memory: Killed process 45 (log-collector) total-vm:892145kB, anon-rss:813224kB
修法:把 sidecar 拆成单独 container,各自给资源 limits,互不影响:
spec:
containers:
- name: app
image: my-app:1.0
resources:
limits: { memory: 1Gi, cpu: 500m }
- name: log-collector
image: my-log-collector:1.0
resources:
limits: { memory: 200Mi, cpu: 100m } # 独立限制,挂了不影响主应用
原因 6:内存碎片 / 内存泄漏
程序看起来用 800MB,但实际 RSS 占 2GB —— 内存碎片或泄漏。看几个关键指标:
# 容器内看
kubectl exec -it my-pod -- cat /proc/1/status | grep -E 'VmRSS|VmPeak|VmSize|VmData'
# VmPeak: 3845620 kB ← 历史峰值
# VmSize: 2521324 kB ← 虚拟内存
# VmRSS: 1923456 kB ← 物理内存(常驻)
# VmData: 1721234 kB ← 数据段(堆)
# 看 cgroup 视角的内存使用
kubectl exec -it my-pod -- cat /sys/fs/cgroup/memory.current # cgroup v2
# 或 cgroup v1:cat /sys/fs/cgroup/memory/memory.usage_in_bytes
# 看 working set(K8s 评判 OOM 用的指标)
kubectl top pod my-pod
# NAME CPU(cores) MEMORY(bytes)
# my-pod-1 245m 1834Mi
如果发现 VmRSS 长期接近 limits 且持续涨,大概率是泄漏。每个语言都有对应的内存 profiler:
- Java:jcmd / VisualVM / Eclipse MAT / Async Profiler
- Go:
go tool pprof -alloc_space - Python:
tracemalloc/memray/py-spy - Node.js:
node --inspect+ Chrome DevTools Heap Snapshot
K8s 配置避坑清单
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
image: my-app:1.0
resources:
requests: # 必须设,K8s 调度器才能算
memory: "1Gi"
cpu: "200m"
limits: # 必须设,防单实例失控
memory: "2Gi"
cpu: "1000m"
# ↓↓↓ 这几个 probe 配错也会导致重启循环 ↓↓↓
startupProbe: # 给慢启动应用留时间
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30 # 60 秒慢启动也能撑住
periodSeconds: 2
livenessProbe: # 死了才重启
httpGet: { path: /livez, port: 8080 }
periodSeconds: 30
failureThreshold: 3
readinessProbe: # 暂时不行就摘流量
httpGet: { path: /readyz, port: 8080 }
periodSeconds: 5
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"] # 让 LB 摘干净流量
OOMKilled 排查 6 步
- kubectl describe pod 看 Last State / Exit Code 137 确认是 OOMKilled
- kubectl top pod 看是不是接近 limits
- kubectl logs --previous 看死前最后日志,有没有大对象在加载
- kubectl exec ps -eo pid,rss 看具体哪个进程吃内存
- jcmd / pprof / tracemalloc / heap snapshot 看应用内部内存分布
- 对应语言调参(JVM 参数 / GOMEMLIMIT / NODE_OPTIONS / gunicorn worker 数)
真正能救命的 5 条规则
- 容器 limits 给应用堆的 1.4 倍:堆外要 30-40% 缓冲
- 所有 cache 必须有上限:LRU + TTL,无限增长的 Map / Dict 是 OOM 之源
- 每个 worker / 进程定期重启:max-requests = 1000-5000,清除累积的碎片
- 开 NMT / pprof:出问题时能立刻看内存分布,不开等于黑盒
- Prometheus 监控 working set 与 limits 比值:超过 80% 就告警,不要等 OOM
- alert: PodMemoryNearLimit
expr: |
container_memory_working_set_bytes{container!=""}
/ kube_pod_container_resource_limits{resource="memory"}
> 0.85
for: 5m
labels: { severity: warning }
annotations:
summary: '{{ $labels.pod }} 内存使用 > 85% limits'
description: '当前 {{ $value | humanizePercentage }},接近 OOMKilled'
OOMKilled 不是问题,是症状。真正的问题是「内存到哪去了」。把上面 6 步走一遍,8 成情况能定位到具体进程 / 具体对象,然后才知道是调参 / 拆分 / 还是修 bug。
—— 别看了 · 2026