Linux 进程被 OOM Kill:区分主机内存耗尽与 cgroup 限额

116 次浏览5 条回复

适用于 Linux 主机或 systemd 服务出现进程被 SIGKILL、退出码 137,且怀疑与内存有关的场景。命令基线为 Linux 5.4+、systemd 245+、procps-ng 3.3+;cgroup v2 的文件名与 v1 不同,以下先给出 v2 路径。读取完整内核日志及其他进程的 /proc 信息通常需要 root 权限。示例单元 app.service 必须替换为实际值。先保留现场,不要一开始就重启服务、清缓存或放大内存限额。

1. 先确认是否真的发生了 OOM Kill

date -u
uname -r
systemctl --version | head -n 1

journalctl -k --since '-30 min' --no-pager -o short-iso \
  | grep -Ei 'out of memory|oom-kill|killed process|memory cgroup'
journalctl -u app.service --since '-30 min' --no-pager -o short-iso
systemctl show app.service \
  -p ActiveState -p SubState -p Result \
  -p ExecMainCode -p ExecMainStatus -p MainPID

退出码 137 只表示进程最终收到 SIGKILL,不能单独证明是内核 OOM killer;人工 kill -9、编排器超时和服务管理器也可能产生相同结果。应以同一时间窗口内的内核日志、cgroup 计数器和服务退出记录相互印证。过滤结果为空时仍应检查该时间段的完整内核日志,因为不同内核版本的字段和措辞可能不同。若故障跨过重启,且系统启用了持久化 journal,可再查:

journalctl -k -b -1 --no-pager -o short-iso

2. 判断是主机级压力还是 cgroup 达到上限

先确认层级类型并定位服务的控制组:

stat -fc %T /sys/fs/cgroup
cg=$(systemctl show app.service -p ControlGroup --value)
printf 'ControlGroup=%s\n' "$cg"

systemctl show app.service \
  -p MemoryCurrent -p MemoryHigh -p MemoryMax -p MemorySwapMax -p OOMPolicy

stat 返回 cgroup2fs 时,可读取该服务的 v2 指标:

base="/sys/fs/cgroup${cg}
"
base=${base%$'\n'}
cat "$base/memory.current"
cat "$base/memory.max"
cat "$base/memory.events"
[ ! -r "$base/memory.events.local" ] || cat "$base/memory.events.local"
sed -n '/^anon /p;/^file /p;/^slab /p;/^sock /p' "$base/memory.stat"

memory.max 为 max 表示未设置硬上限;memory.events 中 oom 或 oom_kill 增加,才支持该 cgroup 在其生命周期内触发过 OOM。这个文件的计数是累计值,单次读取不能确定故障时间;最好与告警前基线、内核日志时间和服务重启时间对齐。memory.events 可能包含子 cgroup 事件,内核提供 memory.events.local 时可用它区分本层事件。

若不是 cgroup2fs,先用 findmnt -t cgroup 找到 v1 memory 控制器挂载点,再按 ControlGroup 映射路径读取 memory.usage_in_bytes、memory.limit_in_bytes 和 memory.failcnt,不要把 v2 文件名直接套到 v1。

同时检查主机整体压力:

free -h
sed -n '/^MemAvailable:/p;/^SwapTotal:/p;/^SwapFree:/p' /proc/meminfo
vmstat 1 10
[ ! -r /proc/pressure/memory ] || cat /proc/pressure/memory

MemAvailable、swap 和 PSI 都需要连续观测;故障后的单个快照不能还原峰值。内核日志若明确出现 constraint=CONSTRAINT_MEMCG、oom_memcg 或目标 cgroup 路径,更接近 cgroup 限额;若主机可用内存持续耗尽并出现全局 OOM 记录,则应从主机容量、匿名内存、页缓存和 slab 一并排查。PSI 的 full 持续升高表示所有非空闲任务都受内存停顿影响,但 PSI 高本身不等于已经发生 OOM Kill。

3. 保留当前进程的内存构成

若服务已经自动拉起,当前 PID 是新实例,不能代替被杀进程的现场;它仍可用于寻找可重复增长的基线:

pid=$(systemctl show app.service -p MainPID --value)
ps -eo pid,ppid,user,rss,vsz,stat,comm --sort=-rss | head -n 20

if [ "${pid:-0}" -gt 0 ]; then
  sed -n '/^Name:/p;/^VmRSS:/p;/^VmSwap:/p;/^RssAnon:/p;/^RssFile:/p;/^Threads:/p;/^oom_score_adj:/p' \
    "/proc/$pid/status"
  [ ! -r "/proc/$pid/smaps_rollup" ] || \
    sed -n '/^Rss:/p;/^Pss:/p;/^Private_/p;/^Swap:/p' "/proc/$pid/smaps_rollup"
fi

RSS 排序适合找当前大户,但共享页会影响简单求和,短生命周期进程也可能已经消失。对容器化服务,应沿实际容器或 Pod 的 cgroup 读取指标,并核对编排器事件;只看宿主机 free 可能漏掉容器自己的硬上限。

4. 按证据修复并闭环

先区分增长来源:memory.stat 中 anon 持续增长更接近堆、栈或匿名映射,file 增长多与文件页缓存相关,slab 增长需要继续拆分内核对象。再结合应用指标、请求并发、队列积压和发布时间定位。不要仅凭一次 RSS 峰值宣布存在内存泄漏。

若确认为 cgroup 上限,先检查应用工作集和峰值是否合理,再决定降低并发、修复无界缓存,或调整 MemoryHigh、MemoryMax。MemoryHigh 主要施加回收压力,MemoryMax 是硬边界;盲目增大上限可能把局部故障扩大为主机级 OOM。若确认为主机级耗尽,还应检查同机其他服务、swap 策略和容量余量。

修复前后应在获准环境以相同负载窗口记录:服务 cgroup 的 memory.current 与 memory.events、主机 MemAvailable 与 memory PSI、内核 OOM 日志以及应用成功率。闭环至少应满足 oom/oom_kill 计数不再增加、没有新增同类内核记录、内存峰值留有可解释余量,并且服务功能与延迟仍符合预期。

管理员提示:第 2 节的 cgroup v2 路径示例建议把 base 直接写成单行,避免把换行符放进变量再剥离;复制时更不容易出错。可替换为:

cg=$(systemctl show app.service -p ControlGroup --value)
base="/sys/fs/cgroup${cg}"
printf 'ControlGroup=%q\nBase=%q\n' "$cg" "$base"
test -r "$base/memory.current"
test -r "$base/memory.events"
cat "$base/memory.current"
cat "$base/memory.max"
cat "$base/memory.events"

若 ControlGroup 为空或路径不是以 / 开头,应先停止并核对 systemd 单元状态,不要继续拼接路径。memory.events 会累计统计该 cgroup 及子 cgroup 的事件;需要只看本层时,在文件存在的内核上同时读取 memory.events.local。判断 OOM 仍应把计数器增量与同一时间窗口的内核日志、服务退出记录对齐,单次快照不足以定位发生时间。

再补一个容易与内核 OOM 混淆的分支:systemd-oomd 是用户态压力处置器,可能在内核 OOM killer 介入前向目标 cgroup 内的进程发送 SIGKILL。因此也可能看到退出码 137,但内核 OOM 日志为空,memory.events 的 oom_kill 也没有增加。此项适用于 systemd 247+,且发行版实际启用了 systemd-oomd:

systemctl status systemd-oomd.service --no-pager -l
journalctl -u systemd-oomd.service --since '-30 min' --no-pager -o short-iso
command -v oomctl >/dev/null && oomctl

systemctl show app.service \
  -p ManagedOOMSwap \
  -p ManagedOOMMemoryPressure \
  -p ManagedOOMMemoryPressureLimit \
  -p ManagedOOMPreference

还应检查服务所在的父级 slice,因为策略可能配置在 slice 上,而不是直接写在 app.service。可先用 systemctl show app.service -p Slice -p ControlGroup 定位,再对相应 slice 执行 systemctl show。

判断时以同一时间窗口内 systemd-oomd.service 的处置日志为主,并与服务退出时间、memory PSI 和内核日志对齐。故障后的 oomctl 只反映当前监控状态,不能单独还原历史;反过来,服务未启用或日志中没有处置记录,才应继续沿内核 OOM、人工信号或编排器超时等路径排查。

可以再补一组“被杀前压力”的证据。主机级 /proc/pressure/memory 会混入同机其他服务;在 cgroup v2 且内核启用 PSI(通常需要 CONFIG_PSI)时,服务 cgroup 自己的 memory.pressure 更适合判断局部回收停顿。读取 cgroup 文件通常需要宿主机上的相应权限:

cg=$(systemctl show app.service -p ControlGroup --value)
case "$cg" in /*) ;; *) printf 'invalid ControlGroup: %q\n' "$cg" >&2; exit 1;; esac
base="/sys/fs/cgroup${cg}"

snapshot() {
  date -u
  cat "$base/memory.current"
  if [ -r "$base/memory.events.local" ]; then
    cat "$base/memory.events.local"
  else
    cat "$base/memory.events"
  fi
  [ ! -r "$base/memory.pressure" ] || cat "$base/memory.pressure"
}

snapshot
# 覆盖一次实际告警或获准的负载窗口后,再执行 snapshot

应比较两次快照的增量,而不是只看非零值:high 增加表示触及 memory.high 后发生节流或直接回收,max 增加表示分配触及 memory.max,二者都不等同于已经杀进程;oom 与 oom_kill 的增量才分别支持发生了 cgroup OOM 和实际执行了 OOM kill。缺少 memory.events.local 时,memory.events 还会汇总子 cgroup,需先核对层级。

memory.pressure 中 some/full 的 total 是累计停顿时间,修复后不会归零;闭环时应在可比负载下确认其增长速率下降,同时 oom/oom_kill 不再增加。这样即使服务重启后 memory.current 已回落,也能保留接近故障窗口的压力趋势。

再补一个容易混淆的边界:cgroup 的成组终止、内核的受害进程选择,以及 systemd 的后续处置是三件事。以下适用于 cgroup v2;OOMPolicy= 需要 systemd 243+。先读取配置,不要为排障临时修改:

cg=$(systemctl show app.service -p ControlGroup --value)
case "$cg" in /*) ;; *) printf 'invalid ControlGroup: %q\n' "$cg" >&2; exit 1;; esac
base="/sys/fs/cgroup${cg}"

cat "$base/memory.oom.group"
systemctl show app.service -p OOMPolicy -p OOMScoreAdjust
grep -E '^(oom|oom_kill|oom_group_kill) ' "$base/memory.events.local" 2>/dev/null \
  || grep -E '^(oom|oom_kill|oom_group_kill) ' "$base/memory.events"

pid=$(systemctl show app.service -p MainPID --value)
if [ "${pid:-0}" -gt 0 ]; then
  cat "/proc/$pid/oom_score" "/proc/$pid/oom_score_adj"
fi

memory.oom.group=0 时,一次 cgroup OOM 可能只杀掉组内某个进程,其他工作进程仍存活;为 1 时,内核会把该 cgroup 及其后代作为不可分割的工作负载处理,但 oom_score_adj=-1000 的任务仍受保护。若内核提供 oom_group_kill 计数,可将其增量与 oom_kill、内核日志对齐;旧内核没有该字段时不要据此判定未发生成组终止。

OOMScoreAdjust 影响内核选择受害进程的倾向,不能扩容或阻止 cgroup 达到 memory.max;OOMPolicy 则决定 systemd 在确认单元内进程遭内核 OOM kill 后,是继续、停止还是终止单元中的其余进程,它不参与内核选杀。若出现“主进程被杀后还有子进程占用端口”或“一个 worker 被杀但服务表面仍存活”,应同时核对这三项,并以同一时间窗口的内核日志、计数器增量和单元状态闭环。

sarahLv1#3

可以再补一组“被杀前压力”的证据。主机级 /proc/pressure/memory 会混入同机其他服务;在 cgroup v2 且内核启用 PSI(通常需要 CONFIG_PSI)时,服务 cgroup 自己的 memory.pressure 更适合判断局部回收停顿。读取 cgroup 文件通常需要宿主机上的相应权限:

cg=$(systemctl show app.service -p ControlGroup --value)
case "$cg" in /*) ;; *) printf 'invalid ControlGroup: %q\n' "$cg" >&2; exit 1;; esac
base="/sys/fs/cgroup${cg}"

snapshot() {
  date -u
  cat "$base/memory.current"
  if [ -r "$base/memory.events.local" ]; then
    cat "$base/memory.events.local"
  else
    cat "$base/memory.events"
  fi
  [ ! -r "$base/memory.pressure" ] || cat "$base/memory.pressure"
}

snapshot
# 覆盖一次实际告警或获准的负载窗口后,再执行 snapshot

应比较两次快照的增量,而不是只看非零值:high 增加表示触及 memory.high 后发生节流或直接回收,max 增加表示分配触及 memory.max,二者都不等同于已经杀进程;oom 与 oom_kill 的增量才分别支持发生了 cgroup OOM 和实际执行了 OOM kill。缺少 memory.events.local 时,memory.events 还会汇总子 cgroup,需先核对层级。

memory.pressure 中 some/full 的 total 是累计停顿时间,修复后不会归零;闭环时应在可比负载下确认其增长速率下降,同时 oom/oom_kill 不再增加。这样即使服务重启后 memory.current 已回落,也能保留接近故障窗口的压力趋势。

补充一个计数器生命周期的边界:memory.events、memory.events.local 属于当前 cgroup 对象,不属于持久的 systemd 单元名。单元完全停止后,systemd 可能回收空 cgroup;后续启动若重新创建该 cgroup,计数会从零开始。因此跨 stop/start、重启或部署周期直接相减,可能把“基线已重置”误判为 OOM 消失。

采样时建议把激活周期和路径一起保存。以下适用于 systemd 245+ 与 cgroup v2,读取权限要求同主帖:

date -u
systemctl show app.service \
  -p ControlGroup -p InvocationID \
  -p ActiveEnterTimestampMonotonic -p NRestarts

cg=$(systemctl show app.service -p ControlGroup --value)
case "$cg" in /*) ;; *) printf 'invalid ControlGroup: %q\n' "$cg" >&2; exit 1;; esac
base="/sys/fs/cgroup${cg}"
stat -Lc 'cgroup_inode=%i' "$base"
if [ -r "$base/memory.events.local" ]; then
  grep -E '^(high|max|oom|oom_kill|oom_group_kill) ' "$base/memory.events.local"
else
  grep -E '^(high|max|oom|oom_kill|oom_group_kill) ' "$base/memory.events"
fi

InvocationID 可用于区分服务激活周期;cgroup inode 只能作为同一主机运行期内的辅助线索,不能当作长期唯一标识,因为 inode 可能复用。若后一次计数小于前一次,已可确认不能按连续计数器计算增量;即使数值更大,只要激活周期或 cgroup 对象发生变化,也不应直接相减。更稳妥的闭环是由宿主机监控持续抓取这些计数,并同时记录 UTC 时间、ControlGroup、InvocationID 和服务退出时间。这样既能识别真实增量,也能明确标注计数重置。