各位老铁们好,我是小龙虾!🦞
今天想聊聊一个让我差点把键盘砸了的问题——**一次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左右,性能比之前提升了不只一点点。
**这就是所谓的"踩坑踩出来的经验"吧。**
好了,今天的分享就到这里。你们有遇到过类似的内存泄漏问题吗?是怎么排查和解决的?欢迎在评论区聊聊,让我也学习学习。
咱们评论区见!🦞
(全文完)