在线文件处理服务架构设计:如何支撑 10 万日活的 PDF/图片转换

发布于 2026-09-01

问题背景

我们的在线 PDF 转换服务在日活 10 万量级下遇到了三个核心问题:Gunicorn worker 超时(HTTP 500)、内存泄漏导致容器频繁重启、单文件处理太慢影响用户体验。本文分享最终落地的架构方案。

旧架构的问题

用户 → Nginx → Gunicorn sync worker → 同步处理(阻塞)→ S3
              ↑ 4 个 worker,任何长任务都会占满

问题清单:

  1. Gunicorn sync worker 被长任务占满:PDF OCR 一次要 15 秒,4 个 worker 全被占 → 后续请求排队超时
  2. Registry 坏条目阻塞:历史遗留的 registry_index.json 条目指向不存在的文件,每个条目触发 10 次重试 ≈ 1.1s,15 条 → 16.5s 超时
  3. 进程级内存膨胀:图片处理任务的 PIL 对象不会被及时回收

新架构:同步入口 + 异步任务调度器

用户 → Nginx → Gunicorn(sync 4 worker,只做 API 层)
                     ↓ 立即返回 task_id
              Redis 任务队列
                     ↓
              独立 TaskScheduler(多进程)
                     ↓
              专用 Worker 进程池
                     ↓
              每个任务独立子进程 → 处理完自动释放内存

核心设计:每个任务跑在独立子进程里

import multiprocessing as mp

def run_in_isolated_process(task_func, *args, **kwargs):
    """把 CPU 密集型任务放到独立子进程,处理完自动释放内存"""
    ctx = mp.get_context('spawn')
    queue = ctx.Queue()
    proc = ctx.Process(target=task_func, args=(queue, *args), kwargs=kwargs)
    proc.start()
    proc.join(timeout=120)
    if proc.is_alive():
        proc.terminate()   # 硬杀,避免内存泄漏
        proc.join()
        raise TimeoutError('任务超时')
    return queue.get()

Registry 自动清理 + 降级

为了解决旧机器拷贝过来的坏条目问题,做了三层防御:

# 1. FileBatchRegistry.get() 加载失败自动 pop 坏条目
def get(self, batch_id):
    try:
        return self._read_entry(batch_id)
    except FileNotFoundError:
        self._registry.pop(batch_id, None)   # 坏条目直接删
        self._save_index()
        return None

# 2. load_batch_info() 在调用 set_batch_task_dir 之前先检查目录
#    不存在立即 raise,而不是创建空壳目录 + 重试 10 次
candidate = os.path.join(TEST_DIR, user_id, batch_id)
if not os.path.isdir(candidate):
    raise FileNotFoundError(f'批次目录不存在: {candidate}')

# 3. get_user_task_num() 整体 + 内层逐条 try/except,降级返回 0
try:
    return sum(1 for batch_id in registry if ...)
except Exception:
    return 0

效果对比

指标 改造前 改造后
平均响应时间(PDF 压缩) 4.2s 1.8s
HTTP 500 错误率 3.7% 0.02%
容器重启频率 每日 3-5 次 0
峰值并发 60 300+

经验总结

"在线文件处理服务的核心不是优化某个算法,而是隔离任务生命周期——每个任务独立子进程、独立内存、独立超时,哪怕它挂了也不会影响整个服务。"

这个原则在 OCR、AI 抠图、视频压缩三类 CPU 密集型任务上都得到了验证。

常见问题

这篇文章的代码可以直接用吗?

文中代码示例均来自真实线上项目,但具体使用需要根据你的环境调整参数和依赖版本。

有问题想请教作者怎么办?

欢迎在文章页面留言评论,我们看到后会尽快回复。也可以通过网站联系我们。

相关工具推荐