Linux服务器发生OOM后的排查方法和解决步骤

描述

 

Linux 服务器发生 OOM 后,运维人员经常遇到一种看似矛盾的现场:监控图上还有可用内存,free -h 也没有显示物理内存耗尽,但应用进程已经被杀,Kubernetes Pod 状态中甚至明确写着 OOMKilled

这个现象并不矛盾。OOM 不是只有“整台主机物理内存用完”一种触发路径。进程可能超过 cgroup 限额,节点可能在 NUMA 节点或特定内存域中分配失败,内核可能因无法满足高阶连续内存分配而进入 OOM,systemd-oomd 也可能根据内存压力主动终止 cgroup。监控采样周期还可能错过秒级尖峰,进程被杀后释放的内存又会让现场看起来恢复正常。

本文从故障现场出发,建立一套可复核的 OOM 排查方法。示例以现代 Linux 为主,同时区分传统全局 OOM、cgroup v2、systemd-oomd 和 Kubernetes 场景。不同内核、systemd、容器运行时和 Kubernetes 版本的日志字段可能不同,应以本机实际输出为准。

一、先确认发生的是哪一种“OOM”

不要看到应用退出就直接判断 OOM。至少要区分以下情况:

  1. 全局 OOM :主机整体内存回收困难,内核 OOM killer 选择并终止进程。
  2. cgroup OOM :某个服务或容器达到自己的内存上限,即使主机仍有大量可用内存,也可能在该 cgroup 内触发 OOM。
  3. Kubernetes 容器 OOMKilled :容器进程超过内存限制或所在 cgroup 发生 OOM,kubelet/运行时将退出原因记录为 OOMKilled。
  4. systemd-oomd 主动处置 :systemd-oomd 根据 PSI 等压力信息选择 cgroup 并发送终止信号。它是用户态策略守护进程,不等同于内核 OOM killer。
  5. 应用自身内存错误 :例如 JVM 抛出 java.lang.OutOfMemoryError、数据库内部内存限制报错、malloc 返回失败。这些错误不一定伴随内核杀进程。
  6. 被外部信号终止 :发布系统、探针、管理员或进程管理器也可能发送信号。退出码 137 通常表示进程收到 SIGKILL,但 SIGKILL 的来源不只 OOM。

排查的第一目标不是马上调参数,而是确认终止者和限制边界。

二、现场信息收集:先保护证据

记录故障发生时间、主机、进程或 Pod、应用版本、最近变更以及监控采样周期。确认当前登录节点和系统时间:


			   bashhostnamectl date --iso-8601=seconds uptime 

立即保存内存概况:


			   bashfree -h cat /proc/meminfo vmstat 1 10 ps -eo pid,ppid,user,stat,rss,vsz,pmem,etime,comm,args --sort=-rss | head -n 30 

ps 的 RSSVSZ 只适合初筛。共享页会造成重复计算,虚拟地址空间也不等于已占用物理内存。要分析单个进程的实际映射,可读取:


			   bashPID=12345 cat "/proc/$PID/status" cat "/proc/$PID/smaps_rollup" cat "/proc/$PID/limits" 

PID=12345 是占位示例,必须替换为现场仍存在的目标进程 PID。进程已退出时 /proc/$PID 会消失,应转向内核日志、服务日志、监控和 cgroup 计数器。

如果要把输出写入文件,先检查诊断目录容量和 inode。不要因为磁盘紧张直接清理系统日志:OOM 日志是根因证据,删除或截断会破坏分析和审计。


			   bashdf -h /tmp df -i /tmp 

三、正确理解 free:free 不等于 available

执行:


			   bashfree -h 

下面只用于解释字段格式。

示例输出:


			   text               total        used        free      shared  buff/cache   available Mem:            31Gi         24Gi       600Mi       1.2Gi        6.4Gi        5.3Gi Swap:          4.0Gi        1.1Gi        2.9Gi 

free 很低通常不是问题。Linux 会利用空闲内存保存页缓存,必要时回收。比 free 更有参考价值的是 MemAvailable:它是内核基于空闲页、可回收页缓存、水位等信息估算的、在不进行交换的情况下可供新应用使用的内存,但仍然只是估算值。

以下情况会造成“当前 available 还很多,但之前已经 OOM”:

  • OOM killer 已杀掉大进程,内存随进程退出被释放。
  • 监控每 1 分钟采样一次,内存尖峰在两次采样之间发生并结束。
  • 发生的是 cgroup OOM,主机的 MemAvailable 并不是该 cgroup 的可用额度。
  • 监控展示的是节点总内存,但应用受容器 limit、systemd MemoryMax= 或其他层级限制。
  • 失败的分配要求特定内存域或连续页,整机汇总的 available 无法说明该分配一定成功。

因此,不能用故障后的 free -h 推翻 OOM 日志,也不能只用一条内存总量曲线判断限制边界。

四、先查内核日志:确认全局 OOM 或 memcg OOM

在 systemd 系统上查询故障窗口:


			   bashjournalctl -k --since '2026-07-10 1400' --until '2026-07-10 1400' --no-pager 

时间是示例,必须替换为实际故障窗口。再按常见关键字过滤:


			   bashjournalctl -k --since '-2 hours' --no-pager | grep -Ei 'out of memory|oom-kill|killed process|memory cgroup' 

没有 journal 持久化的系统,可检查 /var/log/messages/var/log/syslog 或日志平台,具体位置由发行版和 rsyslog/journald 配置决定。容器中的 dmesg 可能因权限或命名空间限制看不到宿主机日志。

内核 OOM 记录通常包含触发任务、gfp_mask、order、约束信息、进程内存列表和最终 Killed process。字段随内核版本变化,不应依赖单一固定正则。分析时重点看:

  • 日志是否明确出现 oom-kill 或 Out of memory
  • constraint mems_allowednodemask 等信息是否提示 cpuset、NUMA 或 memcg 约束。
  • 是否出现 Memory cgroup out of memory 或 cgroup 路径。
  • 被杀 PID、进程名、total-vmanon-rssfile-rssshmem-rss
  • 故障前后的内存回收、交换、内核告警和进程启动记录。

下面仅用于展示可能的日志形态,字段不保证与所有内核一致。

示例输出:


			   textkernel: oom-kill:constraint=CONSTRAINT_MEMCG,...,task=example,pid=12345,... kernel: Memory cgroup out of memory: Killed process 12345 (example) ... 

如果日志中是 CONSTRAINT_MEMCG 或明确给出 memory cgroup 路径,主机还有 available 并不奇怪:失败发生在 cgroup 限额内。若为全局 OOM,则继续结合 /proc/meminfo 历史指标、交换活动、slab、页表和内核日志判断系统为何回收失败。

五、检查 cgroup:最常见的“主机有内存,应用仍 OOM”

先确定目标进程属于哪个 cgroup:


			   bashcat "/proc/$PID/cgroup" 

cgroup v2 的输出通常包含统一层级路径。根据实际路径拼出目录,以下只是占位示例:


			   bashCGROUP_PATH=/sys/fs/cgroup/system.slice/example.service cat "$CGROUP_PATH/memory.current" cat "$CGROUP_PATH/memory.max" cat "$CGROUP_PATH/memory.high" cat "$CGROUP_PATH/memory.events" cat "$CGROUP_PATH/memory.stat" cat "$CGROUP_PATH/memory.pressure" 

CGROUP_PATH 必须替换为 /proc/$PID/cgroup 对应的真实目录。常见字段含义:

  • memory.current :当前 cgroup 及其后代的内存使用量。
  • memory.max :硬上限;值为 max 时表示未设置硬上限。
  • memory.high :高水位控制。超过后进程会受到回收和限制,但它不是硬上限。
  • memory.events :可能包含 lowhighmaxoomoom_kill 等累计事件计数,具体字段取决于内核版本。
  • memory.stat :匿名页、文件页、内核内存、slab 等细分统计。
  • memory.pressure :PSI 内存压力信息。

memory.events 是累计计数,单次看到 oom_kill 3 只能说明该 cgroup 生命周期内累计发生过 3 次,不能证明刚刚又发生一次。应比较故障前后增量,或结合日志时间。

systemd 服务的限制还可以通过以下命令确认:


			   bashsystemctl show example.service -p MemoryCurrent -p MemoryHigh -p MemoryMax -p MemorySwapMax -p OOMPolicy systemctl cat example.service 

example.service 是占位符,应替换为真实单元名。还要检查 drop-in 配置,因为 /etc/systemd/system/example.service.d/*.conf 可能覆盖主单元设置。

不要只检查当前服务层。cgroup 限制具有层级关系,父 slice 的 MemoryMax= 也可能限制多个子服务的合计用量。可使用:


			   bashsystemd-cgls systemd-cgtop 

六、Kubernetes 中如何确认 OOMKilled

Kubernetes 命令必须明确 namespace。以下 $NAMESPACE$POD$CONTAINER 都是占位符:


			   bashNAMESPACE=production POD=example-pod CONTAINER=example-container kubectl -n "$NAMESPACE" get pod "$POD" -o wide kubectl -n "$NAMESPACE" describe pod "$POD" kubectl -n "$NAMESPACE" get pod "$POD" -o jsonpath='{range .status.containerStatuses[*]}{.name}{" "}{.restartCount}{" "}{.lastState.terminated.reason}{" "}{.lastState.terminated.exitCode}{" "}{end}' kubectl -n "$NAMESPACE" logs "$POD" -c "$CONTAINER" --previous --timestamps 

如果容器从未重启或上一实例日志已不可用,--previous 可能返回错误。还要检查工作负载清单中的资源限制:


			   bashkubectl -n "$NAMESPACE" get pod "$POD" -o jsonpath='{range .spec.containers[*]}{.name}{" requests="}{.resources.requests.memory}{" limits="}{.resources.limits.memory}{" "}{end}' kubectl -n "$NAMESPACE" get events --field-selector involvedObject.name="$POD" --sort-by='.metadata.creationTimestamp' 

事件有保留期限,不能把“没有事件”解释成“没有 OOM”。完整证据应包含容器 lastState.terminated.reason、节点内核或运行时日志、cgroup memory.events 增量以及故障前容器内存曲线。

Prometheus 常见容器指标包括 container_memory_working_set_bytescontainer_memory_rsscontainer_memory_cache 和 container_memory_failcnt,但采集链和 cgroup 版本会影响可用性与语义,必须注明:以实际 exporter 暴露的指标为准。working_set 也不能简单等同于“不可回收内存”。

如果 Pod 没有内存 limit,它仍可能因节点全局 OOM 被杀;Kubernetes 的 QoS 类别和 oom_score_adj 会影响节点压力下的选择,但不是绝对保护。检查:


			   bashkubectl -n "$NAMESPACE" get pod "$POD" -o jsonpath='{.status.qosClass}{" "}' 

七、systemd-oomd:不是内核 OOM,也可能杀服务

确认服务是否运行并检查日志:


			   bashsystemctl status systemd-oomd --no-pager journalctl -u systemd-oomd --since '-2 hours' --no-pager oomctl 

oomctl 是否可用取决于 systemd 版本。systemd-oomd 通常依据 cgroup v2 和 PSI 进行策略处置,相关单元可配置 ManagedOOMSwap=ManagedOOMMemoryPressure= 等属性;具体支持项必须以本机 man systemd.resource-control 和 systemd 版本为准。

如果日志明确表明 systemd-oomd 对某个 cgroup 执行了 kill,就不能把根因写成“内核内存耗尽”。应继续确认:哪个上级 cgroup启用了管理策略、压力持续多久、swap 使用情况、服务的内存增长与业务负载是否匹配。

不要为避免误杀直接停用 systemd-oomd。停用会改变整机内存压力保护行为,可能把可控的服务终止演变成系统级卡死或全局 OOM。任何策略调整都应先明确影响的 slice、验证压力阈值并准备恢复原配置。

八、应用 OOM 与内核 OOM 要分开分析

JVM 的 OutOfMemoryError 可能来自 heap、metaspace、direct buffer 或线程创建,不一定由内核杀进程。即使 Java 堆未达到容器 limit,native memory、线程栈、JIT 和共享库仍会计入容器内存。对仍在运行且 JDK 工具可用的进程,可读取:


			   bashjcmd "$PID" VM.flags jcmd "$PID" GC.heap_info jcmd "$PID" VM.native_memory summary 

Native Memory Tracking 必须在 JVM 启动时启用,否则最后一条不能提供完整信息。堆转储可能造成停顿和大量磁盘 I/O,不能在磁盘余量不足或主实例高峰期贸然执行。数据库及 Go、Node.js、Python 等运行时有各自的内存模型,不能套用 JVM 参数;应使用对应版本的诊断接口,并与 cgroup 和内核数据对齐。应用报分配失败但内核没有 OOM 记录时,还要检查应用自身上限、运行时错误类型和 /proc/$PID/limits

九、全局 OOM 的进一步证据:内存都去了哪里

从 /proc/meminfo 关注 AnonPagesShmemSReclaimableSUnreclaimSlabPageTablesHugePages_FreeCommitted_AS 和 CommitLimit,字段是否存在取决于内核配置。查看 slab:

查看 slab:


			   bashslabtop -o 

SUnreclaim 或某类 slab 持续增长时,要结合时间序列、内核版本和工作负载分析;单次值大不能证明内核泄漏。tmpfs 也会计入内存:


			   bashfindmnt -t tmpfs df -h -t tmpfs 

删除仍被进程打开的 tmpfs 文件不会立即释放占用。lsof +L1 可查找这类文件,但大型主机上应限定范围。

检查 swap 和 PSI:


			   bashswapon --show vmstat 1 10 cat /proc/pressure/memory 

vmstat 的 siso 表示每秒换入、换出;已有 swap 使用量不等于正在频繁交换。PSI 的 some 和 full 反映任务因内存压力停顿的情况,应与历史基线比较。没有 swap 不一定错误,有 swap 也不能解决持续泄漏;Kubernetes 节点的 swap 支持和策略还存在版本差异。

检查 overcommit:


			   bashsysctl vm.overcommit_memory vm.overcommit_ratio grep -E 'CommitLimit|Committed_AS' /proc/meminfo 

Committed_AS 不是当前 RSS。修改 overcommit 会改变数据库、缓存和大规模 fork 的分配行为,属于内核级高风险变更。NUMA 主机还要检查目标进程允许使用的内存节点:


			   bashnumactl --hardware numastat numastat -p "$PID" cat "/proc/$PID/status" | grep -E 'Mems_allowed|Cpus_allowed' 

numactl/numastat 来自 numactl 软件包。应结合 OOM 日志中的 mems_allowed 和绑定策略判断,不能在未评估延迟、带宽影响时直接修改 NUMA 或 overcommit 参数。

十、从证据推导根因,而不是从结果猜原因

根因必须由同一时间窗口的证据组合支持:cgroup OOM 要有 memory.current/memory.maxmemory.events 增量和内核日志;内存泄漏要有同版本同负载下匿名内存或运行时堆持续增长,以及对象或分配路径证据;瞬时峰值要有高分辨率并发与内存曲线;全局 OOM 要有内核日志、MemAvailable、swap、PSI 和回收恶化;systemd-oomd 处置要有其日志与策略证据。

还要区分触发条件和业务根因。memory.max 被触发是直接原因,但业务根因可能是缓存无界、批任务并发上升,或 limit 与实际工作集不匹配。仅凭 RSS 大、退出码 137 或“重启后恢复”都不足以证明泄漏或内核 OOM。

十一、修复方案:先控制风险,再改限制

1. 临时控制

优先使用应用已有的限流、并发控制、任务暂停、缓存上限或流量摘除能力。暂停批任务或降低并发也可能影响数据时效和积压,执行前应确认队列长度、任务幂等性、恢复速度和业务 SLA。

2. 调整容器或 systemd 内存上限

提高 limit 不是无成本操作。Kubernetes Deployment 资源字段变更通常会触发滚动更新;systemd MemoryMax= 修改后可能需要 daemon-reload 和服务重启或重新应用属性,具体取决于修改方式。

这类高风险操作必须明确:

  • 影响范围 :目标 Pod、服务或父 slice,以及节点上其他工作负载可用内存。
  • 执行前检查 :当前工作集、峰值、节点可分配内存、剩余容量、QoS、PDB、副本数和滚动策略。
  • 备份方式 :保存原 Kubernetes 清单、Helm values、systemd 单元及 drop-in;保存 OOM 日志和指标快照。
  • 灰度方法 :先在测试环境或单副本/单节点灰度,限制流量,观察至少一个有代表性的业务周期。可先用版本控制中的清单做 diff;不要直接在所有节点手改。
  • 验证方式 :验证 memory.events 不再增长、工作集和 RSS 有上界、PSI 正常、错误率与延迟无回归、节点余量充足。
  • 回滚方案 :恢复原清单或上一发布版本,重新滚动;若放大 limit 导致节点压力,立即停止扩面并把流量切回健康实例。

Kubernetes 可先做客户端 dry-run 验证清单语法,但 dry-run 不能验证调度容量和运行时内存行为:


			   bashkubectl -n "$NAMESPACE" apply --dry-run=client -f workload.yaml 

workload.yaml 是待评审的真实清单路径占位符。正式变更前还应通过代码评审、服务端校验和既定发布系统。

3. 增加或调整 swap

创建 swap 文件、执行 mkswap/swapon、修改 /etc/fstab 都会改变主机内存行为和启动配置,属于高风险操作。影响范围是整台主机;执行前检查磁盘空间、文件系统是否支持、加密要求、Kubernetes/kubelet 策略、应用对换页延迟的容忍度。备份 /etc/fstab 和当前 swapon --show 输出,先在同类测试节点验证,并逐台灰度。

验证应包含 swapon --showfree -hvmstat、PSI、应用 P95/P99 延迟和节点重启后的挂载状态。回滚需要先确认 swap 使用已安全迁移,不能在内存余量不足时直接 swapoff,否则可能触发 OOM。本文不提供可直接复制的 swap 创建命令,避免在未确认文件系统、磁盘和集群策略时误操作。

4. 修改内核参数

vm.overcommit_memoryvm.overcommit_ratiovm.swappiness、HugePages 和 NUMA 相关参数均可能影响整机工作负载。执行前必须记录原值、确认应用和数据库官方建议、在同配置节点压测,并通过配置管理系统灰度。临时 sysctl -w 与持久配置的生效范围不同,回滚必须同时恢复运行值和持久文件,避免重启后再次变化。

十二、修复验证与回滚判据

修复后不要只验证“进程没再被杀”。至少观察:

  • memory.current  与 memory.max 的距离,以及 memory.events 增量。
  • 节点 MemAvailable、swap 活动、PSI、slab 和页表是否稳定。
  • 应用工作集、匿名内存、堆、native memory 是否达到稳定平台。
  • 请求量、并发、错误率、吞吐、P95/P99 延迟是否恢复。
  • Kubernetes Pod 重启次数、lastState 和节点资源余量。
  • 是否把内存压力转移到了同节点其他服务或下游依赖。

出现以下任一情况应停止扩面并回滚:节点 PSI 或换页明显恶化、其他服务延迟上升、OOM 仍持续、内存曲线仍无上界、滚动过程中可用副本不足,或新限制导致调度失败。

十三、复盘与长期治理

一次完整复盘至少应记录:终止者、限制边界、被杀对象、故障时间线、直接触发条件、业务根因、修复验证和回滚结果。长期治理建议包括:

  • 同时监控节点、父 cgroup、服务和容器内存,不只看节点使用率。
  • 提高关键内存指标的采样精度,保留 memory.events、PSI、工作集、RSS、匿名页和重启原因。Prometheus 指标以实际 exporter 暴露的指标为准。
  • 对缓存、连接池、任务并发和单请求最大内存建立显式上限。
  • 用真实负载建立基线,根据工作集、峰值和安全余量制定 limit,而不是按经验随意翻倍。
  • 保存内核日志和容器上一实例日志,统一节点、应用、监控和发布系统时间。
  • 为内存变更准备版本化清单、单实例灰度、容量检查和自动回滚条件。

“服务器还有内存”只描述了主机某一时刻的汇总状态。是否会 OOM,取决于分配发生在哪个边界、当时的压力、限制、回收能力和策略。把全局 OOM、cgroup OOM、应用 OOM 与用户态策略终止区分开,再用日志、计数器和高分辨率指标建立证据链,才能真正解释为什么进程被杀,以及下一次如何避免。

 


打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分