进程突然被杀,先分清内核 OOM、cgroup OOM 和 systemd-oomd

194 次浏览8 条回复

适用于 Linux 5.4+、cgroup v2、systemd 247+。示例单元 app.service 要替换;读取内核日志和其他服务的 cgroup 通常需要 root 或对应权限。先别急着重启,退出状态、OOM 计数和日志很容易被后续现场覆盖。

先确认服务退出方式和实际 cgroup:

date -u
uname -r
stat -fc %T /sys/fs/cgroup
systemctl status app.service --no-pager -l
systemctl show app.service \
  -p ControlGroup -p Result -p ExecMainCode -p ExecMainStatus -p OOMPolicy
journalctl -u app.service --since '-30 min' --no-pager

ExecMainCode=killedExecMainStatus=9 只说明收到了 SIGKILL,不能单凭这一项认定是谁触发的。拿到 ControlGroup 后,再看 cgroup v2 的限制和事件:

CG=$(systemctl show app.service -p ControlGroup --value)
cat "/sys/fs/cgroup${CG}/memory.current"
cat "/sys/fs/cgroup${CG}/memory.max"
cat "/sys/fs/cgroup${CG}/memory.high"
cat "/sys/fs/cgroup${CG}/memory.events"

memory.events 里的 oom 表示该 cgroup 发生过分配失败,oom_kill 增长才表示内核 OOM killer 在这个层级杀过任务;这些是累计计数,最好和故障前基线比较。memory.maxmax 时也不能排除上层 cgroup 或整机 OOM。

再对齐同一时间窗:

journalctl -k --since '-30 min' --no-pager \
  | grep -E 'oom-kill|Out of memory|Killed process|Memory cgroup'
journalctl -u systemd-oomd --since '-30 min' --no-pager
oomctl --no-pager

内核日志有 Killed process 或 memory cgroup 记录,通常沿内核 OOM 分支查;systemd-oomd 日志明确记录被选中的 cgroup,而内核 OOM 记录缺失时,则沿 PSI、ManagedOOMMemoryPressureManagedOOMSwap 配置查。不要只因为看到状态 9 就直接扩大 MemoryMax

调整应用峰值、并发或内存策略后,在受控负载下同时观察 memory.currentmemory.events 和服务日志;至少确认服务保持 active、oom_kill 不再增长,且业务延迟和吞吐仍符合预期。

再补一个容易误判的点:cgroup v2 的 memory.events 是分层计数,子 cgroup 触发的事件也会反映到父级。服务里如果又挂了容器或委托子树,只看到父级 oom_kill 增长,还不能确定被杀的是服务主进程。内核支持时可以同时看本级计数:

CG=$(systemctl show app.service -p ControlGroup --value)
cat "/sys/fs/cgroup${CG}/memory.events"
cat "/sys/fs/cgroup${CG}/memory.events.local"

两者有差值时再往子目录定位。另外这些计数只覆盖当前 cgroup 的生命周期,单元停止后 cgroup 被删除再创建,计数会从头开始,所以最好在重启前把文件和时间戳一起留存。

还有个名字很容易带偏:OOMPolicy= 不是 OOM 的触发策略,它管的是 systemd 发现单元进程被 OOM 杀掉后怎么收尾。内核是否把 cgroup 当成一个整体处理,要看 memory.oom.group

CG=$(systemctl show app.service -p ControlGroup --value)
systemctl show app.service -p OOMPolicy
cat "/sys/fs/cgroup${CG}/memory.oom.group"

值为 1 时,cgroup OOM 会尽量把该 cgroup 及其后代里的任务作为一组处理,避免只杀掉一个进程后留下半残的服务;oom_score_adj=-1000 的任务例外。这个值本身不能说明是内核 OOM 还是 systemd-oomd 动的手,来源仍要和 memory.events、内核日志及 oomd 日志对齐。

如果怀疑 systemd-oomdmemory.events 里的 oom_kill 没增长也不能排除。oomd 是用户态在内核 OOM 之前主动发 SIGKILL,这时更该留存它的日志、单元配置和 PSI:

CG=$(systemctl show app.service -p ControlGroup --value)
systemctl show app.service \
  -p ManagedOOMMemoryPressure -p ManagedOOMMemoryPressureLimit \
  -p ManagedOOMSwap -p ManagedOOMPreference
cat "/sys/fs/cgroup${CG}/memory.pressure"
journalctl -u systemd-oomd --since '-30 min' --no-pager

memory.pressureavg10/avg60/avg300 是不同窗口的压力均值,单次快照不能直接证明触发条件成立;要和 oomd 日志里选中的 cgroup、配置阈值及持续时间对齐。这样能避免把“没有 oom_kill”误判成“不是内存压力导致的”。

隔壁摸鱼号Lv1#3

如果怀疑 systemd-oomdmemory.events 里的 oom_kill 没增长也不能排除。oomd 是用户态在内核 OOM 之前主动发 SIGKILL,这时更该留存它的日志、单元配置和 PSI:

CG=$(systemctl show app.service -p ControlGroup --value)
systemctl show app.service \
  -p ManagedOOMMemoryPressure -p ManagedOOMMemoryPressureLimit \
  -p ManagedOOMSwap -p ManagedOOMPreference
cat "/sys/fs/cgroup${CG}/memory.pressure"
journalctl -u systemd-oomd --since '-30 min' --no-pager

memory.pressureavg10/avg60/avg300 是不同窗口的压力均值,单次快照不能直接证明触发条件成立;要和 oomd 日志里选中的 cgroup、配置阈值及持续时间对齐。这样能避免把“没有 oom_kill”误判成“不是内存压力导致的”。

再补个配置定位的边界:systemd-oomd 监控的 cgroup 不一定就是 app.service 这一层,ManagedOOMMemoryPressure=ManagedOOMMemoryPressureLimit=ManagedOOMSwap= 也可能来自所属 slice。只看服务单元的属性,可能漏掉父级的生效配置。可以先定位层级,再一起留证:

unit=app.service
slice=$(systemctl show "$unit" -p Slice --value)
printf 'unit=%s\nslice=%s\n' "$unit" "$slice"
systemctl show "$unit" "$slice" \
  -p ControlGroup -p ManagedOOMMemoryPressure \
  -p ManagedOOMMemoryPressureLimit -p ManagedOOMSwap
oomctl dump

随后把 oomd 日志里记录的被选 cgroup 和这条层级对应起来,再看 memory.pressure 的持续窗口;这样能区分“服务自身触发”与“父级 slice 统一回收”,也避免只凭 SIGKILL 或叶子 cgroup 配置下结论。

WillowLv1#4

再补个配置定位的边界:systemd-oomd 监控的 cgroup 不一定就是 app.service 这一层,ManagedOOMMemoryPressure=ManagedOOMMemoryPressureLimit=ManagedOOMSwap= 也可能来自所属 slice。只看服务单元的属性,可能漏掉父级的生效配置。可以先定位层级,再一起留证:

unit=app.service
slice=$(systemctl show "$unit" -p Slice --value)
printf 'unit=%s\nslice=%s\n' "$unit" "$slice"
systemctl show "$unit" "$slice" \
  -p ControlGroup -p ManagedOOMMemoryPressure \
  -p ManagedOOMMemoryPressureLimit -p ManagedOOMSwap
oomctl dump

随后把 oomd 日志里记录的被选 cgroup 和这条层级对应起来,再看 memory.pressure 的持续窗口;这样能区分“服务自身触发”与“父级 slice 统一回收”,也避免只凭 SIGKILL 或叶子 cgroup 配置下结论。

再补一个和“被杀”相邻、但不会留下 oom_kill 的分支:memory.high 命中时,内核会让该 cgroup 进入回收和节流,服务可能只是延迟升高,并不会被杀。现场可以把 memory.events 里的 highmaxoom_kill 连续取差值,同时看 memory.currentmemory.statanon/file 以及 swap.current。如果 high 持续增长而 oom_kill 不动,先查 MemoryHigh= 或祖先 cgroup 的 memory.high,别只盯着 MemoryMax=。这些都是累计值,重启或 cgroup 重建前先留基线。

不想起名Lv1#5

再补一个和“被杀”相邻、但不会留下 oom_kill 的分支:memory.high 命中时,内核会让该 cgroup 进入回收和节流,服务可能只是延迟升高,并不会被杀。现场可以把 memory.events 里的 highmaxoom_kill 连续取差值,同时看 memory.currentmemory.statanon/file 以及 swap.current。如果 high 持续增长而 oom_kill 不动,先查 MemoryHigh= 或祖先 cgroup 的 memory.high,别只盯着 MemoryMax=。这些都是累计值,重启或 cgroup 重建前先留基线。

如果主机启用了 swap,我会把 cgroup 的换出限制也一起留证,免得把“换不出去导致压力上升”和 OOM killer 混为一谈。比如:

CG=$(systemctl show app.service -p ControlGroup --value)
for f in memory.swap.current memory.swap.max memory.swap.events; do
  printf '\n[%s]\n' "$f"
  cat "/sys/fs/cgroup${CG}/$f" 2>/dev/null || echo 'unavailable'
done
cat "/sys/fs/cgroup${CG}/memory.events"

memory.swap.events 里的 max/fail 说明换出受限或分配失败,不等于已经发生 kill;归因仍要看 memory.eventsoom_kill、内核日志和 systemd-oomd 日志,并在 cgroup 重建前保存时间戳和基线。

再补一个参数边界:OOMScoreAdjust= 只影响内核 OOM killer 选哪个进程,不能用来规避 systemd-oomd。可以把运行中进程和单元配置对一下:

pid=$(systemctl show app.service -p MainPID --value)
systemctl show app.service -p OOMScoreAdjust -p ManagedOOMPreference
cat "/proc/$pid/oom_score_adj"

oom_score_adj 越大,进程越容易被内核选中;-1000 会让该进程免于内核 OOM 选择。systemd-oomd 按 cgroup 处理,应该看 ManagedOOMPreference=、被监控的 slice 和 oomd 日志。别给一批服务统一调成很低的值,这只是把风险转移给别的进程;修改后还要重启对应服务,并复核新 PID 实际继承的值。读取其他用户进程可能受权限或 /proc 挂载选项限制。

空白Lv1#7

再补一个参数边界:OOMScoreAdjust= 只影响内核 OOM killer 选哪个进程,不能用来规避 systemd-oomd。可以把运行中进程和单元配置对一下:

pid=$(systemctl show app.service -p MainPID --value)
systemctl show app.service -p OOMScoreAdjust -p ManagedOOMPreference
cat "/proc/$pid/oom_score_adj"

oom_score_adj 越大,进程越容易被内核选中;-1000 会让该进程免于内核 OOM 选择。systemd-oomd 按 cgroup 处理,应该看 ManagedOOMPreference=、被监控的 slice 和 oomd 日志。别给一批服务统一调成很低的值,这只是把风险转移给别的进程;修改后还要重启对应服务,并复核新 PID 实际继承的值。读取其他用户进程可能受权限或 /proc 挂载选项限制。

MainPID 这一步更像抽查;多进程服务里,真正被内核选中的可能是 worker,而且进程也可能在启动后自行调整 oom_score_adj。要核对运行态,最好把该单元及其子 cgroup 的 PID 都扫一遍:

CG=$(systemctl show app.service -p ControlGroup --value)
find "/sys/fs/cgroup${CG}" -type f -name cgroup.procs -exec cat {} + 2>/dev/null \
  | sort -un \
  | while read -r pid; do
      printf 'pid=%s comm=' "$pid"
      cat "/proc/$pid/comm" 2>/dev/null || { echo '<exited>'; continue; }
      printf '  oom_score_adj='
      cat "/proc/$pid/oom_score_adj" 2>/dev/null || echo '<unavailable>'
    done

cgroup.procs 只列当前层,递归查子目录是关键。进程列表会变化,所以输出要和时间戳、内核 OOM 日志放在同一窗口看;systemctl show 的单元值和某个 PID 的实际值不一致时,以故障时运行态为准。