核心架构变化:手写异步队列改为成熟任务队列库;arq 简介
核心架构变化:手写异步队列改为成熟任务队列库
删除了自建的
TaskDispatcherService(asyncio.Queue + worker 协程 + 手写状态机),迁移到 arq(基于 Redis 的成熟任务队列库)。
为什么做这个决定
- 自建 dispatcher 要手写取消功能:区分"排队中"和"正在执行"两种状态,
前者要维护_canceled_pending集合,后者要维护session_id -> asyncio.Task
映射并调用task.cancel(),代码量和心智负担都不小。 - arq 原生支持
job.abort(),取消逻辑不用自己写。 - arq 是 pydantic/FastAPI 同一作者写的,API 是 async 原生风格,跟现有代码风格契合。
⚠️ 阻断项:InMemorySaver 在多进程/多次初始化下会失效
问题本质
@lru_cache(maxsize=1)
def build_adaptive_rag_graph():
...
return g.compile(checkpointer=InMemorySaver())
InMemorySaver 的状态只存在于当前进程的内存里。之前 dispatcher 和 FastAPI
在同一个进程,lru_cache 保证全进程共享同一份 checkpoint 数据。
如果 arq worker 是独立进程跑(arq app.worker.WorkerSettings),worker 进程和
API 进程是两个独立的 Python 解释器,各自的 InMemorySaver 互不相通——worker 写入的
状态,API 那边的 /result 永远查不到。
结论
必须换成跨进程共享的 checkpointer(如 AsyncPostgresSaver 或 AsyncRedisSaver),
这一步还没有完成,是上线前必须解决的阻断项,不是可以后延的优化。
部署模式:arq worker 内嵌到 FastAPI 同进程
选择了在 FastAPI 的 lifespan 里直接用 arq.worker.Worker 构造并 async_run(),
而不是让 arq worker 作为独立进程/Supervisor 管理的服务运行。
取舍
- 好处:省一个部署进程,本地开发和小规模单机部署更省心。
- 代价:失去了"横向扩展 worker 数量"和"API 崩了 worker 还能继续跑"这两个独立
进程模式自带的优势。以后如果要用uvicorn --workers N起多进程,每个进程都会
各自起一个内嵌 worker,max_jobs会被放大 N 倍,需要注意别重复扩大并发。
arq 简介
arq是一个基于 Redis 的异步任务队列库,专为 Python 的 asyncio 设计
特点
- 任务函数就是普通的
async def - 只依赖 Redis,不需要 RabbitMQ 等额外中间件
- 内置支持:任务超时、取消、重试、定时任务(cron)、结果过期
基本用法
# 定义任务
async def my_task(ctx: dict, x: int) -> int:
return x * 2
# worker 配置
from arq.connections import RedisSettings
class WorkerSettings:
functions = [my_task]
redis_settings = RedisSettings(host="localhost", port=6379)
# 启动
# 命令行:arq app.worker.WorkerSettings
# 入队任务
job = await redis.enqueue_job("my_task", 5)
# 查状态
from arq.jobs import Job
status = await Job(job.job_id, redis).status()
# 取消
await Job(job.job_id, redis).abort()
和 Celery 比
- Celery:生态更大更成熟,但异步支持是后加的,不如 arq 原生
- arq:更轻量,专门给纯 asyncio 项目用,够用就好
浙公网安备 33010602011771号