不使用数据库作为 Broker

Broker 的选择大致有消息队列和数据库两种,这里建议尽量避免使用数据库作为 Broker,除非你的业务系统足够简单。在并发量很高的复杂系统中,大量 Workers 访问数据库的行为会使得操作系统磁盘 I/O 一直处于高峰值状态,非常影响系统性能。如果数据库 Broker 同时还兼顾着后端业务的话,那么应用程序也很容易被拖垮。

反观选择消息队列,例如 RabbitMQ,就不存在以上的问题。首先 RabbitMQ 的队列存放到内存中,速度快且不占用磁盘 I/O。再一个就是 RabbitMQ 会主动将任务推送给 Worker,所以 Worker 无需频繁的去轮询队列,避免无谓的资源浪费。

不要过分关注任务结果

Task.delay/Task.apply_async 返回的 AsyncResult 对象用于关联任务的执行结果,前提是启用了 Result Backend。不过任务结果的传递同样需要成本,所以 Celery 默认会将其 Disabled。

  • 全局开启返回任务结果,默认为关闭:
app.conf.task_ignore_result = False
  • 局部关闭返回任务结果:
@app.task(ignore_result=True)
def add(...):

如果你仅希望返回并持久化任务执行失败的异常结果,以便于后续的调查分析,那么你可以在使用数据库作为 Result Backend 的同时应用下列配置:

# Only store task errors in the result backend.
app.conf.task_ignore_result = True
app.conf.task_store_errors_even_if_ignored = True

实现优先级任务

所谓事有轻重缓急,任务如是。例如,用户的验证码短信比较紧急,应及时发送,而宣传短信则可以延后再发,以此提供更好的用户体验。

实现任务优先级最简单的思路就是,首先将任务进行合理分类,一般的我们会将实时任务、高频率任务、短时间任务划分为高优先级任务;而定时任务、低频率任务、长时间任务则为低优先级任务。然后再为处理高优先级任务的队列分配更多的 Worker。

不过这种简单粗暴的方式还存在一个问题,当高优先级任务被消费完后,相应的Workers 就会空闲下来,非常浪费系统资源。那么改善的方法就是,「在高优先级任务队列始终拥有更多 Worker 的前提下,当这些 Worker 空闲时,也可以用于处理低优先级的任务」。利用 Worker 多队列订阅特性即可实现这个效果,例如,现在有 high_queue、low_queue 以及 worker_1、2、3。那么就可以让 worker_1、2、3 均订阅 high_queue 的同时,也让 worker_2、3 订阅 low_queue。

应用 Worker 并发池的动态扩展

Celery Worker 支持下列四种并发方式。

  • celery.concurrency.solo (Single-threaded execution pool)
  • celery.concurrency.prefork (Multiprocessing)
  • celery.concurrency.eventlet
  • celery.concurrency.gevent

通过配置项 worker_pool 指定,默认为 prefork:

# Single-threaded execution pool
app.conf.worker_pool = 'solo'

同时还可以通过配置项 worker_concurrency 来指定并发池的 size,默认为运行环境的 CPU 数量:

app.conf.worker_concurrency = 10

回到正题,当我们选择使用 prefork/gevent 并发方式时,建议应用 Worker 并发池的 autoscale 自动适配功能,在 celery CLI 中使用 --autoscale 选项来指定并发池的上下限。例如:

celery worker -A proj --autoscale=6,3

但需要注意的是,无论是 Worker 的数量还是并发池的数量都并非越多越好,毕竟其自身的存在就需要消耗系统资源。但有一个原则是,当你的任务为 I/O 密集型时,可以适量增大并发池的 size;如果你的任务为 CPU 密集型时,默认 size 不失为一个保险的选择。总而言之,最佳配比需要结合自身实际情况不断的尝试得出。

应用任务预取数

Prefetch 预取数是继承至 RabbitMQ 的原语,即为 Worker 一次从队列中获取的任务消息的数量。任务的执行时间有长有短,我们应该为短时间任务设置更大的任务预取数,以降低获取任务带来的资源消耗。

通过配置项 worker_prefetch_multiplier 来指定全局预取数乘子,默认为 4。当设置为 1 时,表示 disable 预取功能;当设置为 0 时,表示 Worker 会尽可能多的获取任务。

# prefetch_count =  worker_prefetch_multiplier * concurrent_processes_count
app.conf.worker_prefetch_multiplier = 10

如果你的任务既有长任务,又有短任务,那么这里建议你应用分开配置的 Worker 。以文件上传为例,上传小文件(小于 1MB)的数量要远大于上传大文件(大于 20MB)的数量。那么小文件上传任务就属于高频短任务,而大文件上传任务则是低频长任务。分别实现 queue_small/worker_small_1、2 以及 queue_big/worker_big 来处理,同时应该为 worker_small_1、2 设置更大的 Prefetch。

  • 设定不同的 celeryconfig 配置文件
# filename: big_prefetch.py
CELERYD_PREFETCH_MULTIPLIER = 10 # filename: small_prefetch.py
CELERYD_PREFETCH_MULTIPLIER = 100
  • 使用 celery CLI 的 –config 选项分别为 worker 指定不同的 celeryconfig
celery worker -A proj -Q queue_small --config big_prefetch -n worker_small_1
celery worker -A proj -Q queue_small --config small_prefetch -n worker_small_2
celery worker -A proj -Q queue_big --config big_prefetch -n worker_big

保持任务的幂等性

Celery 虽然提供了任务异常重试,但却无法保证任务的事务性,即不提供任务状态的回滚能力。所以为了让任务更易于部署和重试,应该尽量将一个长任务拆解为多个符合幂等性的短任务。

幂等(idempotent)是一个数学概念,常见于抽象代数。幂等性函数的特征为「如果接受到相同的实参,那么无论重复执行多少次,都能得到相同的结果」。例如,get_user_name()set_true() 均属幂等函数。

可见幂等性任务结合任务异常重试,能够非常有效的提高任务执行的健壮性。

应用任务超时限制

避免某些任务一直处于非正常的进行中状态,阻塞队列中的其他任务。应该为任务执行设置超时时间。如果任务超时未完成,则会将 Worker 杀死,并启动新的 Worker 来替代。

  • 全局设置任务超时时间:
app.conf.task_time_limit = 1800
  • 局部设置任务超时时间
@app.task(time_limit=1800)
def add(...):

善用任务工作流

Celery 支持 group/chain/chord/chunks/map/starmap 等多种工作流原语,基本可以覆盖大部分复杂的任务组合需求,善用任务工作流能够更好的应用 Celery 优秀的并发特性。例如,如果下一步任务需要等待上一步任务的执行结果,那么不应该单纯的应用 get 方法来实现同步子任务,而是应该使用 chain 任务链。

合理应用 ack_late 机制

使用 RabbitMQ 充当 Broker,可以应用 RabbitMQ 的 ACK 机制来保证任务有效传递。但在任务执行要求非常严格的场景中,「有效传递」显然是不够的,「有效执行」才可以。

为了支持「有效执行」,Celery 在 ACK 的基础上提供了 ack_late 机制。即只有当任务完成(成功/失败)后,再向 Broker 回传 ACK。而代价就是消息队列的性能会降低,毕竟任务消息占用队列资源的时间变长了。

通常的,对于一些以小时为单位的长时间任务,我会建议实现一次只保留一项任务的 ack late 方式。

app.conf.task_acks_late = True
app.conf.worker_prefetch_multiplier = 1
  • 局部开启 ack_late:
@app.task(ack_late=True)
def add(...):

传递 ORM 对象的唯一标识

有时候任务执行需要对象的参与,此时建议传递对象的唯一标识,而非直接将对象序列化后再传递。例如,不要尝试将数据库的 ORM 对象作为任务消息传递,而是传递 ORM 对象的主键 id。当任务执行到需要使用 ORM 对象时,再通过 id 从数据库实时获取,避免 ORM 对象因为队列阻塞导致与数据库实时记录不一致的情况。

预防内存泄漏

同一个 Worker 在执行了大量任务后,会有几率出现内存泄漏的情况。这里建议全局设置 Worker 最大的任务执行数,Worker 在完成了最大的任务执行数后就主动退出。

app.conf.worker_max_tasks_per_child = 100

合理安排定时任务的调度计划

定时任务的调度计划要经过科学合理的设计,一般的,我们建议遵守以下几点原则:

  1. 与系统管理员和数据库管理员沟通,确保你预期的调度时间不会与他们的定时任务冲突。
  2. 将定时调度任务分散到各个时间点执行,均衡负载。
  3. 要考虑执行定时任务对生产业务系统的影响,尽可能在业务低峰期执行。

启用任务监控

Flower 是 Celery 官方推荐的实时监控工具,用于监控 Tasks 和 Workers 的运行状态。Flower 提供了下列功能:

  • 查看 Task 清单、历史记录、参数、开始时间、执行状态等
  • 撤销、终止任务
  • 查看 Worker 清单、状态
  • 远程开启、关闭、重启 Worker 进程
  • 提供 HTTP API,方便集成到运维系统

相比查看日志,Flower 的 Web 界面会显得更加友好。

Django分布式任务队列celery的实践的更多相关文章

  1. 分布式任务队列 Celery —— Task对象

    转载至 JmilkFan_范桂飓:http://blog.csdn.net/jmilk  目录 目录 前文列表 前言 Task 的实例化 任务的名字 任务的绑定 任务的重试 任务的请求上下文 任务的继 ...

  2. 分布式任务队列 Celery —— 深入 Task

    目录 目录 前文列表 前言 Task 的实例化 任务的名字 任务的绑定 任务的重试 任务的请求上下文 任务的继承 前文列表 分布式任务队列 Celery 分布式任务队列 Celery -- 详解工作流 ...

  3. 分布式任务队列 Celery —— 应用基础

    目录 目录 前文列表 前言 Celery 的周期定时任务 Celery 的同步调用 Celery 结果储存 Celery 的监控 Celery 的调试 前文列表 分布式任务队列 Celery 分布式任 ...

  4. 分布式任务队列 Celery —— 详解工作流

    目录 目录 前文列表 前言 任务签名 signature 偏函数 回调函数 Celery 工作流 group 任务组 chain 任务链 chord 复合任务 chunks 任务块 mapstarma ...

  5. 分布式任务队列 Celery

    目录 目录 前言 简介 Celery 的应用场景 架构组成 Celery 应用基础 前言 分布式任务队列 Celery,Python 开发者必备技能,结合之前的 RabbitMQ 系列,深入梳理一下 ...

  6. [源码解析] 分布式任务队列 Celery 之启动 Consumer

    [源码解析] 分布式任务队列 Celery 之启动 Consumer 目录 [源码解析] 分布式任务队列 Celery 之启动 Consumer 0x00 摘要 0x01 综述 1.1 kombu.c ...

  7. [源码解析] 并行分布式任务队列 Celery 之 Task是什么

    [源码解析] 并行分布式任务队列 Celery 之 Task是什么 目录 [源码解析] 并行分布式任务队列 Celery 之 Task是什么 0x00 摘要 0x01 思考出发点 0x02 示例代码 ...

  8. [源码解析] 并行分布式任务队列 Celery 之 消费动态流程

    [源码解析] 并行分布式任务队列 Celery 之 消费动态流程 目录 [源码解析] 并行分布式任务队列 Celery 之 消费动态流程 0x00 摘要 0x01 来由 0x02 逻辑 in komb ...

  9. [源码解析] 并行分布式任务队列 Celery 之 多进程模型

    [源码解析] 并行分布式任务队列 Celery 之 多进程模型 目录 [源码解析] 并行分布式任务队列 Celery 之 多进程模型 0x00 摘要 0x01 Consumer 组件 Pool boo ...

随机推荐

  1. Netty学习笔记(三) 自定义编码器

    编写一个网络应用程序需要实现某种编解码器,编解码器的作用就是讲原始字节数据与自定义的消息对象进行互转.网络中都是以字节码的数据形式来传输数据的,服务器编码数据后发送到客户端,客户端需要对数据进行解码, ...

  2. [Python][Scrapy 框架] Python3 Scrapy的安装

    1.方法(只介绍 pip 方式安装) PS.不清楚 pip(easy_install) 可以百度或留言. cmd命令: (直接可以 pip,而不用跳转到 pip.exe目录下,是因为把所在目录加入 P ...

  3. c/c++ 多线程 std::call_once

    多线程 std::call_once 转自:https://blog.csdn.net/hengyunabc/article/details/33031465 std::call_once的特点:即使 ...

  4. varnish学习以及CDN的原理

    一.varnish学习Web Page Cache: squid --> varnish 程序的运行具有局部性特征: 时间局部性:一个数据被访问过之后,可能很快会被再次访问到: 空间局部性:一个 ...

  5. python 之 查找某目录中最新的文件

    记录一下这个方法,感觉很有用!>.< import os def find_newest_file(path_file): lists = os.listdir(path_file) li ...

  6. 【Python 01】Python可以做什么

    Python学习未来方向: 1.数据分析 2.自然语言处理 3.社交网络分析 4.人工智能 5.深度学习 6.计算机视觉 7.网络爬虫 8.量化交易

  7. window.loaction.href 不自动跳转的问题

    window.location.href无效/不跳转的原因分析   1.源代码: <a href="javascript:void(0);" onclick="mo ...

  8. Java注解开发与应用案例

    Java注解开发与应用案例 Annotation(注解)是JDK5.0及以后版本引入的,可以对包.类.属性.方法的描述,给被述对象打上标签,被打上标签后的类.属性.方法将被赋予特殊的“功能”:打个比喻 ...

  9. 为什么 npm 要为每个项目单独安装一遍 node_modules?

    nodejs中package.json中的依赖必须每个项目都有自己的node_modules文件夹,而无法在多个项目之间共用一套node_modules(像Java中的Maven那样). 依赖管理是每 ...

  10. 查看jar包的jdk版本