程序突然被杀掉:Linux OOM排查

程序突然退出,只留下 Killed,或者容器显示退出码 137,先不要直接认定服务器内存不够。强制停止、管理程序超时处理等也可能产生类似结果,需要把故障时间、被终止进程和内存限制对应起来。

下面以 Ubuntu 24.04、systemd 管理的服务和 cgroup v2 为例。myapp.service、容器名及时间都是示例,请替换为实际值。本篇先读取证据,不主动制造内存耗尽。

先找发生那一分钟的记录

在服务器终端执行。先看时区,再把查询窗口缩到故障前后几分钟,避免用自己电脑上的时间直接套日志:

date -Is
sudo journalctl -k --since '2026-11-05 08:50:00' --until '2026-11-05 09:10:00' --no-pager
sudo journalctl -u myapp.service --since '2026-11-05 08:50:00' --until '2026-11-05 09:10:00' --no-pager

重点找 Out of memory、oom-kill、Killed process等上下文,核对进程名、PID 和时间。被选中终止的进程未必就是内存增长的最初来源,要继续检查同一时段的其他服务。

journalctl -k默认看本次启动。故障后重启过服务器时,先列出保留的启动记录,再按实际序号查询;没有上次日志,不能推断上次没有 OOM:

sudo journalctl --list-boots
sudo journalctl -k -b -1 --no-pager

分清整机压力、服务限额和提前干预

找到的证据 下一步查什么
内核 OOM 记录指向整机内存压力 对照故障时的监控、同时运行的服务和批处理,不能只看重启后的空闲内存。
记录带有 memory cgroup、memcg 或具体控制组路径 核对该服务、容器及父级的内存限制;整机仍有余量,也可能触发组内 OOM。
systemd-oomd 记录终止了某个控制组 检查内存压力策略和当时负载。它可能在内核 OOM 前介入,内核日志未必出现同样的杀进程记录。
只有退出码 137 或 signal=KILL 证据还不足,继续查停止任务、部署记录和服务日志。

系统启用了 systemd-oomd时,补查同一个时间窗口:

sudo journalctl -u systemd-oomd.service --since '2026-11-05 08:50:00' --until '2026-11-05 09:10:00' --no-pager

服务没有吃满整机,为什么仍被杀?

先查看实际服务属性和 cgroup 类型:

systemctl show myapp.service -p Result -p ExecMainCode -p ExecMainStatus -p Slice -p ControlGroup -p MemoryCurrent -p MemoryHigh -p MemoryMax
stat -fc %T /sys/fs/cgroup

MemoryHigh主要用于施加回收与节流压力,MemoryMax是硬上限;两者不能混为一谈。服务显示 MemoryMax=infinity,也不能排除父级 slice 的限制,应按输出中的 Slice 继续核对。当前内存用量在进程退出后可能已经下降。

第二条输出为 cgroup2fs,且服务仍有控制组时,可读取事件计数。路径为空或已消失就停止这一步,不拿别的服务路径代替:

oom_cgroup=$(systemctl show myapp.service -p ControlGroup --value)
if [ -n "$oom_cgroup" ] && [ "$oom_cgroup" != '/' ] && [ -d "/sys/fs/cgroup$oom_cgroup" ]; then
    sudo cat "/sys/fs/cgroup$oom_cgroup/memory.events"
    sudo cat "/sys/fs/cgroup$oom_cgroup/memory.events.local"
    sudo cat "/sys/fs/cgroup$oom_cgroup/memory.max"
else
    printf '%s\n' '控制组不存在,请先核对服务名称和状态。'
fi

oom_kill增长说明这组任务有被内核 OOM 机制终止的记录,但计数本身没有故障时间,也不能单独区分整机与组内原因。memory.events通常包含子组事件,memory.events.local只看本级。重建控制组后计数会重新开始;文件不存在时先核对控制器与服务状态。

Docker 部署改查实际容器,myapp替换为容器名:

docker inspect --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} Memory={{.HostConfig.Memory}}' myapp

同时核对发生时间与宿主机日志。Memory=0表示未设置这项显式容器上限,不代表拥有无限内存。容器重启、重建后,当前状态也未必保留事发时的完整情况。

按原因调整,再观察同类负载

查看现在的内存和主要进程,作为下一次比较的起点:

free -h
swapon --show
ps -eo pid,comm,rss --sort=-rss | head -n 11

RSS 的单位是 KiB,多个进程可能共享页面,不直接相加当成整机实耗。更多指标见内存占用检查。

  • 任务瞬时峰值:先降低并发、拆小批次,错开备份和导入等重负载任务。
  • 服务或容器限额偏低:先算清整机、父级与其他服务的预算,再在原配置来源调整限额,保留修改前的值。
  • 用量持续上涨:检查缓存、队列和应用泄漏。反复重启只能暂时释放内存。

调整后用相近业务负载比较峰值、错误率和事件计数,确认服务持续可用。swap 可作为缓冲,不能替代必要内存,也不应通过关闭 OOM 保护来掩盖持续超量。需要提交日志时,按journalctl 教程保留同一故障窗口。