文章目录
从“能运行”到“能生产”还差什么?
开发环境里,一条命令启动 Redis,一条命令启动 Worker,任务成功返回,似乎已经完成。
生产环境还必须回答:
- 周期任务如何避免重复调度?
- 视频转码为什么不能和通知任务共用同一队列?
- Worker 并发数应该设置多少?
- 队列积压时如何发现?
- Worker 发布重启时,在途任务怎么办?
- 任务参数是否泄漏敏感数据?
- Broker 或 Backend 故障后如何恢复?
- 如何定位任务慢在排队还是执行?
Celery 是分布式系统的一部分,不是一个装饰器库。生产化的重点是资源隔离、可靠性、安全和可观测性。
使用 Celery Beat 执行周期任务
Celery Beat 是调度器。它按计划创建任务消息并发送到 Broker,真正执行任务的仍然是 Worker。
配置固定间隔:
app.conf.beat_schedule = {
"build-health-report-every-5-minutes": {
"task": "tasks.build_health_report",
"schedule": 300.0,
},
}
配置 Crontab:
from celery.schedules import crontab
app.conf.beat_schedule = {
"cleanup-every-night": {
"task": "tasks.cleanup_expired_data",
"schedule": crontab(hour=2, minute=0),
"options": {
"queue": "maintenance",
"expires": 3600,
},
},
}
启动 Worker:
celery -A celery_app worker --loglevel=INFO
另开进程启动 Beat:
celery -A celery_app beat --loglevel=INFO
开发环境可以使用 worker -B 合并启动,但生产环境更适合分开管理和扩缩容。
为什么同一套计划通常只能运行一个 Beat?
如果两个 Beat 同时加载相同时间表,它们可能在同一时刻各发送一次任务,于是周期任务重复执行。
解决思路:
- 确保只有一个 Beat 实例;
- 使用支持锁或高可用选主的调度方案;
- 即使调度层防重,任务本身仍保持幂等;
- 对必须单实例执行的任务增加分布式锁或业务状态约束。
还要考虑任务重叠:每五分钟调度一次,但任务需要十分钟,下一次触发时上一次还没结束。
可以使用基于业务键的锁:
lock_key = "periodic:daily-settlement:2026-08-19"
锁需要设置合理过期时间,并处理 Worker 崩溃、锁续期和误释放。很多场景下,数据库唯一约束比单纯 Redis 锁更容易形成可审计结果。
为什么需要多个队列?
假设同一队列里同时存在:
- 50 毫秒的通知任务;
- 5 秒的第三方 API 调用;
- 30 分钟的视频转码;
- 高内存的报表任务。
长任务占满 Worker 后,用户通知会长时间排队;高内存任务还可能导致执行其他任务的子进程一起受到资源压力。
按工作负载分队列:
app.conf.task_routes = {
"tasks.send_email": {"queue": "io_fast"},
"tasks.call_partner_api": {"queue": "io_external"},
"tasks.transcode_video": {"queue": "cpu_heavy"},
"tasks.build_report": {"queue": "memory_heavy"},
}
分别启动 Worker:
celery -A celery_app worker \
-Q io_fast \
--concurrency=20 \
--loglevel=INFO
celery -A celery_app worker \
-Q cpu_heavy \
--concurrency=4 \
--loglevel=INFO
资源隔离的收益:
- 长任务不再阻塞短任务;
- 不同队列可以独立扩缩容;
- 并发模型和资源限制可以分别配置;
- 单一业务故障不容易拖垮所有后台任务;
- 队列积压更容易定位到具体工作负载。
Worker 并发池怎么选择?
Prefork
默认且最常用的多进程模型。适合普通 Python 任务和 CPU 密集型工作,进程隔离也更明确。
代价是每个子进程都有内存开销,创建大量进程会增加数据库连接和系统资源消耗。
Eventlet / Gevent
适合大量 I/O 等待且依赖库能够配合协作式并发的任务。需要 monkey patch,并非所有库都兼容。
不要仅因为“并发数可以设置很大”就使用。下游服务、数据库连接池和限流策略仍然决定真实容量。
Solo
在主进程单线程执行,适合调试或特殊环境,没有并行能力。
Threads
线程池可用于部分 I/O 场景,但受 Python 库线程安全性和 GIL 等因素影响,需要基准测试。
并发数不是越大越好
并发数受到多个瓶颈约束:
Worker 并发
≤ CPU / 内存能力
≤ 数据库连接池
≤ Redis / RabbitMQ 容量
≤ 第三方 API 限流
≤ 下游服务可承受并发
如果数据库只允许二十个连接,却启动一百个同时访问数据库的任务,结果可能是更多超时和重试,而不是更高吞吐。
正确方法:
- 测量单任务 CPU、内存、I/O 和执行时间;
- 确定下游容量;
- 从保守并发开始压测;
- 观察吞吐、错误率和尾延迟;
- 按队列分别调整。
理解 Prefetch
Worker 可以提前从 Broker 预取任务。预取能提高吞吐,但也可能造成任务分配不均:某个 Worker 预取了大量长任务,其他 Worker 却没有工作。
常见配置:
app.conf.worker_prefetch_multiplier = 1
较低预取通常更适合长任务和公平分配;短小、稳定的任务可能从更高预取获得吞吐收益。
worker_prefetch_multiplier=1 不是万能最佳值。应按队列特征压测。
管理 Worker 子进程生命周期
第三方库可能缓慢泄漏内存。Celery 可以在子进程处理一定任务数或达到内存阈值后替换它:
app.conf.update(
worker_max_tasks_per_child=1000,
worker_max_memory_per_child=512_000,
)
含义:
- 子进程最多执行一千个任务后重启;
- 子进程内存超过约 512 MB 后被替换。
这些配置只能缓解问题,不能代替定位内存泄漏。频繁重启也会带来初始化开销。
优雅停止与滚动发布
Worker 收到 TERM 时会进行温和关闭,停止接收新工作并等待当前任务完成。QUIT 更接近冷关闭,SIGKILL 则不给进程清理机会。
生产发布应:
- 使用 systemd、Supervisor、Kubernetes 等管理进程;
- 配置足够长的终止宽限时间;
- 停止前让负载均衡或队列逐步摘除 Worker;
- 观察在途任务和队列积压;
- 对长任务使用幂等和晚确认时,验证重投行为;
- 避免所有 Worker 同时退出。
如果 Kubernetes 的 terminationGracePeriodSeconds 小于任务正常耗时,所谓优雅停止实际仍会变成强制终止。
使用命令行观察 Celery
celery -A celery_app status
celery -A celery_app inspect registered
celery -A celery_app inspect active
celery -A celery_app inspect reserved
celery -A celery_app inspect scheduled
celery -A celery_app inspect stats
含义:
registered:Worker 注册了哪些任务;active:正在执行;reserved:已被 Worker 预取但尚未执行;scheduled:Worker 内部等待 ETA 的任务;stats:进程池、Broker 和运行统计。
这些命令依赖 Broker 对远程控制的支持。SQS 等 Broker 的能力与 RabbitMQ、Redis 不同。
使用 Flower 进行 Web 监控
Celery 官方监控指南推荐 Flower 作为实时 Web 监控工具。
安装并启动:
pip install flower
celery -A celery_app flower --port=5555
访问:
http://localhost:5555
Flower 可以显示:
- Worker 在线状态;
- 任务历史、参数、状态和运行时间;
- 活跃、保留、计划和撤销任务;
- Worker 池大小和队列;
- 部分远程控制功能;
- Prometheus 指标集成。
不要把 Flower 无认证地暴露到公网。它可能显示敏感任务参数,并具备管理 Worker 和撤销任务的能力。
真正应该监控哪些指标?
队列指标
- 队列长度;
- 最老消息等待时间;
- 入队和出队速率;
- 未确认消息数量;
- 各队列消费者数量。
只看队列长度不够。如果任务进入和处理速度都很高,队列长度可能稳定;最老消息年龄更能说明用户等待多久。
任务指标
- 成功率、失败率和重试率;
- P50、P95、P99 排队时间;
- P50、P95、P99 执行时间;
- 超时和撤销数量;
- 按任务类型统计的异常;
- 最终失败和人工补偿数量。
Worker 指标
- 在线 Worker 和心跳;
- CPU、内存、负载和文件句柄;
- 子进程异常退出和重启;
- 当前并发使用率;
- Broker 重连次数。
依赖指标
- Broker 连接、内存和磁盘;
- Result Backend 延迟与容量;
- 数据库连接池;
- 第三方 API 延迟、限流和错误率;
- 对象存储吞吐。
Worker 在线不代表系统健康。Worker 全部在线,但队列最老消息已经等待一小时,业务仍然不可用。
生产环境安全配置
Broker 和 Backend
- 使用独立账号和最小权限;
- 限制网络访问范围;
- 开启 TLS;
- 定期轮换凭据;
- 不与不可信应用共享同一队列或 Redis 数据库;
- 按重要性设计持久化、备份和高可用。
序列化
默认优先 JSON:
app.conf.update(
task_serializer="json",
result_serializer="json",
accept_content=["json"],
)
pickle 能表达更多 Python 类型,但反序列化不可信 Pickle 数据可能执行任意代码。除非整个生产者、Broker 和 Worker 的信任边界都经过严格控制,否则不要启用。
敏感参数
任务参数可能出现在:
- Broker 消息;
- Worker 日志;
- Flower;
- 监控事件;
- Result Backend;
- 异常追踪系统。
不要直接传密码、完整银行卡号和访问令牌。传安全存储中的引用,由 Worker 在执行时按权限读取。
使用 argsrepr 或 kwargsrepr 可以隐藏日志展示,但不会加密 Broker 中的原始消息。
生产配置示例
import os
from celery import Celery
app = Celery(
"production_app",
broker=os.environ["CELERY_BROKER_URL"],
backend=os.environ["CELERY_RESULT_BACKEND"],
)
app.conf.update(
task_serializer="json",
accept_content=["json"],
result_serializer="json",
enable_utc=True,
timezone="Asia/Singapore",
result_expires=3600,
broker_connection_retry_on_startup=True,
worker_prefetch_multiplier=1,
worker_max_tasks_per_child=1000,
task_soft_time_limit=300,
task_time_limit=330,
task_routes={
"myapp.tasks.send_email": {"queue": "io_fast"},
"myapp.tasks.build_report": {"queue": "reports"},
},
)
这只是起点,不是所有系统通用的最佳配置。特别是预取、时间限制、进程回收和结果过期时间必须根据任务特征测试。
常见生产故障
队列持续积压
分析顺序:
- 入队速率是否突然增加;
- Worker 数量或并发是否下降;
- 单任务执行时间是否变长;
- 下游数据库或 API 是否变慢;
- 重试是否造成消息放大;
- 某类长任务是否占满共享队列;
- 预取是否造成分配不均。
不要第一反应只扩容 Worker。下游已经饱和时,扩容会让故障更严重。
任务重复执行
检查:
- 是否使用
acks_late; - Worker 是否在执行中失联;
- Redis visibility timeout 是否短于任务耗时;
- 是否启动了多个 Beat;
- 生产者是否因 HTTP 重试重复发送;
- 任务是否缺少业务幂等键。
Worker 内存不断增长
检查:
- 任务是否加载超大数据;
- 库或全局缓存是否泄漏;
- 返回值是否过大;
- Prefork 子进程是否长期不回收;
- 是否可以流式处理或分块;
worker_max_tasks_per_child能否临时缓解。
任务永远卡住
优先寻找:
- 没有超时的网络请求;
- 数据库锁等待;
- 子进程或外部命令未设置超时;
- 无限循环;
- 在任务里调用其他任务的
.get()。
如何测试生产行为?
单元测试
把业务逻辑和 Celery 外壳分开:
def calculate_invoice(order_id: int) -> dict:
...
@app.task(autoretry_for=(TemporaryError,), retry_backoff=True)
def calculate_invoice_task(order_id: int) -> dict:
return calculate_invoice(order_id)
普通函数可以快速、稳定地单元测试。
集成测试
启动真实测试 Broker 和 Worker,验证:
- 任务注册;
- JSON 序列化;
- 路由;
- 重试;
- 结果存储;
- Chain、Group 和 Chord;
- Worker 退出后的行为。
task_always_eager=True 在当前进程同步执行,不能覆盖 Broker、Worker、并发和消息确认,因此不能代替集成测试。
故障演练
主动测试:
- Broker 短暂断开;
- Worker 执行中被终止;
- 下游服务超时和限流;
- Result Backend 不可用;
- 队列突然积压;
- Beat 重启;
- 同一任务重复投递。
只有在故障中验证过的恢复方案,才接近可信。
从 GitHub 仓库理解 Celery
Celery 的官方主仓库是 celery/celery。阅读源码时,不建议从 Worker 启动流程一路硬追到底,而应围绕已经理解的概念分层阅读。
应用与配置:celery/app/base.py
celery/app/base.py包含核心 Celery 应用对象。重点搜索:
class Celery;send_task;- 配置加载;
- Backend 与连接创建;
- 任务注册和自动发现。
它回答“Celery 应用怎样把配置、任务和通信能力组织在一起”。
任务对象:celery/app/task.py
celery/app/task.py是理解使用层行为的关键。重点搜索:
class Task;delay;apply_async;retry;__call__;- 生命周期钩子。
这里可以看清:
task(1, 2)
task.delay(1, 2)
为什么走的是完全不同的路径。
结果抽象:celery/result.py
celery/result.py包含 AsyncResult、GroupResult 等。
一个重要认知是:AsyncResult 自身不是存放最终结果的容器,它是根据任务 ID 查询 Result Backend 的抽象。
工作流:celery/canvas.py
celery/canvas.py实现 Signature、Chain、Group、Chord 等 Canvas 原语。
先读官方 Canvas 文档,再结合源码查找同名类和方法,会比直接读整份文件更高效。
Worker:celery/worker/
celery/worker涵盖 Worker、Consumer、并发池交互和启动组件。
建议带着问题阅读:
- Worker 如何连接 Broker?
- Consumer 如何接收消息?
- 收到消息后怎样转成任务请求?
- 请求如何交给进程池?
- 成功、失败、重试和确认分别在哪里发生?
Result Backend:celery/backends/
celery/backends包含 Redis、数据库等结果后端实现和共同抽象。
当遇到 Chord、结果过期或 Backend 连接问题时,这一目录很有价值。
消息传输为什么经常出现 Kombu?
Celery 通过 Kombu抽象 RabbitMQ、Redis、SQS 等消息传输。
因此报错栈中常出现:
kombu.connection
kombu.transport.redis
kombu.messaging
Celery 负责任务语义与执行,Kombu 负责更底层的消息连接、Producer、Consumer 和 Transport 抽象。
推荐源码阅读顺序
- 阅读主仓库 README,明确项目边界和支持环境;
- 在
app/task.py中跟踪delay → apply_async; - 在
app/base.py中阅读send_task; - 在
result.py中理解AsyncResult; - 在
canvas.py中对应 Signature、Chain、Group、Chord; - 在
worker/consumer/中跟踪消息消费; - 阅读具体 Backend;
- 最后进入 Kombu 查看所用 Broker 的 Transport。
阅读方法:
- 使用
rg "def apply_async" celery/搜索入口; - 用调试器或日志验证调用链;
- 固定 Celery 版本,不要拿 main 分支源码解释旧版本生产行为;
- 先回答一个具体问题,再扩展阅读范围;
- 结合单元测试理解边界情况。
生产上线检查清单
架构
- Broker、Backend 的角色和容量明确;
- 长短任务、CPU 与 I/O 任务已经分队列;
- 各队列有独立扩缩容策略;
- Beat 单实例或具备可靠选主;
- 关键任务具有业务幂等方案。
可靠性
- 只重试可恢复异常;
- 配置指数退避、jitter 和最大次数;
- 所有网络 I/O 有超时;
- 明确提前确认或晚确认;
- 数据库事务与消息发送不存在明显竞态;
- 永久失败可告警、查询和补偿。
安全
- Broker 和 Backend 使用最小权限;
- 凭据由密钥系统或环境变量提供;
- 网络访问受到限制并启用 TLS;
- 只接受可信序列化格式;
- 任务参数不包含敏感明文;
- Flower 有认证且不直接暴露公网。
运维
- Worker 支持优雅停止;
- 发布宽限时间覆盖合理任务时长;
- 监控队列长度和最老消息年龄;
- 监控成功率、重试率和尾延迟;
- 监控 Worker 心跳、CPU 和内存;
- Broker、Backend 和下游依赖都有告警;
- 已完成 Worker、Broker 和下游故障演练。
系列总结
学会 Celery 可以分为五个层次:
- 理解角色:生产者、Broker、Worker、Backend 和 Beat;
- 跑通链路:定义任务、启动 Worker、发送消息、读取结果;
- 保证正确:重试、超时、确认、重复执行和幂等;
- 表达流程:用 Canvas 组合顺序、并行和汇总任务;
- 长期运行:队列隔离、容量、监控、安全、发布和故障恢复。
最值得记住的一句话是:
Celery 负责可靠地分发和执行任务,但业务是否正确,最终仍取决于幂等性、事务边界、资源治理和可观测性的设计。

415

被折叠的 条评论
为什么被折叠?



