一、业务目标 & 前提假设
业务目标
• 支持 PDF OCR(多页)和 图片 OCR
• 任务耗时可能较长(几十秒~几分钟)
• 要求:
• 支持大量并发任务,不会把 FastAPI 顶死
• 支持重试(云 OCR 抖一下不要直接失败)
• 支持服务重启后任务可恢复(至少未执行/挂一半的任务还能补偿)
• 支持任务状态查询(PENDING/RUNNING/SUCCESS/FAILED/进度)
前提假设
• 技术栈:FastAPI + ARQ + Redis + Postgres + 对象存储(本地或 MinIO/OSS)
• OCR 方式:
• 可以是 云 OCR API(百度/阿里/腾讯)------IO 密集,非常适合 async
• 或者 本地 OCR 服务(例如 PaddleOCR 独立服务),ARQ 只负责调服务
关键点:真正重 CPU/GPU 的推理最好在独立的推理服务里跑,ARQ 更适合作为"编排 + IO 请求调度"。
⸻
二、基于 ARQ 的整体架构设计
- 组件划分
- FastAPI 服务(api-service)
• 提供 HTTP API:
• POST /ocr/tasks:上传文件 / 提交任务,返回 task_id
• GET /ocr/tasks/{task_id}:查询任务状态+进度+结果摘要
• 负责:
• 文件接收 & 存储(写到对象存储/本地磁盘)
• 创建 DB 记录(任务 & 文档 & 页)
• 把任务扔进 ARQ 队列(只传 ID,不传大文件) - Redis
• ARQ 的队列 + 任务结果存储
• 只存少量任务参数 / 状态,不存大文本(避免 Redis 爆) - ARQ Worker(ocr-worker)
• 使用 arq worker.WorkerSettings 启动
• 核心任务:
• ocr_document(doc_id, retry_count=0)
• 内部:拆页 → 并发调用 OCR → 存 DB → 更新进度 → 合并结果
• 任务函数全部使用 async def,适配云 OCR / HTTP 调用场景 - Postgres
• 存任务状态 & 结果:
• ocr_task 表:任务级别(PDF/图片)
• ocr_page 表:按页存储识别结果
• 提供数据持久化,保证重启后不会丢结果 - 对象存储 / 本地文件系统
• 存原始 PDF/图片 + 拆页后的中间图片(如果有)
- FastAPI 服务(api-service)
⸻
- 任务处理流程(以 PDF 为例)
- 提交任务(FastAPI)
• 用户上传 PDF
• API 做的事情: - 保存文件到存储,得到 file_path 或 file_key
- 在 ocr_task 表插一条记录:
• task_id
• file_path
• status = PENDING
• progress = 0 - 通过 ARQ 入队:
- 提交任务(FastAPI)
bash
job = await redis_pool.enqueue_job(
"ocr_document",
task_id,
retry_count=0,
)
4. 返回 task_id 给前端
2. Worker 侧:ocr_document 任务逻辑
bash
async def ocr_document(ctx, task_id: str, retry_count: int = 0):
db = ctx["db"] # 启动时注入
try:
# 1. 更新任务状态为 RUNNING
await db.update_task_status(task_id, "RUNNING")
# 2. 根据 task_id 查出 file_path,判断是 PDF 还是图片
task = await db.get_task(task_id)
file_path = task.file_path
if task.file_type == "pdf":
# 2.1 拆 PDF 为多页图片
page_paths = await split_pdf_to_images(file_path)
else:
page_paths = [file_path]
total = len(page_paths)
results = []
# 3. 控制并发调用 OCR(云 OCR / 本地 OCR 服务)
sem = asyncio.Semaphore(5) # 限制同时请求数
async def ocr_one(i, page_path):
async with sem:
text, extra = await call_ocr_api(page_path)
await db.save_page_result(task_id, i, text, extra)
更新进度
await db.update_task_progress(task_id, int((i+1) / total * 100))
await asyncio.gather(*[
ocr_one(i, p) for i, p in enumerate(page_paths)
])
# 4. 合并结果/做后处理(可选)
await db.mark_task_success(task_id)
except TemporaryError as e:
自定义的"暂时性错误",比如网络/云服务 5xx
MAX_RETRY = 3
if retry_count < MAX_RETRY:
10 秒后重试,并带上 retry_count+1
from arq import Retry
raise Retry(defer=10, kwargs={
"task_id": task_id,
"retry_count": retry_count + 1
})
else:
await db.mark_task_failed(task_id, reason=str(e))
raise
except Exception as e:
其他不可恢复错误
await db.mark_task_failed(task_id, reason=str(e))
raise
3. 查询任务结果
• GET /ocr/tasks/{task_id} 从 Postgres 读:
• status
• progress
• 如果成功:可以返回文本摘要 / 页数 / 下载链接
⸻
- 宕机 & 重启时的恢复策略
1)Redis 队列里的任务
• 未开始执行的任务都在 Redis 里
• 只要 Redis 没挂(开启 AOF 或持久化),重启 worker 后会继续执行
2)执行中的任务(RUNNING)
• 配置 job_timeout,比如 10 分钟:
bash
class WorkerSettings:
functions = [ocr_document]
redis_settings = RedisSettings(...)
job_timeout = 600
• 如果 worker 崩掉 / kill -9:
• Redis 认为这个 job 处于执行中,但 job_timeout 到期后会判定为失败
• 我们的补偿策略:
• 在 ocr_task 中维护 last_update_time(每处理一页更新一次)
• 启一个"巡检任务"(可以是另一个定时脚本 / 服务):
• 定期扫描 status=RUNNING 且 last_update_time 超过 N 分钟的任务
• 判断为"疑似僵尸任务"
• 再次通过 ARQ enqueue_job("ocr_document", task_id, retry_count=当前+1)
这样就实现了:
• 服务优雅关闭:worker 会把手上的任务跑完再退出
• 服务异常宕机:通过 job_timeout + last_update_time 把"半途挂掉"的任务重新入队
⸻
三、使用 ARQ 做这类业务的优点
-
和 FastAPI 风格统一:全链路 async
• FastAPI 本身是 async 框架
• ARQ 的任务函数也是 async def,调用云 OCR、对象存储、DB 都是 await
• 整个项目是纯 async 风格,思维模型一致,协程调度简单清晰
-
对云 OCR / HTTP IO 场景特别友好
• OCR 如果是走云厂商 API,本地主要是网络 IO + 等待时间
• 使用 ARQ + asyncio.gather 可以轻松做到:
• 一个 worker 同时跑多个 OCR 请求
• 控制并发(Semaphore)避免打爆云服务 QPS
• CPU 不重的情况下:这种 async 并发非常高效
-
架构简单、组件少
• 只需要 Redis(既做队列又存 job 状态)
• 对比 Celery:
• 无需 RabbitMQ / 额外 backend
• Worker 配置简单,一个 WorkerSettings 就够
对于你这种自己掌控部署、还要搞一堆微服务的人来说,少一个组件就少一堆运维心智负担。
-
重试机制可按业务精细控制
• 用 Retry(defer=秒数, kwargs=...) 明确告诉 ARQ"过多久再重试"
• 很适合 OCR 里这种"云接口暂时 500/超时,再试几次"的场景
• 你可以在任务中设计:
• 最大重试次数
• 重试间隔(固定/递增)
• 哪些异常重试,哪些异常直接失败
• 完全业务驱动,不被框架的黑魔法限制
-
适合"调度+编排",而不是"重推理"
• 你本来就打算把 PaddleOCR / 大模型等重推理部分单独做服务:
• ARQ 负责:排队 → 调 OCR 服务 → 存结果 → 更新进度
• OCR 服务只负责推理
• 在这个定位下,ARQ 非常合适当"业务编排层"的队列框架
⸻
四、使用 ARQ 的不足 / 风险点
- 仅支持 Redis,扩展性受限
• ARQ 目前只支持 Redis 作为队列和结果存储
• 如果你将来希望:
• 使用 RabbitMQ / Kafka / SQS 等更"重量级"的消息系统
• 或者需要更强的持久化语义 / 消息重放
• 那 ARQ 就不适合,需要换框架(例如 Celery 或自己对接 Kafka)
对你目前来说,Redis 足够,但这是个中长期的约束。
- 没有内置类似 Celery beat 的定时调度器
• ARQ 没有像 Celery beat 那样的"任务调度器"
• 如果你要:
• 定期扫描僵尸任务
• 定时批量做 OCR 任务
• 需要:
• 用 crontab / APScheduler / 一个小的 FastAPI 定时服务来自行实现
不是不能做,就是需要你自己写一点调度逻辑。
- 重试策略需要自己封装"标准化"
• ARQ 只提供一个 Retry 异常
• "最大重试次数、退避策略、统一日志记录"都需要你自己封装一个小工具层
• 对你来讲不难,但团队协作时要保证所有任务遵循同一套规范
Celery 这块有比较完整的官方支持(max_retries, countdown, retry_backoff 等)。
-
可视化监控和生态偏弱
• Celery 有 Flower,还有无数经验博客
• ARQ 的生态比较"极客",可视化监控需要你自己接:
• Prometheus + Grafana
• 自写管理接口(比如列出任务状态、处理速度等)
• 对你这种本来就要搭日志/监控体系的人来说问题不大,但不如 Celery 开箱即用。
-
对 CPU/GPU 密集任务不是最优形态
• ARQ 是 async 单进程事件循环模型,要充分利用多核/多 GPU,需要:
• 启动多个 worker 进程 / 容器
• 或把重 CPU 逻辑放到其他服务(推荐)
• Celery 的多进程 worker 模型在直接跑本地推理时更自然一些
对你的场景:推荐把重推理独立服务化,ARQ 做调度,这个缺点就不算大问题。
- Redis 任务持久化要自己注意配置
• 如果 Redis 配置不好(比如纯内存、没有 AOF/RDB),崩溃时队列里的任务会丢
• ARQ 自己不管这些,需要你在 Redis 层:
• 开启 RDB/AOF
• 做主从/哨兵(高可用)
不过这点不管 Celery/ARQ 都一样:broker 崩了都得你自己兜底。
⸻
五、结合你当前业务的建议结论
如果我们只看你现在这条线:
• PDF / 图片 OCR
• 很多调用云 OCR、未来还要调智能编目、质检等服务
• 有 FastAPI、Redis、Postgres 的基础
• 你能接受自己封装一层"任务重试 + 状态管理 + 监控"
那么:
✅ 用 ARQ 做"异步任务 & 编排层"是可行且好用的选择,尤其是对于 IO 型任务(云 OCR)很合适。
⚠️ 但前提是:
• 真正重推理(PaddleOCR / 大模型)放到独立推理服务
• ARQ + Redis 只存 ID & 状态,结果进 DB
• 你愿意自己写一点:重试封装、僵尸任务恢复、监控接口。
如果你后面打算把这一套做成"全公司统一任务中台",还要承载各种类型的任务(视频转码、大模型推理等等),那可以:
• 当前 OCR 项目用 ARQ(轻便、开发快)
• 并并行规划一套 更通用的 Celery 任务平台 作为长远演进方向(甚至可以共存一段时间)