朕喜技术(ZhenxiJS)
2026.08.28
1 分钟阅读

内存泄漏导致容器 OOM Killed(exit code 137)排查与修复实战

日期:2026-08-28 | 分支:2026-08-28-0000(第4题) 技术栈:Python 3.11、Docker Compose、cgroup、psutil、OOM Killer 关键词:OOM Killed、内存泄漏、Memory Leak、exit code 137、cgroup memory limit、Out of Memory、全局缓存无限增长、LRU 淘汰、OrderedDict、PEP 668、externally-managed-environment、--break-system-packages

一、问题背景

Kubernetes 中部署的 Web 服务(容器内存限制 256MB),运行一段时间后进程神秘被杀:

❌ 容器重启了!进程被 OOM Killed (exit code 137)
  • 服务刚启动正常,运行一段时间才崩溃,且崩溃间隔越来越短
  • 代码里没有明显的文件/连接泄漏,CPU、磁盘、网络都正常
  • 内核日志出现:myservice invoked oom-killer / Out of memory: Killed process (python3)

这是非常经典的 内存泄漏(Memory Leak)导致容器 OOM Killed 的生产问题。

二、复现步骤

git checkout 2026-08-28-0000
cd scenario/2026-08-28-0000
docker compose up -d --build
docker compose exec app bash
# 查看内存限制
cat /sys/fs/cgroup/memory.max               # cgroup v2(本环境为 v1)
cat /sys/fs/cgroup/memory/memory.limit_in_bytes  # cgroup v1
# 运行泄漏脚本
python3 memory_leak.py

复现现象(内存持续攀升、永不回落):

初始内存: 11.4 MB
[t=2500]  缓存项: 50,  内存: 61.9 MB
[t=5000]  缓存项: 100, 内存: 111.9 MB
[t=7500]  缓存项: 150, 内存: 162.2 MB
[WARNING] 内存已达 200.3 MB,即将触发 OOM Killed...
[t=117500] 缓存项: 2350, 内存: 2370.9 MB   ← 无限增长

三、根本原因

泄漏脚本 memory_leak.py 中存在长生命周期全局缓存:

_cache = {}
def process_request(request_id):
    _cache[f"req_{request_id}"] = "A" * (1024*1024)  # 1MB/请求
    # 从不清理 → 无限增长

请求持续写入 _cache 但从不清理,导致:

  1. 进程 RSS 持续攀升,最终超过容器 cgroup 内存上限;
  2. 内核 OOM Killer 出手,杀掉进程(exit code 137);
  3. 崩溃间隔越来越短 = 缓存累积对象越多,内存增长越快。

四、解决办法

根因修复:给全局缓存加「容量上限 + LRU 淘汰」

from collections import OrderedDict

MAX_CACHE_SIZE = 50              # 内存有界的关键:限制条目数
_cache = OrderedDict()

def process_request(request_id):
    if len(_cache) >= MAX_CACHE_SIZE:
        _cache.popitem(last=False)            # 满了淘汰最旧
    _cache[f"req_{request_id}"] = "A"*(1024*1024)
    _cache.move_to_end(f"req_{request_id}")   # 标记最近使用

配套生产实践

  • 用现成缓存库(cachetools、Redis + maxmemory、functools.lru_cache),避免手写无限 dict。
  • 容器设置 restart: on-failure 自动重启;合理 mem_limit + mem_reservation。
  • 监控内存使用率,阈值建议 80%,在 OOM 前预警。

排查工具推荐

  • tracemalloc / objgraph / pympler 定位泄漏对象与引用链
  • psutil 采样 RSS、ps aux --sort=-%mem
  • 定时打印 gc.get_objects() 数量判断对象是否只增不减

五、验证

修复后运行脚本,缓存项稳定 50,内存稳定 ~63MB:

[t=50]   缓存项: 50, 内存: 61.9 MB
[t=500]  缓存项: 50, 内存: 63.6 MB
[t=1250] 缓存项: 50, 内存: 63.6 MB   ← 平稳,不再增长

后台跑 12 秒以上,ps -o rss 显示 RSS 恒定 65064 KB(约 63.5MB),泄漏已修复,不再 OOM。

六、经验与避坑

  1. 全局缓存必须「有界」——这是内存泄漏最典型的隐藏源头。任何 dict/list 只要长生命周期且只增不减,都会拖垮服务。
  2. 崩溃间隔越来越短 = 内存增长速度越来越快,这是内存泄漏的典型特征,与「峰值突发」不同。
  3. exit code 137(128+9,SIGKILL) 通常意味着被 OOM Killer 杀掉,dmesg / kern.log 能确认。
  4. PEP 668 坑:Debian bookworm(Python 3.11)下 pip3 install 会报 externally-managed-environment,需加 --break-system-packages,或用 venv / pipx。
  5. 嵌套 Docker 的 cgroup 限制不生效:pipeline 容器内再起 docker 时,docker inspect 显示 HostConfig.Memory 正确,但容器内 cgroup 读到的是宿主机整机内存。此时观察「内存无限增长」即可稳定复现根因,不必纠结于精确到 256MB 的 OOM kill。

版权声明

本文为博主原创文章,遵循 CC BY-NC-SA 4.0 许可协议。 转载请注明出处:http://localhost/blogs/2026-08-28-0015-oom-killed

分享这篇文章