适用场景

这篇文章适用于线上服务突然退出、容器被重启、systemd 日志里只有 Killed、监控显示内存瞬时打满,但应用日志没有明确异常栈的场景。常见环境包括物理机、云主机、Docker 容器和 Kubernetes 节点。

OOM Killer 的本质是内核在内存不可回收时主动选择进程终止,以保证系统还能继续运行。排查重点不是只把服务拉起来,而是确认是谁触发了内存压力、为什么会被选中、后续如何降低再次发生的概率。

现象描述

典型现象如下:

  • 服务进程突然消失,应用日志中没有正常退出记录。
  • systemd 服务状态显示 code=killed, signal=KILL 或日志里出现 Killed
  • Docker 容器状态出现 OOMKilled=true
  • Kubernetes Pod 状态为 OOMKilledrestartCount 增加。
  • 机器在故障时间点出现内存、swap、page cache 或 slab 异常变化。

先不要急着改参数,第一步应确认是否真的发生了 OOM。

可能原因

常见原因可以分为几类:

  1. 应用自身内存泄漏,例如缓存无限增长、批量查询一次性加载大结果集。
  2. 并发突增导致堆内存、连接池、队列或临时对象快速膨胀。
  3. 机器上多个进程争抢内存,目标服务只是被内核选中杀掉。
  4. 容器或 Pod 的 memory limit 设置过低,业务峰值超过限制。
  5. swap 关闭或过小,短时内存尖峰没有缓冲空间。
  6. JVM、Go、Python 等运行时内存参数与容器限制不匹配。
  7. 日志、压缩、备份、报表等离线任务在业务高峰期占用大量内存。

排查思路

推荐按下面顺序排查:

  1. 确认 OOM 事件是否存在。
  2. 找到被杀进程、触发时间和内核选择原因。
  3. 对比故障前后的内存、swap、进程占用和业务流量。
  4. 判断是整机 OOM、容器 OOM,还是 cgroup limit 触发。
  5. 给出短期止血和长期治理方案。

常用命令

查看内核 OOM 日志

journalctl -k --since "2026-07-12 00:00:00" | grep -Ei "out of memory|oom|killed process"

关键字段说明:

  • Out of memory:内核确认进入 OOM 处理。
  • Killed process:被杀进程的 PID、进程名和内存占用。
  • total-vm:进程虚拟内存大小,不等于实际物理内存。
  • anon-rss:匿名页常驻内存,通常更接近应用实际占用。
  • file-rss:文件映射占用。
  • oom_score_adj:进程被 OOM Killer 选中的倾向,值越大越容易被杀。

如果系统没有持久化 journal,可以尝试:

dmesg -T | grep -Ei "out of memory|oom|killed process"

dmesg 里的日志可能会被覆盖,事故后应尽快采集。

查看 systemd 服务退出原因

systemctl status myapp.service -l
journalctl -u myapp.service --since "2026-07-12 00:00:00" --no-pager

重点看:

  • 是否有 signal=KILL
  • 是否在同一时间点自动重启。
  • 重启前是否有大量请求、任务调度或异常日志。

查看当前内存概况

free -h
cat /proc/meminfo | egrep "MemTotal|MemFree|MemAvailable|SwapTotal|SwapFree|Cached|Slab|SReclaimable|SUnreclaim"

判断时不要只看 free,更要看 MemAvailable。它表示系统在不明显影响运行的情况下还能分配给新进程的内存,比 MemFree 更有参考价值。

找出当前占用内存较高的进程

ps -eo pid,ppid,user,comm,%mem,rss,vsz,oom_score,oom_score_adj --sort=-rss | head -n 20

字段说明:

  • rss:实际驻留内存,单位 KB。
  • vsz:虚拟内存,不适合作为唯一判断依据。
  • oom_score:内核综合评估的 OOM 风险分。
  • oom_score_adj:人为调整值,范围通常是 -10001000

如果某个非核心进程 oom_score_adj 很低,而核心业务进程较高,内核在 OOM 时可能优先杀业务进程,这需要调整服务策略。

Docker 容器排查

docker inspect my_container --format '{{.State.OOMKilled}} {{.State.ExitCode}} {{.State.FinishedAt}}'
docker stats --no-stream
docker inspect my_container --format 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}'

如果 OOMKilled=true,说明容器被内存限制杀掉。HostConfig.Memory0 表示没有设置容器内存上限;非 0 时单位是字节。

Kubernetes Pod 排查

kubectl describe pod my-pod -n my-namespace
kubectl get pod my-pod -n my-namespace -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}{"\n"}'
kubectl top pod -n my-namespace

重点关注:

  • Last State: Terminated
  • Reason: OOMKilled
  • Exit Code: 137
  • Limits.memoryRequests.memory

Exit Code 137 通常表示进程收到 SIGKILL,在容器场景中经常与 OOMKilled 一起出现。

定位示例

假设故障时间是 10:32,用户反馈接口大量 502,服务随后自动恢复。先查看内核日志:

journalctl -k --since "2026-07-12 10:20:00" --until "2026-07-12 10:40:00" \
  | grep -Ei "out of memory|killed process"

示例输出:

Out of memory: Killed process 18423 (python) total-vm:5242880kB, anon-rss:3145728kB, file-rss:1024kB, oom_score_adj:0

这说明 PID 18423 的 Python 进程被杀,匿名 RSS 约 3 GB。继续查业务日志:

journalctl -u order-api.service --since "2026-07-12 10:20:00" --until "2026-07-12 10:40:00" --no-pager

如果发现 10:30 开始执行全量导出任务,就要继续确认导出逻辑是否一次性把结果集加载到内存。例如 Python 中常见的风险写法:

rows = cursor.fetchall()
for row in rows:
    write_csv(row)

更稳妥的方式是分页或流式处理:

while True:
    rows = cursor.fetchmany(1000)
    if not rows:
        break
    for row in rows:
        write_csv(row)

关键点是让单批数据量可控,避免一次请求或一次任务把全部数据放进内存。

修复方案

短期止血

  1. 先恢复服务,并保留故障时间点的内核日志、应用日志和监控截图。
  2. 临时降低高风险任务的并发,例如导出、报表、批处理、图片处理。
  3. 对容器或 Pod 适当提高 memory limit,但不要只加内存后结束排查。
  4. 如果是整机内存不足,迁移非核心任务或扩容节点。
  5. 对业务入口增加限流,避免峰值流量把队列和对象堆满。

长期治理

  1. 为批处理任务改造成分页、流式、分片执行。
  2. 为缓存设置最大容量和过期策略,避免无限增长。
  3. 为核心服务设置合理的内存 requests 和 limits。
  4. 为 JVM、Go、Python 等运行时配置与容器限制匹配的内存参数。
  5. 将定时任务和在线服务隔离部署,避免互相抢内存。
  6. 建立 OOM 告警,出现 OOMKilledKilled process 或内存可用量过低时及时通知。

预防措施

监控指标

建议至少监控以下指标:

  • MemAvailable 持续低于阈值。
  • swap 使用率和 swap in/out。
  • 进程 RSS 增长速度。
  • 容器 container_memory_working_set_bytes
  • Kubernetes Pod 重启次数和 OOMKilled 事件。
  • 业务队列长度、请求并发、批处理任务耗时。

Prometheus 中可以用类似规则发现容器内存逼近限制:

container_memory_working_set_bytes{container!="",pod!=""}
/
container_spec_memory_limit_bytes{container!="",pod!="",container_spec_memory_limit_bytes>0}
> 0.9

这个表达式表示容器工作集内存超过限制的 90%。如果持续触发,就要检查是否存在内存泄漏、容量不足或 limit 设置过低。

服务配置

systemd 服务可以根据重要程度调整 OOM 倾向:

[Service]
OOMScoreAdjust=-500

OOMScoreAdjust=-500 会降低进程被 OOM Killer 选中的概率,但不要把所有服务都设置成低值。更推荐保护核心入口服务,同时让可重试的离线任务保持默认或更高的 OOM 倾向。

在 Kubernetes 中,合理设置 requests 和 limits:

resources:
  requests:
    memory: "512Mi"
    cpu: "500m"
  limits:
    memory: "1Gi"
    cpu: "1"

requests 影响调度和资源保障,limits 是容器可使用的上限。limit 太低会频繁 OOMKilled,太高则可能让节点整体内存压力失控。

总结

OOM 排查要避免两个误区:一是只看应用日志,忽略内核日志;二是只加内存,不追踪触发内存增长的业务路径。完整链路应该是先确认 OOM 事件,再定位被杀进程和触发时间,随后结合业务流量、任务日志、容器限制和进程 RSS 判断根因。

治理上要同时处理容量、隔离、代码和告警。对于线上服务,分页处理、限制并发、合理设置容器内存、隔离批处理任务和监控 OOM 事件,通常比单纯扩容更可靠。