在内存受限容器里触发 cgroup OOM,练习区分 OOMKilled、exit 137 与进程内部内存错误

演练目标

环境准备

隔离边界:只在一台一次性 Linux 虚拟机或本地 Docker Desktop 上做,所有容器都加 --memory 与 --memory-swap,不要调整宿主机内存、不要关闭 swap、不要对生产节点执行 stress。实验结束删除容器即可,不会改动宿主机内核参数。

需要 Docker 和 python:3.12-alpine 镜像,预留约 300 MB 磁盘。

docker version --format '{{.Server.Version}}'
docker info --format 'cgroup={{.CgroupVersion}} driver={{.Driver}}'
docker pull python:3.12-alpine

确认 cgroup=2。cgroup v1 的路径是 /sys/fs/cgroup/memory/memory.oom_control,本页以 v2 的 memory.events 为准。

注入故障

启动一个带硬限制的容器,主进程只做一次大块内存分配。分配量远大于 64 MiB,且关闭 swap 配额,确保触发的是本容器 cgroup 的 OOM,而不是宿主机内存压力:

docker run -d --name lab-oom --memory=64m --memory-swap=64m \
  python:3.12-alpine \
  python3 -c 'a = bytearray(256 * 1024 * 1024); import time; time.sleep(300)'
sleep 5
docker ps -a --filter name=lab-oom

--memory-swap 与 --memory 相等表示容器不能使用 swap。sleep 5 给内核留出统计和回收时间;如果没有退出,用 timeout 30 docker logs -f lab-oom 观察,不要直接加大到宿主机级别。

观察现象

docker inspect -f 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' lab-oom
timeout 5 docker events --since 5m --filter container=lab-oom --filter event=oom
docker logs --tail=20 lab-oom

预期 OOMKilled=true、ExitCode=137。137 = 128 + 9,只说明进程收到 SIGKILL;只有 OOMKilled=true 或 cgroup 的 oom_kill 计数增加,才能把这次退出归因于内存超限。

再看 cgroup 计数。重新起一个常驻容器,让分配进程在容器内被 kill,而 PID 1 继续存活,便于读取计数:

docker run -d --name lab-oom-events --memory=64m --memory-swap=64m \
  python:3.12-alpine sleep 300
docker exec lab-oom-events python3 -c 'a = bytearray(256 * 1024 * 1024)'
docker exec lab-oom-events cat /sys/fs/cgroup/memory.events
docker exec lab-oom-events cat /sys/fs/cgroup/memory.current

预期 memory.events 中出现 oom 1 与 oom_kill 1,memory.current 在 64 MiB 上限附近。若 oom_kill 为 0,说明分配被 Python 的 MemoryError 或 overcommit 拦截,并没有真正触发 cgroup OOM。

有宿主机 root 时,可在一次性虚拟机上核对内核日志:

sudo dmesg -T | grep -iE 'oom|killed process' | tail -10

预期能看到 Memory cgroup out of memory: Killed process ... (python3)。容器内通常看不到宿主机 dmesg,不要因为容器里没有该日志就否定 OOM。

判断与定位

  1. 先写假设:是容器 cgroup 超限、宿主机内存不足,还是进程自己收到 kill -9。假设不同,取证位置不同。
  2. 看 OOMKilled 与 exit code。OOMKilled=true 才指向内存;exit 137 单独出现可能来自 docker kill、Kubernetes 探针失败、节点驱逐或人为 kill -9。
  3. 看 memory.events 的 oom_kill 是否增长,以及 memory.current、memory.peak 是否贴近 memory.max。
  4. 看容器是否设置了 --memory-swap。只设 --memory 时容器可能使用 swap,内存增长被掩盖,表现为延迟抖动而不是立即 OOM。
  5. 容易误判:Python、Java、Go 进程报 MemoryError、OutOfMemoryError 或 runtime OOM,不等于被 cgroup OOM kill。前者是进程内部申请失败,后者是内核杀进程,二者日志和计数完全不同。
  6. 另一个误判:容器重启后 docker inspect 的 OOMKilled 只反映最近一次退出;要看 RestartCount 和事件历史,避免把旧结论套到新实例上。

修复

演练环境直接删除容器;生产环境按“限制、观测、修复”顺序处理:

docker rm -f lab-oom lab-oom-events

真实服务应先确认内存增长曲线和 limit 是否匹配:

docker run -d --name lab-oom-fixed --memory=256m --memory-swap=256m \
  python:3.12-alpine \
  python3 -c 'a = bytearray(64 * 1024 * 1024); import time; time.sleep(30)'
sleep 3
docker inspect -f 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' lab-oom-fixed
docker stats --no-stream lab-oom-fixed

OOMKilled=false 且进程正常退出即表示限制与负载匹配。生产中还应检查泄漏、缓存上限、JVM heap 与容器 limit 的比例,并给 container_memory_working_set_bytes、OOM 计数配告警。

清理与回滚

docker rm -f lab-oom lab-oom-events lab-oom-fixed 2>/dev/null || true

本页没有修改宿主机配置和内核参数,不需要回滚系统设置。不要用 docker system prune -a 清理,以免误删其他实验镜像。

复盘

延伸阅读

读完自测

  1. OOMKilled=true 与 exit code 137 是什么关系?
  2. 容器设置 memory limit 后仍然使用 swap,会怎样影响 OOM 判断?
  3. memory.events 中的 oom_kill 为 0,但进程退出了,可能是什么原因?
答案
  1. OOMKilled 是 cgroup 内存超限的明确标记,exit 137 只是收到 SIGKILL 的结果,137 还可能来自人为 kill、探针或驱逐。
  2. swap 会延后 OOM,表现为性能抖动和内存持续上涨,不能只看是否被杀来判断内存是否超限。
  3. 可能进程自己申请失败退出、被其他信号杀死,或根本没有触发 cgroup OOM,应结合日志和 exit code 判断。