用户点击“导出报表”,服务需要查询数据、生成文件、上传存储,整个过程可能持续几十秒。让 HTTP 请求一直等着,不仅用户体验差,还会占用处理请求的资源。如果把这件事交给后台,接口很快就能返回;但新的问题随之而来:任务交给谁?服务重启后怎么办?失败了谁重试?用户到哪里看结果?

Celery 就是围绕这些问题组织后台工作的任务队列框架。理解它的重点,不是记住几个装饰器,而是弄清楚:任务怎样从“有人提出要做”,变成“有人真正做完”,中间有哪些边界需要自己负责。

为什么需要任务队列

在报表导出的例子中,同步处理意味着:只有查询、生成和上传全部结束,接口才能返回。后台处理则把流程拆成两段:

  • 请求阶段:检查权限和参数,创建一条报表记录,提交任务,返回一个可查询的标识。
  • 执行阶段:后台生成文件,记录成功或失败,用户稍后查询状态或接收通知。

这不等于让一份报表凭空生成得更快,而是把“用户等待接口”和“系统完成工作”分开。任务队列还允许执行端独立扩容,让多台机器共同处理任务。Celery 官方把它定位为通过消息分配工作、由 Worker 执行的分布式任务队列。1

如果只是需要一个很短的、丢了也无所谓的进程内回调,未必值得引入一整套队列系统。真正需要考虑 Celery 的信号,是后台工作开始要求独立部署、排队、重试、并发控制或运行状态管理。

架构:分清五个角色

先看一张全景图。可以点击节点查看职责,也可以选中图里的文字。

Celery 任务流
下载
主链路是 Producer → Broker → Worker。Beat 和 Result backend 按需加入;图中 Worker 使用 prefork 进程池。

Producer:提出工作请求

Producer 是提交任务的一方,通常是 Web 应用,也可以是命令行程序或另一个任务。它说明“要执行哪个任务,参数是什么”,再把消息发给 Broker。

这里有个容易误解的地方:通常发出去的是任务名、参数、任务 ID 等数据,不是把 Python 函数源码传过去。 Worker 必须事先部署并注册对应任务。提交端和执行端可以分开运行,但双方要对任务名和参数含义保持一致。2

Broker:保存和分发待处理消息

Broker 是消息中介,例如 RabbitMQ 或 Redis。它负责接收消息,把消息交给消费相应队列的 Worker,自己不执行报表生成代码。

Broker 是服务,队列是其中组织消息的通道。 一个系统可以有多个队列,例如通知队列、报表队列和图片处理队列;Worker 可以只消费其中一部分。这样可以把不同类型的工作分开。3

Worker:真正执行任务

Worker 是持续运行的执行服务:接收任务消息,找到本地注册的任务,将工作交给内部执行池,再处理执行结果。

图里画的是默认的 prefork 模型:一个 Worker 节点管理多个子进程,多个子进程可以同时执行不同任务。Celery 也提供线程池、单进程和基于协程的执行模型,选择时要考虑任务是计算密集型还是 I/O 密集型,以及不同执行池支持的功能。4

因此,“Celery 有多少个 Worker”和“同时能做多少件事”不是同一个问题:前者通常指节点数,后者还取决于每个节点的并发数和执行模型。

Result backend:保存可查询的状态和结果

Result backend 回答的是“任务现在怎样了,返回了什么”,而不是“下一个任务交给谁”。Redis、数据库等都可以承担这个角色。它和 Broker 是两个不同职责;即使使用同一个 Redis 服务,也不能把两者当成同一件事。5

结果后端默认并不启用。如果任务只需要更新业务数据库,应用也通过业务记录展示状态,未必需要保存 Celery 返回值;但需要通过 Celery 查询状态、获取结果,或者使用某些聚合工作流时,就需要配置合适的结果后端。67

对报表系统,我更倾向于把“导出申请、文件地址、有效期”放在业务数据库中,把文件本身放在文件存储里。Celery 的执行结果用于辅助追踪,不代替产品自己的数据模型。

Beat:按时提交任务,不负责执行

Beat 是定时调度器,例如每天凌晨提交汇总任务。它的工作到“派发任务”为止,真正运行代码的仍是 Worker。没有定时需求时,可以不运行 Beat。8

同一套调度计划应只有一个活动调度器,否则可能重复派发任务。另外,Beat 不会因为上一次任务还没完成就自动阻止下一次派发;有互斥要求的任务,需要在业务层加入锁或其他并发约束。8

一条报表任务怎样走完整个系统

把组件放回具体业务中,一次导出大致经历以下过程:

  1. 用户发起导出。 Web 应用校验请求,保存报表记录。
  2. 应用提交任务。 消息里携带报表 ID、任务名和执行所需的其他参数。
  3. 接口返回。 用户拿到查询标识,页面显示“处理中”,不等待文件生成。
  4. Worker 领取并执行。 它根据报表 ID 读取数据,生成文件,上传到共享存储。
  5. 记录结果。 更新业务记录;如果启用了结果后端,也记录相应的 Celery 状态或返回值。
  6. 用户获取产物。 通过业务接口查看状态,再下载文件;也可以由系统主动通知。

这里的流程是一个设计示例,不是 Celery 自动替应用完成的业务流程。Celery 负责提交、分发和执行任务,权限校验、报表记录、下载接口以及通知方式仍由应用实现。

“后台任务”不等于 Python 的 async

async def 主要描述当前进程怎样在等待 I/O 时切换工作。Celery 的异步调用则意味着:执行被交给另一组进程,甚至另一台机器。 对于一次普通的任务提交,调用者不需要等任务函数执行结束,但发送消息本身仍需要连接 Broker,连接失败也可能让提交失败。9

任务函数通常仍可以是普通的 def。从架构上看,改变的是执行地点和完成时间,而不是必须把所有函数改成协程。

参数应该是可传输的数据

不同进程无法共享 Web 请求里的数据库会话、打开的文件句柄或临时对象。更清晰的边界是传业务 ID、查询条件、对象存储地址等可序列化数据,再由 Worker 获取它需要的资源。2

这也意味着:一个上传文件如果只存在于 Web 容器的本地临时目录,另一台机器上的 Worker 未必能读到。异步化之前,要先决定输入和产物怎样共享,而不只是给函数加装饰器。

并发、扩容与排队:速度由什么决定

假设有九个彼此独立、耗时相同的任务。在理想模型中,一个执行槽位需要九轮,三个槽位需要三轮。下面可以调整任务数和槽位数,观察排队、执行和完成之间的变化。

任务队列演示
下载
演示的是并发执行槽位,不是独立 Worker 节点数量。每个任务耗时相同,未模拟预取、网络、失败和共享资源竞争。

执行槽位是这里用来理解并发的概念:在 prefork 模型中,通常对应一个可执行任务的子进程。扩容可以增加节点,也可以调整节点内部的并发数。4

但“并发翻倍,速度也翻倍”只适用于足够理想的情形。真实的报表任务可能争用同一数据库;外部接口可能有配额;机器的 CPU、内存和连接数也都有上限。增加 Worker 不会替这些下游系统扩容。

任务队列还能吸收短时间的流量峰值,但不能消除持续的产能缺口。如果任务进入的速度长期高于完成的速度,积压只会继续增长。判断系统是否健康,要看排队时间和处理能力,而不只是看到“任务成功入队”。10

为什么长任务和短任务应该分开

发送通知可能只花几百毫秒,导出报表可能花几分钟。如果共用一组繁忙的执行槽位,通知就可能被报表拖慢。更容易控制的做法是使用不同队列,让不同 Worker 组消费它们,分别安排资源。3

还要理解 prefetch(预取):Worker 可以提前领取一批消息,等待内部执行池空闲。因此,消息离开队列不代表它已经开始执行;提高预取数量也不等于提高执行并发。长短任务混合时,预取策略会影响任务分配的均衡程度。10

可靠性:ACK、重试与幂等是三件事

后台任务跨越应用、Broker、Worker 和业务存储。任何一个环节都可能失败,所以不能把“使用了 Celery”理解为“任务绝不会丢、业务绝不会重复”。

ACK:确认一条消息不再需要投递

ACK 是消息确认。Celery 默认通常在任务开始执行前确认消息,而不是等业务完成后再确认。启用 acks_late 可以把确认推迟到执行结束,让某些未确认消息在故障后有机会重新投递;代价是同一任务可能执行多次。11

不要把它当作万能恢复开关:子进程异常退出等情形还受其他配置影响。消息确认策略决定投递边界,不等于为业务承诺“恰好执行一次”。11

Retry:针对可恢复的失败重新尝试

如果外部服务临时不可用,可以主动调用 retry(),或针对指定异常配置自动重试。普通任务抛出异常并不等于它一定会重新入队,不能只靠延迟 ACK 处理所有业务失败。12

重试策略应回答三个问题:什么错误值得重试,隔多久重试,最多试几次。临时网络故障可能恢复;无效参数不会因为再执行五次就变得有效。适当的退避与抖动,也能避免大量失败任务同时再次冲击外部服务。

幂等:重复执行,不重复产生业务效果

设想 Worker 已上传报表文件,却在记录完成状态或确认消息前退出。任务再次执行时,不能仅因为“这次又收到消息”就无条件产生第二份业务结果。

可以围绕报表申请 ID 设计唯一业务记录、可重复更新的输出路径和有条件的状态转换。对于发通知或调用第三方接口,还要考虑对方是否支持幂等键。具体方案取决于业务,但目标一致:允许执行过程重试,不允许业务效果无限重复。

Celery 官方强调任务应尽量具备幂等性。ACK 和 retry 帮助决定怎样再执行,幂等设计则决定再执行是否安全。1112

数据库提交与发消息不是同一个事务

如果先提交任务,再提交数据库事务,Worker 可能很快启动,却读不到尚未提交的报表记录。Celery 的 Django 集成就专门提供了提交后派发任务的方式来处理这种顺序问题。13

但“提交数据库以后再发消息”仍有一个间隙:数据库提交成功,消息发送却失败。因此,关键业务还需要思考派发失败后的补偿、重发和核对机制,不能把两步操作想象成天然原子的一步。

适合什么场景,又不适合什么场景

下面这些是判断是否采用 Celery 时可以参考的场景,而不是必须使用它的清单。

  • 耗时但不必立即返回结果的工作。 报表导出、文件转换、缩略图生成、批量分析。用户关心的是进度和最终产物,不是同一个 HTTP 请求一直保持连接。
  • 主流程之后可以补做的工作。 通知发送、外部系统同步、索引更新。要先区分“可以异步”的后续动作和必须在主流程里完成的业务约束。
  • 定时触发的后台工作。 周期性汇总、清理和检查,可以由 Beat 提交,再由 Worker 执行。8
  • 可以拆分并行或串行步骤的处理。 Celery 的 Canvas 提供 chain 串联任务、group 并行分发,以及 chord 在一组任务完成后汇总结果的能力。它适合表达这类任务依赖,不意味着应用不再需要自己的业务状态和失败处理。7

这些场景对应官方介绍中的后台执行、周期工作和并行处理等能力。1

相反,如果一个操作必须在返回 HTTP 响应前完成并给出结果,把它移进队列后立刻等待,并没有解决用户等待的问题,还引入了消息和进程间通信。如果只是低频、简单、允许丢失的后台动作,也应先评估更轻量的实现,避免只为了“架构完整”引入 Broker、Worker 和运维负担。

最小使用方式:把三个动作对应起来

下面只用一个求和任务展示“定义任务、启动执行端、提交任务”的关系,不展开项目目录或配置清单。例子假设已有可连接的本地 Redis,运行环境为 Linux 或 macOS;Celery 官方不支持 Windows。12

先安装 Redis 对应的依赖:

pip install 'celery[redis]'

在 tasks.py 中定义任务:

from celery import Celery

app = Celery(
    "tasks",
    broker="redis://localhost:6379/0",
    backend="redis://localhost:6379/1",
)

@app.task
def calculate_total(values):
    return sum(values)

再在这个文件所在目录启动 Worker:

celery -A tasks worker --loglevel=INFO --concurrency=3

另一个 Python 进程提交任务:

from tasks import calculate_total

job = calculate_total.delay([10, 20, 30])
print(job.id)

例子里 Redis 的 /0 和 /1 分别用于消息和结果,只是逻辑上的区分,不是故障隔离。@app.task 注册任务,Worker 执行它,delay() 发送消息并返回一个结果句柄;calculate_total([10, 20, 30]) 则是在当前进程直接调用。需要指定队列或延迟等执行选项时,再使用 apply_async()。69

生产里的报表系统通常返回业务记录 ID,并通过独立接口展示状态。不要为了让接口“拿到结果”,又立即调用 job.get() 阻塞等待,把异步流程变回同步流程。6

上线以后,应该观察什么

最有用的观察顺序是:任务有没有成功提交,是否已经被领取,是否正在执行,结果有没有被记录。

  • 队列积压和等待时间,用来判断工作是否来得比做得快。
  • Worker 的存活状态、活动任务和已预留任务,用来判断执行能力和任务分布。
  • 任务耗时、失败和重试情况,用来区分慢任务、外部故障与业务错误。
  • 业务记录与产物是否一致,用来判断“函数返回成功”是否真的等于用户拿到了结果。

Celery 的事件、inspect 和 Flower 等工具可以观察执行端,但它们不自动替业务验证文件或外部副作用。14

查询状态也要避免误读:PENDING 既可能表示等待执行,也可能只是没有已知结果,例如结果未保存或已过期;STARTED 默认不报告,需要开启相应追踪。不要单凭一个状态值就断言消息还在 Broker 里。611

总结:把“怎么执行”与“业务完成”分开

理解 Celery,可以记住这条主线:

Producer 提出工作 → Broker 组织消息 → Worker 执行代码。

需要查询 Celery 状态时加入 Result backend,需要周期派发时加入 Beat。队列和并发控制决定工作怎样分配;ACK 与重试决定失败后怎样处理;幂等、事务边界和业务状态决定最终结果是否可信。

真正重要的不是让某个函数“在后台跑起来”,而是让后台工作变成一条能观察、能扩容、能解释失败,也能安全再次执行的流程。

参考资料

本文于 2026 年 10 月 13 日核对 Celery 官方稳定文档,内容以其标注的 5.6 系列为依据。报表导出流程和选型建议是基于这些机制给出的设计示例。


  1. Celery 官方:Introduction,任务队列与分布式执行的总体定位。 ↩︎ ↩︎

  2. Celery 官方:Message Protocol,任务消息中的名称、ID 和参数。 ↩︎ ↩︎

  3. Celery 官方:Routing Tasks,队列路由与 Worker 消费范围。 ↩︎ ↩︎

  4. Celery 官方:Concurrency,prefork 及其他执行池。 ↩︎ ↩︎

  5. Celery 官方:Backends and Brokers,消息传输和结果后端的不同职责。 ↩︎

  6. Celery 官方:First Steps with Celery,最小使用方式、结果句柄与状态查询。 ↩︎ ↩︎ ↩︎ ↩︎

  7. Celery 官方:Canvas: Designing Work-flows,串联、并行与汇总任务。 ↩︎ ↩︎

  8. Celery 官方:Periodic Tasks,Beat、单一活动调度器与周期任务重叠。 ↩︎ ↩︎ ↩︎

  9. Celery 官方:Calling Tasks,直接调用、delay、apply_async 与消息发送。 ↩︎ ↩︎

  10. Celery 官方:Optimizing,处理能力、预取与长短任务分离。 ↩︎ ↩︎

  11. Celery 官方:Tasks,消息确认、幂等、重试和任务状态。 ↩︎ ↩︎ ↩︎ ↩︎

  12. Celery 官方:Frequently Asked Questions,retry 与 acks_late 的边界。 ↩︎ ↩︎ ↩︎

  13. Celery 官方:First steps with Django,数据库提交后派发任务。 ↩︎

  14. Celery 官方:Monitoring and Management Guide,事件、Worker 检查和 Flower。 ↩︎