技术指南

Linux 内存耗尽排障:区分压力、限制与 OOM Kill

用 PSI、cgroup 事件和同窗日志区分内存压力、服务边界与真实 OOM kill,再完成可验证、可回滚的恢复。

服务突然变慢、被重启或退出时,先回答三个问题:主机是否正在持续回收内存,服务自己的 cgroup 是否撞到上限,内核是否真的执行过 OOM kill。空闲内存少、缓存多或进程 RSS 大都不能单独给出结论;可靠处置需要把同一时间窗内的 PSI、cgroup 事件、unit 状态和内核日志对齐。

用三类证据决定下一步

观察能说明什么处置方向
PSI 的 some/full 持续上升,但没有 OOM 事件任务正在等待内存或回收已影响吞吐,尚不能断言发生过 kill先削减并发、队列摄入或可暂停的低优先级工作
服务 cgroup 的 high/max 计数增加该 unit 受到自己的或父级的内存边界影响核对 MemoryHigh/MemoryMax、父级 slice 与工作集基线
oom_kill 增加,或内核日志记录被杀进程已经发生内核 OOM 处置保留受害者、触发 cgroup、调用栈与同窗业务事件后再恢复
unit 退出但以上证据都没有变化不能把退出归因于 OOM回到退出码、signal、应用日志、watchdog 与部署事件

先固定事件时间窗

把 example.service 换成真实 unit;不同 systemd 版本可能不提供全部 Memory* 属性,缺项要记录为环境差异而不是填零
unit=example.service
stamp=$(date --utc --iso-8601=seconds)
printf 'incident_utc=%s\nunit=%s\n' "$stamp" "$unit"
systemctl status "$unit" --no-pager --full
systemctl show "$unit" \
  -p ActiveState -p SubState -p Result -p ExecMainCode -p ExecMainStatus \
  -p MainPID -p ControlGroup -p MemoryCurrent -p MemoryPeak \
  -p MemoryHigh -p MemoryMax -p MemorySwapMax

如果服务已经退出,先记下 unit 的 Result、主进程退出状态和 journal 时间,再决定是否恢复。`Result=oom-kill` 是 systemd 对 unit 结果的线索,但仍应与 cgroup 计数和内核日志交叉验证;一个同机其他进程被杀,不等于这个服务越界。

读取主机与 cgroup 压力

适用于统一 cgroup v2 层级;先保存原始键值,不要只截取一个数字。memory.events 包含子树事件,memory.events.local 只看当前 cgroup
cat /proc/pressure/memory

cg=$(systemctl show "$unit" -p ControlGroup --value)
test -n "$cg"
printf 'cgroup=%s\n' "$cg"
for name in memory.current memory.peak memory.high memory.max \
            memory.swap.current memory.swap.max \
            memory.events memory.events.local memory.stat; do
  path="/sys/fs/cgroup${cg}/${name}"
  if test -r "$path"; then
    printf '\n[%s]\n' "$name"
    cat "$path"
  else
    printf '\n[%s unavailable]\n' "$name"
  fi
done

PSI 的 `some` 表示至少一部分任务因资源不足而停顿,`full` 表示所有非空闲任务同时停顿;avg10、avg60、avg300 是不同窗口的百分比,total 是累计停顿微秒数。它衡量的是等待造成的生产力损失,不是“剩余内存百分比”,必须和请求延迟、队列深度及事件计数的增量一起看。

正确解释 memory.events

内核语义不要误读成
high越过 memory.high 后进程被节流并进入直接回收的次数已经 OOM kill
max使用量将越过 memory.max 的次数;回收失败后才可能进入 OOM每次都杀了进程
oom到达限制且一次分配将失败,OOM 机制被考虑必然存在受害进程
oom_kill该 cgroup 中被任一种 OOM killer 杀死的进程数整个服务一定被完整终止
oom_group_kill按 memory.oom.group 发生的整组 OOM 次数普通单进程 kill

这些计数是累计值,所以单张截图没有基线时只能证明历史上发生过事件。事件开始时保存一次,减载或恢复后再保存一次;如果使用层级 `memory.events`,还要检查父级 slice,避免把子树事件错误归到当前 unit。

把 kill、unit 与业务症状对到同一时间

使用事件的真实 UTC 起止时间;grep 只用于缩小阅读范围,最终仍要保留相邻内核日志和完整 unit 日志
since='2026-08-18 18:00:00 UTC'
until='2026-08-18 18:20:00 UTC'
journalctl -u "$unit" --since "$since" --until "$until" \
  --output=short-precise --no-pager
journalctl -k --since "$since" --until "$until" \
  --output=short-precise --no-pager \
  | grep -Ei 'out of memory|oom-kill|killed process|memory cgroup'

内核日志应至少回答受害 PID/进程名、触发范围是全局还是 memory cgroup、当时任务和内存摘要。再把 PID、unit 的 ControlGroup、应用请求 ID 与部署时间对齐。日志被轮转、远端采集延迟或权限不足时,结论应保持为未知,不能用当前 RSS 反推过去的受害者。

先减载,再恢复最小服务

  1. 停止新的批任务、导入、爬取或无界重试,保留健康检查和管理流量;如果有队列,优先暂停消费者摄入而不是删除任务。
  2. 确认状态型服务的 WAL、事务日志、队列租约或文件完整性要求;需要恢复检查时先做检查,再启动写流量。
  3. 只恢复受影响的最小 unit,并观察启动阶段的 MemoryCurrent、memory.events 增量、PSI 和业务错误;不要同时重启整台主机和所有依赖。
  4. 若必须临时提高边界,先证明宿主机和父级 slice 有保留空间,并记录旧值、到期时间和撤销负责人。不要把 `MemoryMax=infinity` 当作恢复方案。

`memory.high` 是节流和回收边界,不会直接触发 OOM killer;`memory.max` 是最后的硬边界,回收无法把使用量降下去时会在该 cgroup 内触发 OOM。通常先用可观测的 high 暴露过量工作集,再用 max 约束失控负载,但两者都必须来自真实基线和主机故障预算,而不是复制固定数值。

把永久修复绑定到一种根因

根因优先修复验证重点
请求或任务并发无界入口限流、消费者并发上限、背压和有界队列相同负载下队列可控,PSI 与 high 不持续增长
缓存或堆随时间增长容量上限、驱逐策略、堆快照或分配剖析长时间窗口内工作集收敛且命中率/延迟可接受
边界低于正常工作集依据峰值与宿主保留量调整 high/max候选边界下没有节流回归,邻居和宿主仍有余量
全局内存不足迁移、扩容或降低同机工作负载总预算父级与主机 PSI 恢复,未把压力转移给关键服务

用增量和业务结果验收

在同一受控业务场景的开始与结束各保存一次;验收需要计算事件增量,并同时检查真实请求、错误率、延迟和队列
cg=$(systemctl show "$unit" -p ControlGroup --value)
for file in memory.current memory.peak memory.events memory.events.local; do
  path="/sys/fs/cgroup${cg}/${file}"
  test ! -r "$path" || { printf '\n[%s]\n' "$file"; cat "$path"; }
done
cat /proc/pressure/memory
systemctl show "$unit" -p ActiveState -p SubState -p Result -p MemoryCurrent -p MemoryPeak
  • 真实用户路径恢复,错误率、延迟和积压满足事先定义的门槛。
  • 观察窗内 oom、oom_kill 与 oom_group_kill 没有新增;high/max 是否允许新增由本次边界设计明确规定。
  • 主机和目标 cgroup 的 PSI 回到基线,且没有把压力转移到数据库、代理或其他 slice。
  • 保留变更前配置和部署版本;指标变坏、事件继续增加或数据检查失败时,恢复旧版本与旧资源边界并重新验收。

这些动作会破坏判断

不要用 `drop_caches` 证明内存已修复:文件缓存本来可被内核回收,强制清理会改变现场并制造额外 I/O。不要全局关闭 OOM、给关键进程普遍设置极端 `oom_score_adj`、无限扩 swap 或循环重启;这些做法可能把一次可定位的服务故障变成整机长时间抖动。

把下一次事故变成可比较数据

持续保存每个关键 unit 的 MemoryCurrent/Peak、memory.events 增量、cgroup 与主机 PSI,以及同窗请求 SLO、并发和队列深度。告警应关注事件速率和用户影响,而不是一个通用的“内存使用 80%”。内核、systemd、容器运行时或 unit 层级变化后,重新核对 cgroup 版本、属性可用性和父级边界。

本文命令在 2026-08-18 按当前 Linux 内核与 systemd 上游文档完成语义和语法审阅,没有在读者的发行版、VPS 或生产 unit 上执行。cgroup v1、较旧 systemd、容器内 PID/cgroup 命名空间和托管平台可能暴露不同路径或属性;执行前先确认 `stat -fc %T /sys/fs/cgroup`、`systemctl --version` 和实际 unit 层级。

返回知识库