Seafile 13 缩略图服务器优化实践

Seafile 13 版本将缩略图服务从主服务中拆分出来成为独立的 thumbnail-server 容器,这个架构改变在生产环境中遇到了内存泄漏和高并发下的性能问题。经过和AI几天的折腾,总结出以下优化方案。

问题现象

生产环境高并发请求,nginx 大面积499/504,用户看到的缩略图都裂开了。thumbnail-server 运行后可见内存快速增长,直到 OOM Killer 介入,重启,继续涨,继续崩,Grafana 上看内存曲线就是一个又一个锯齿。

问题分析

内存泄漏

通过 tracemalloc 分析发现主要有三处内存泄漏:

  1. SeaFile._content 缓存泄漏:seafobj 库中 SeaFile 对象的 _content 属性会缓存整个文件内容,处理完成后没有清理,导致大文件内容一直驻留在内存中。
  2. 缩略图生成函数无显式清理:PIL 图像对象、临时文件等资源在函数结束后没有显式释放,依赖 Python 的垃圾回收机制,但在高并发场景下 GC 跟不上分配速度。
  3. 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)会全部加载到内存,占用大量内存容量。

性能瓶颈

  1. 异步阻塞:http_response.py 中使用 time.sleep() 而不是 await asyncio.sleep(),阻塞了事件循环,影响并发处理能力。
  2. 队列竞争:PDF/PSD 等慢任务与普通图片在同一队列中处理,慢任务阻塞快任务。
  3. 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_WORKERS324每种队列的 worker 线程数
THUMBNAIL_IMAGE_SIZE_LIMIT100 MB30 MB原始文件大小限制
THUMBNAIL_IMAGE_ORIGINAL_SIZE_LIMIT1024 MB256 MB图片解码后内存限制

PR 提交

以上优化已提交 PR 到官方仓库 haiwen/seafile-thumbnail-server 的 13.0 分支:

PR内容
#23Python 内存泄漏修复
#24asyncio.sleep 修复
#25THUMBNAIL_TASK_WORKERS 环境变量
#26nginx IPv6 修复
#27THUMBNAIL_IMAGE_SIZE_LIMIT 环境变量
#28队列分离优化
#29智能流式处理
#30内存限制自动重启
#32jemalloc 支持

总结

缩略图服务是典型的 I/O 密集型应用,瓶颈主要在 S3 存储的网络延迟而非 CPU。优化的重点是:

  1. 及时释放内存:处理完文件后立即清理缓存
  2. 避免阻塞:异步代码不要用同步 sleep
  3. 队列隔离:快慢任务分开处理
  4. 可配置性:通过环境变量调整参数

优化后内存稳定在 <2GB 左右,高峰期也能正常处理请求。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理