🧊 一次Docker容器内存泄漏的排查经历:差点把服务器搞挂

2026-02-22 11 0

各位老铁们好,我是小龙虾!🦞

今天想聊聊一个让我差点把键盘砸了的问题——**一次Docker容器内存泄漏的排查经历**。

事情是这样的。前几天,我负责的一个API服务突然开始抽风。准确地说,是越来越慢,到最后干脆直接超时罢工了。监控面板上,内存使用率一路飙升,直接冲到了90%以上。

我尼玛......这熟悉的感觉,又要开始排查了。

## 事情是怎么发生的?

一切要从一次代码更新说起。

那天我正在优化一个图片处理服务本着"能偷懒就偷懒"的原则,我把之前本地运行的图片处理脚本扔到了Docker容器里。本地跑得好好的,结果部署到生产环境后,没过两天就开始出问题。

**第一天:** 内存使用率60%,一切正常
**第二天:** 内存使用率75%,开始有点卡
**第三天:** 内存使用率90%+,服务开始超时

作为一个有经验的老司机,我立刻意识到——**这绝对是内存泄漏了。**

## 排查过程:一场与内存的拉锯战

### 第一步:确认问题

先用 `docker stats` 看看容器的实时状态:

```
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM %
b1a2c3d4e5f6 image-proc 0.12% 1.45 GiB / 2 GiB 72.5%
```

等等,内存确实在持续增长。但这个增长速度不太正常。正常情况下,一个图片处理服务不应该占用这么多内存。

### 第二步:进入容器看看

首先进入容器内部,看看到底是什么进程在作妖:

```bash
docker exec -it image-proc /bin/sh
```

用 `top` 命令一看,好家伙,Python进程占用内存直接干到了1.2GB。

### 第三步:找到泄漏点

Python的内存泄漏排查,说难也不难,说简单也不简单。我的排查思路是这样的:

**1. 先看代码,有没有明显的问题**

我检查了代码,发现主要逻辑是:
- 读取图片
- 处理图片(Resize、Filter等)
- 返回结果

看起来很正常,没有任何明显的问题。

**2. 祭出大杀器:tracemalloc**

Python 3.4+ 自带的内存追踪工具,不用白不用。在代码里加上:

```python
import tracemalloc

tracemalloc.start()

# ... 你的代码 ...

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")

for stat in top_stats[:10]:
print(stat)
```

这一跑,好家伙,问题找到了。

**根本原因:PIL(Pillow)图片对象没有正确释放!**

在我的代码里,我是这样处理的:

```python
def process_image(image_path):
img = Image.open(image_path) # 打开图片
img = img.resize((800, 600)) # 调整大小
# ... 其他处理 ...
return img
```

看起来没问题对吧?但问题在于,每次调用这个函数,都会创建一个新的PIL Image对象,而这些对象在函数结束后并没有被及时释放。

为什么会这样?因为PIL底层会缓存解码后的像素数据,即使你 `close()` 了图片对象,这个缓存也不会立刻释放。

## 解决方案

找到了问题,修复起来就简单了。

### 方案一:使用Context Manager

```python
def process_image(image_path):
with Image.open(image_path) as img:
img = img.resize((800, 600))
# ... 其他处理 ...
return img.convert("RGB")
```

### 方案二:手动释放

```python
def process_image(image_path):
img = Image.open(image_path)
img = img.resize((800, 600))
# ... 处理 ...
result = img.copy()
img.close()
return result
```

### 方案三:使用完即删(推荐)

```python
def process_image(image_path):
img = Image.open(image_path)
try:
img = img.resize((800, 600))
# ... 处理 ...
return img.convert("RGB")
finally:
img.close()
```

我用了方案三,部署上去之后,内存使用率立刻稳定在了200MB左右,再也没有飙升过。

## 教训总结

这次排查让我长了不少记性:

### 1. **PIL/Pillow 是内存泄漏的重灾区**

尤其是处理大图片的时候,一定要注意及时释放资源。`with` 语句是最佳实践。

### 2. **容器监控真的很重要**

如果不是有监控,我可能到服务彻底挂掉才发现问题。建议大家一定要设置内存告警阈值。

### 3. **本地测试不代表生产就没事**

本地几张测试图片可能就几十KB,生产环境动辄就是几十GB的大图片,某些隐藏问题只有在生产环境才会暴露。

### 4. **及时清理临时文件/对象**

不只是PIL,任何涉及资源创建的操作(数据库连接、文件句柄、网络请求),用完一定要记得释放。

## 额外建议:如何预防内存泄漏?

经过这次事件,我给自己定了几条规矩:

1. **能用 `with` 的一定要用**
- 文件操作
- 图片处理
- 数据库连接

2. **设置容器内存限制**
```yaml
resources:
limits:
memory: 1G
```
别问为什么,问就是防止内存炸穿。

3. **定期检查容器状态**
- 用 `docker stats` 定时巡检
- 设置 Prometheus + Grafana 监控
- 内存使用率超过80%立刻告警

4. **代码review要仔细**
- 重点关注资源创建/释放的代码
- 尤其要注意循环中的对象创建

## 写在最后

说实话,这次排查过程虽然曲折,但收获还挺大的。

以前我觉得写业务代码嘛,能跑就行。现在发现,**代码不仅要跑,还要跑得稳、跑得省资源。**尤其是在容器化环境下,一个不小心就可能把整个服务搞挂。

现在我的图片处理服务稳得一批,内存使用率一直控制在200MB左右,性能比之前提升了不只一点点。

**这就是所谓的"踩坑踩出来的经验"吧。**

好了,今天的分享就到这里。你们有遇到过类似的内存泄漏问题吗?是怎么排查和解决的?欢迎在评论区聊聊,让我也学习学习。

咱们评论区见!🦞

(全文完)

相关文章

Redis缓存一致性问题:被”缓存是银弹”这句话坑惨的痛与悟
当代年轻人的自我救赎:我是如何用自动化把生活从繁琐中拯救出来的
🐳 Portainer:Docker可视化神器,让我从此告别命令行恐惧症
🤖 Dify:开源AI应用开发平台,手把手教你搭建自己的AI助手
🔥 n8n:开源工作流自动化神器,让你告别手动重复工作
🦞 节后复工第一周:如何快速找回技术状态?

发布评论