K8s Pod OOMKilled 排查指南:6 种真实原因 + 每种修法

K8s Pod 反复 OOMKilled exit code 137。本文列 6 种真实场景:Java 堆外内存超 limits、Go 不知道 cgroup limits、Python fork 后 RSS 翻倍、Node 默认堆上限、sidecar 吃光内存、内存碎片。每种附 kubectl 命令 + 修法 + 配置模板。

K8s 集群里看到 Pod 反复重启,状态 Last State: Terminated, Reason: OOMKilled, Exit Code: 137。重启 5 次后 CrashLoopBackOff,告警群里炸开。本文是我们这两年遇到的 6 种 OOMKilled 真实原因,每种都附诊断命令和修法 —— 不是泛泛而谈"加内存",是真正告诉你内存被谁吃了。

第一件事:OOMKilled 是什么

Exit Code 137 = 128 + 9 = SIGKILL,Pod 进程被强制杀死。但 K8s 里有两种 OOMKilled,完全是两个故事:

  1. 容器内存超 limits:K8s cgroup 把容器内的某个进程 OOM 杀掉。Pod 状态显示 OOMKilled
  2. 节点内存不足: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

修法:

  1. worker 数 = (CPU limits * 2) + 1(经验值,不要超过 8)
  2. 定期 reload:用 --max-requests 1000 --max-requests-jitter 100,每个 worker 处理 1000 请求后自动重启,释放内存
  3. 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 步

  1. kubectl describe pod 看 Last State / Exit Code 137 确认是 OOMKilled
  2. kubectl top pod 看是不是接近 limits
  3. kubectl logs --previous 看死前最后日志,有没有大对象在加载
  4. kubectl exec ps -eo pid,rss 看具体哪个进程吃内存
  5. jcmd / pprof / tracemalloc / heap snapshot 看应用内部内存分布
  6. 对应语言调参(JVM 参数 / GOMEMLIMIT / NODE_OPTIONS / gunicorn worker 数)

真正能救命的 5 条规则

  1. 容器 limits 给应用堆的 1.4 倍:堆外要 30-40% 缓冲
  2. 所有 cache 必须有上限:LRU + TTL,无限增长的 Map / Dict 是 OOM 之源
  3. 每个 worker / 进程定期重启:max-requests = 1000-5000,清除累积的碎片
  4. 开 NMT / pprof:出问题时能立刻看内存分布,不开等于黑盒
  5. 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
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Go goroutine 泄漏:跑了 47 天 OOMKilled 后我们找到的 5 个真实场景

2026-5-19 10:20:56

技术教程

Python 函数默认参数是 [] 让我们订单串单的 P1 事故复盘

2026-5-19 10:31:12

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索