Seafile 13 版本将缩略图服务从主服务中拆分出来成为独立的 thumbnail-server 容器,这个架构改变在生产环境中遇到了内存泄漏和高并发下的性能问题。经过和AI几天的折腾,总结出以下优化方案。
问题现象
生产环境高并发请求,nginx 大面积499/504,用户看到的缩略图都裂开了。thumbnail-server 运行后可见内存快速增长,直到 OOM Killer 介入,重启,继续涨,继续崩,Grafana 上看内存曲线就是一个又一个锯齿。
问题分析
内存泄漏
通过 tracemalloc 分析发现主要有三处内存泄漏:
- SeaFile._content 缓存泄漏:seafobj 库中 SeaFile 对象的
_content属性会缓存整个文件内容,处理完成后没有清理,导致大文件内容一直驻留在内存中。 - 缩略图生成函数无显式清理:PIL 图像对象、临时文件等资源在函数结束后没有显式释放,依赖 Python 的垃圾回收机制,但在高并发场景下 GC 跟不上分配速度。
- task_results_map 无限增长:任务结果存储在字典中,当客户端超时断开连接时,结果永远不会被取走,字典持续增长。
C 扩展层面的碎片化
Python 层面修复后,内存仍然缓慢增长。用 /proc/PID/smaps 分析发现:
总 RSS: 3020 MB
├── 匿名内存 (malloc): 2995 MB (99%)
│ ├── libavif (Pillow): 743 MB
│ ├── _imagingmath (Pillow): 282 MB
│ └── 碎片化块: 20+ 个 50-64MB 块
└── Python (tracemalloc): 8 MB
Pillow 处理图片时 glibc malloc 分配的内存无法归还系统,造成严重碎片化。
大文件占用内存
大文件(视频/PDF)会全部加载到内存,占用大量内存容量。
性能瓶颈
- 异步阻塞:http_response.py 中使用
time.sleep()而不是await asyncio.sleep(),阻塞了事件循环,影响并发处理能力。 - 队列竞争:PDF/PSD 等慢任务与普通图片在同一队列中处理,慢任务阻塞快任务。
- Worker 数量固定:默认只有 3 个 worker,无法充分利用多核服务器。
Nginx 配置问题
容器内 nginx 配置 proxy_pass http://localhost:8088,当启用IPv6时 localhost 解析到 IPv6 地址,而后端只监听 IPv4,导致连接失败。
优化方案
1. 内存泄漏修复
在 utils.py 的 get_file_content_by_obj_id() 函数中添加 finally 块清理 SeaFile 对象:
def get_file_content_by_obj_id(repo_id, file_id):
f = fs_mgr.load_seafile(repo_id, 1, file_id)
try:
return f.get_content()
finally:
# 清理缓存的文件内容,防止内存泄漏
if hasattr(f, '_content'):
f._content = None
if hasattr(f, 'blocks'):
f.blocks = None
在 thumbnail.py 的各个缩略图生成函数中添加显式的垃圾回收:
def create_image_thumbnail(repo_id, file_id, thumbnail_file, size):
try:
# 原有逻辑
...
finally:
gc.collect()
在 thumbnail_task_manager.py 中添加任务结果过期清理机制:
RESULT_EXPIRE_TIME = 120 # 秒
def _cleanup_expired_results(self):
"""清理超过 120 秒的任务结果"""
now = time.time()
expired_keys = []
with self._results_lock:
for task_id, (result, timestamp) in self.task_results_map.items():
if now - timestamp > RESULT_EXPIRE_TIME:
expired_keys.append(task_id)
for key in expired_keys:
del self.task_results_map[key]
2. 使用 jemalloc 替换 glibc malloc
jemalloc 内存管理更好,能主动归还内存给系统。
Dockerfile 添加:
RUN apt-get install -y libjemalloc2
启动脚本添加:
export JEMALLOC_LIB=${JEMALLOC_LIB:-/usr/lib/x86_64-linux-gnu/libjemalloc.so.2}
if [ -z "$LD_PRELOAD" ] && [ -f "$JEMALLOC_LIB" ]; then
export LD_PRELOAD="$JEMALLOC_LIB"
fi
3. 大文件流式处理
文件大于阈值时不再全部加载到内存,而是流式写入临时文件:
def stream_file_to_path(seafile_obj, dest_path, chunk_size=8*1024*1024):
with open(dest_path, 'wb') as f:
for chunk in iter(lambda: seafile_obj.read(chunk_size), b''):
f.write(chunk)
4. 异步阻塞修复
将 http_response.py 中的 time.sleep() 改为 await asyncio.sleep():
# 修改前
time.sleep(0.2)
# 修改后
await asyncio.sleep(0.2)
5. 队列分离
新增 pdf_queue 处理慢任务(PDF、PSD),让快任务(图片、SVG、XMIND)不被阻塞:
def __init__(self):
self.image_queue = queue.Queue(32) # 快任务
self.pdf_queue = queue.Queue(32) # 慢任务
self.video_queue = queue.Queue(32) # 视频
队列分配:
- image_queue:图片、SVG、XMIND(快)
- pdf_queue:PDF、PSD(慢)
- video_queue:视频(很慢)
6. 环境变量支持
添加 TASK_WORKERS 和 THUMBNAIL_IMAGE_SIZE_LIMIT 环境变量支持,方便根据服务器配置调整:
# settings.py
TASK_WORKERS = int(os.environ.get('THUMBNAIL_TASK_WORKERS', TASK_WORKERS))
THUMBNAIL_IMAGE_SIZE_LIMIT = int(os.environ.get('THUMBNAIL_IMAGE_SIZE_LIMIT', 30))
7. Nginx IPv6 修复
将 localhost 改为 127.0.0.1 强制使用 IPv4:
proxy_pass http://127.0.0.1:8088;
同时启用日志便于排查问题:
access_log /opt/seafile/logs/thumbnail-server.access.log seafileformat;
error_log /opt/seafile/logs/thumbnail-server.error.log;
8. 内存限制自动重启
万一还有漏网之鱼,超过内存限制就自动退出让容器重启:
MEMORY_LIMIT_MB = int(os.environ.get('THUMBNAIL_MEMORY_LIMIT', 4096))
def _check_memory_and_exit():
with open('/proc/self/statm') as f:
rss_pages = int(f.read().split()[1])
rss_mb = rss_pages * 4096 / 1024 / 1024
if rss_mb > MEMORY_LIMIT_MB:
logger.warning(f'Memory {rss_mb:.0f}MB exceeds limit, exiting')
os._exit(1)
生产环境配置
优化后服务器配置缩减到 8 核 16 GB,完全够用。
环境变量配置(/nfs/thumbnail.env):
THUMBNAIL_TASK_WORKERS=32
THUMBNAIL_IMAGE_SIZE_LIMIT=100
THUMBNAIL_IMAGE_ORIGINAL_SIZE_LIMIT=1024
| 变量 | 值 | 默认值 | 说明 |
|---|---|---|---|
| THUMBNAIL_TASK_WORKERS | 32 | 4 | 每种队列的 worker 线程数 |
| THUMBNAIL_IMAGE_SIZE_LIMIT | 100 MB | 30 MB | 原始文件大小限制 |
| THUMBNAIL_IMAGE_ORIGINAL_SIZE_LIMIT | 1024 MB | 256 MB | 图片解码后内存限制 |
PR 提交
以上优化已提交 PR 到官方仓库 haiwen/seafile-thumbnail-server 的 13.0 分支:
| PR | 内容 |
|---|---|
| #23 | Python 内存泄漏修复 |
| #24 | asyncio.sleep 修复 |
| #25 | THUMBNAIL_TASK_WORKERS 环境变量 |
| #26 | nginx IPv6 修复 |
| #27 | THUMBNAIL_IMAGE_SIZE_LIMIT 环境变量 |
| #28 | 队列分离优化 |
| #29 | 智能流式处理 |
| #30 | 内存限制自动重启 |
| #32 | jemalloc 支持 |
总结
缩略图服务是典型的 I/O 密集型应用,瓶颈主要在 S3 存储的网络延迟而非 CPU。优化的重点是:
- 及时释放内存:处理完文件后立即清理缓存
- 避免阻塞:异步代码不要用同步 sleep
- 队列隔离:快慢任务分开处理
- 可配置性:通过环境变量调整参数
优化后内存稳定在 <2GB 左右,高峰期也能正常处理请求。