Celery系列-05-生产实践与源码阅读

从“能运行”到“能生产”还差什么?

开发环境里,一条命令启动 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 限流
≤ 下游服务可承受并发

如果数据库只允许二十个连接,却启动一百个同时访问数据库的任务,结果可能是更多超时和重试,而不是更高吞吐。

正确方法:

  1. 测量单任务 CPU、内存、I/O 和执行时间;
  2. 确定下游容量;
  3. 从保守并发开始压测;
  4. 观察吞吐、错误率和尾延迟;
  5. 按队列分别调整。

理解 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 在执行时按权限读取。

使用 argsreprkwargsrepr 可以隐藏日志展示,但不会加密 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"},
    },
)

这只是起点,不是所有系统通用的最佳配置。特别是预取、时间限制、进程回收和结果过期时间必须根据任务特征测试。

常见生产故障

队列持续积压

分析顺序:

  1. 入队速率是否突然增加;
  2. Worker 数量或并发是否下降;
  3. 单任务执行时间是否变长;
  4. 下游数据库或 API 是否变慢;
  5. 重试是否造成消息放大;
  6. 某类长任务是否占满共享队列;
  7. 预取是否造成分配不均。

不要第一反应只扩容 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包含 AsyncResultGroupResult 等。

一个重要认知是: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 抽象。

推荐源码阅读顺序

  1. 阅读主仓库 README,明确项目边界和支持环境;
  2. app/task.py 中跟踪 delay → apply_async
  3. app/base.py 中阅读 send_task
  4. result.py 中理解 AsyncResult
  5. canvas.py 中对应 Signature、Chain、Group、Chord;
  6. worker/consumer/ 中跟踪消息消费;
  7. 阅读具体 Backend;
  8. 最后进入 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 可以分为五个层次:

  1. 理解角色:生产者、Broker、Worker、Backend 和 Beat;
  2. 跑通链路:定义任务、启动 Worker、发送消息、读取结果;
  3. 保证正确:重试、超时、确认、重复执行和幂等;
  4. 表达流程:用 Canvas 组合顺序、并行和汇总任务;
  5. 长期运行:队列隔离、容量、监控、安全、发布和故障恢复。

最值得记住的一句话是:

Celery 负责可靠地分发和执行任务,但业务是否正确,最终仍取决于幂等性、事务边界、资源治理和可观测性的设计。

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值