本文对Celery进行了研究,由于其实现相对比较复杂没有足够的时间和精力对各方各面的源码进行分析,因此本文根据Celery的使用方法以及实际行为分析其运行原理,并根据查阅相关代码进行了一定程度的验证。

希望本文能有助于读者理解celery是如何工作的,从而能够更好地使用这个任务框架,而不仅仅是复制官网上的例子来配置。

Celery是Python中任务队列的事实标准。其特点在于:

  • 启动后,本身是一个任务分发进程,会启动若干个worker进程完成任务
  • 需要依赖一个消息队列来负责任务从客户端到Celery进程的派发。这样的好处是,客户端代码只需要向MQ中派发任务请求以及参数,Celery进程就可以从MQ中读取消息并派发给worker,从而达到了客户端程序与Celery进程解耦的效果。而且Celery进程并不需要监听任何端口,减少了配置的复杂性。常用的消息队列实现可以使用RabbitMQ,Redis等等。

下面我们结合Celery的基本使用来分析一下Celery是怎么工作的。本文以Python2为例。

1. 定义Celery配置文件并启动

首先,我们需要定义我们的Celery进程访问哪个Redis进程(假设我们使用Redis作为message backend,在celery的术语中叫做broker)。

Celery提供的方式是创建一个celery instance。我们假设文件目录如下:

lab
- play
- __init__.py
- celery.py
- tasks.py

然后创建lab/play/celery.py文件:

from __future__ import absolute_import, unicode_literals
from celery import Celery app = Celery('play',
broker='redis://127.0.0.1:6379',
include=['play.tasks']) if __name__ == '__main__':
app.start()

由于可能会有多个celery进程访问同一个redis,为了让它们之间隔离开就需要给每个celery实例一个名字,我们这里就叫play

除了name和broker参数以外,还使用了include参数来告诉所有的works到哪里去import tasks的代码,因为workers才是真正执行所有这些任务的单位。

好了,接下来就可以启动celery了。在lab目录下执行:

celery -A play.celery worker -l info

即可启动celery进程。Python的路径和模块系统还是比较复杂的,因此在指定包名的时候要注意。

除了使用celery命令以外,由于我们再celery.py中已经加了if __name__ == '__main__':部分代码,因此也可以在lab下直接执行:

python -m play.celery -A play.celery worker -l info

在启动了celery以后,celery进程监听redis消息,并fork出多个worker进程准备将监听到的消息分发给它们执行。

2. 编写任务并执行

现在执行的部分有了,我们开始定义真正需要执行的部分。

我们可以专门写一个文件来存放任务代码(也可以直接写在celery.py里面):

# lab/play/tasks.py
from __future__ import absolute_import, unicode_literals
import time
from celery import Celery app = Celery('play',
broker='redis://127.0.0.1:6379') @app.task
def say_hi():
print 'hi!'

使用另一个Python进程(也可以使用交互式python或者ipython),在lab下执行:

>>> from play.tasks import say_hi
>>> say_hi.delay()
>>> <AsyncResult: db6737ba-ecee-4fd2-8227-a76c594ba338>
>>>

结果就是say_hi函数向消息队列中发出了一个调用请求由某个worker执行。Celery进程会输出:

[2017-09-03 13:49:57,340: INFO/MainProcess] Received task: play.tasks.say_hi[85ff01ca-d7c9-4401-bfa3-0a9ad96c7192]
[2017-09-03 13:49:57,343: WARNING/ForkPoolWorker-1] hi!
[2017-09-03 13:49:57,344: INFO/ForkPoolWorker-1] Task play.tasks.say_hi[85ff01ca-d7c9-4401-bfa3-0a9ad96c7192] succeeded in 0.0016004400095s: None

现在我们来分析一下tasks.py这个文件。很奇怪的一点是,一上来我们又创建了一个app实例。当我们import了task文件后会不会又创建了一个celery进程呢?答案是不会的,因为只有调用了app.start()才会启动。这只有手动调用或者借助celery命令执行后才会发生。如果只是new了一个instance出来,相当于创建了一个配置文件,不会发生任何重要的实质性的操作。

但是这个app对象也不是什么都不干的。接下来我们定义了两个task函数,并将这个两个函数使用@app.task包装了起来。这样的效果是把这两个普通函数包装成了celery的task对象,这样他们就有了delay方法。当我们执行delay方法时,这些task会找自己所属的那个celery instance,从中获取配置信息(主要是broker的地址)后将调用请求发往消息队列。

不过,这样定义task的方法并不是很好,因为需要在代码中就显式将task函数和一个具体的celery instance绑定了起来。这就使得我们无法复用这些tasks。因此我们可以使用celery的另一种定义tasks的方式来重写我们现有的代码(这也是推荐给django使用的方案):

from __future__ import absolute_import, unicode_literals
import time
from celery import shared_task @shared_task
def say_hi():
print 'hi!'

这里我们不再创建app实例,而是直接使用@shared_task来包装。这样就没有绑定哪个app的问题了。但是正如我们之前所说,在调用tasks的时候,task还是会去寻找自己属于哪个celery instance从而获取配置信息。如果你都不绑定app instance,配置信息哪里来呢?

答案是,tasks和celery instance之间仍然具有绑定或关联的关系,只不过不再是显式的了。简单来说,每个celery instance被创建以后,它就会被自动的注册到某个全局的位置。当一个shared task被执行时,这个task就会自己去这个全局的位置找有哪些celery instances可以从中获取配置信息。如果有多个celery instance都注册了,那么可能它们的消息队列都会被这个task发消息(没有确认过,只是猜测。但这可能就是shared_task的来源)。这就意味着,只要在我们Python进程的任何一个地方(对Django服务器进程也是如此),只要随便哪个地方创建一个celery instance就可以,然后只要import tasks然后使用delay执行即可。这样就解决了celery tasks复用的问题。代码之间的耦合也更小。

更进一步,在我们的python进程中,甚至都不用再手写一遍celery instance的创建调用。直接import play.celery 就可以了,这个文件虽然被celery进程用作了配置文件,但这不妨碍我们在自己的进程中也用这个文件。不如说这是更好的一种解决方案。

Celery基本原理探讨的更多相关文章

  1. 基于celery的任务管理

    1.celery基本原理 Celery是一个由python编写的简单.灵活.可靠的用来处理大量信息的分布式系统,同时提供了操作和维护分布式系统所需要的工具,说白了就是一个用来管理分布式队列的工具. C ...

  2. 移动Web之响应式布局的探讨

    响应式布局的探讨 响应式布局的两种方式 基于百分比的布局 例:Bootstrap 基于rem的布局 例:淘宝触屏版 这两种布局都需要依赖于CSS3的media query来设置布局断点(或者通过js监 ...

  3. celery 异步任务小记

    这里有一篇写的不错的:http://www.jianshu.com/p/1840035cb510 自己的"格式化"后的内容备忘下: 我们总在说c10k的问题, 也做了不少优化, 然 ...

  4. [专业名词·硬件] 2、DC\DC、LDO电源稳压基本常识(包含基本原理、高效率模块设计、常见问题、基于nRF51822电源管理模块分析等)·长文

    综述先看这里 第一节的1.1简单介绍了DC/DC是什么: 第二节是关于DC/DC的常见的疑问答疑,非常实用: 第三节是针对nRF51822这款芯片电源管理部分的DC/DC.LDO.1.8的详细分析,对 ...

  5. LDPC编译码基本原理

    LDPC编译码基本原理     学习笔记 V1.1 2015/02/18 LDPC编译码基本原理   概述   本文是个人针对LDPC的学习笔记,主要针对LDPC译码算法做了简要的总结.该版本主要致力 ...

  6. cas系列(一)--cas单点登录基本原理

    (这段时间打算做单点登录,因此研究了一些cas资料并作为一个系列记录下来,一来可能会帮助一些人,二来对我自己所学知识也是一个巩固.) 一.为什么要实现单点登录 随着信息化不断发展,企业的信息化过程是一 ...

  7. (转)从内存管 理、内存泄漏、内存回收探讨C++内存管理

    http://www.cr173.com/html/18898_all.html 内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟 ...

  8. Oracle数据库容灾备份技术探讨

    Oracle数据库容灾备份技术探讨 三种Oracle灾备技术 对于Oracle数据库的灾备技术,我们可以从Data Guard,GoldenGate和CDP角度去考虑. Oracle Data Gua ...

  9. 深度学习新星:GAN的基本原理、应用和走向

    深度学习新星:GAN的基本原理.应用和走向 (本文转自雷锋网,转载已获取授权,未经允许禁止转载)原文链接:http://www.leiphone.com/news/201701/Kq6FvnjgbKK ...

随机推荐

  1. 浅谈JVM与内存分配

    一.程序内存分配 初始内存分配 当一个程序准备运行时,它首先向java虚拟机要内存,但是java虚拟机本身没有权限,它只能向操作系统申请内存,此时java虚拟机会拥有一个初始内存, 此处额外说明一下e ...

  2. java 线程之executors线程池

    一.线程池的作用 平时的业务中,如果要使用多线程,那么我们会在业务开始前创建线程,业务结束后,销毁线程.但是对于业务来说,线程的创建和销毁是与业务本身无关的,只关心线程所执行的任务.因此希望把尽可能多 ...

  3. 笔记evernote

    8542-1090-0308-5951 2786-2836-1103-4104 6835-5846-6090-5388 5443-4068-2394-0845

  4. 基于Vivado调用ROM IP core设计DDS

     DDS直接数字式频率合成器(Direct Digital Synthesizer) 下面是使用MATLAB生成正弦波.三角波.方波的代码,直接使用即可. t=:*pi/^:*pi y=0.5*sin ...

  5. javascript中this的用法

    this是Javascript语言的一个关键字. 它代表函数运行时,自动生成的一个内部对象,只能在函数内部使用.比如, function test(){ this.x = 1; } 随着函数使用场合的 ...

  6. docker的简单应用(总结笔记)

    sudo docker pull ubuntu /*下载Ubuntu最新镜像*/sudo docker pull ubuntu:14.04 /*下载Ubuntu14.04版镜像*/sudo docke ...

  7. Java 三目运算符表达式的一些问题

    最近在处理一个需求,需求描述如下:对数据库中查询出来的数据的某一个字段做一个简单处理.处理方式是:如果该字段的值(取值范围0~4,有可能为null)等于0,那么默认处理成1. 测试代码如下: publ ...

  8. 关于Wifi室内定位应用中的一些问题:

    公司目前在办公室内布设了一套室内定位的实验环境,用的是华为路由器,采用的算法是基于信号强度的RSSI算法.公司目前希望能使用这套设备得到无线网络覆盖范围下的所有移动设备(对应每个人)的MAC地址,同时 ...

  9. Mongodb启动&关闭

    mac 下mongo的启动和关闭以及启动问题解决 mongo的安装在这:http://www.cnblogs.com/leinov/p/6855784.html Mac os mongodb数据安装路 ...

  10. 身在魔都的她,该不该继续"坚持"前端开发?

    一 这个女孩儿,是我很好很好很好的一位朋友,也是中学的同学,去年从她的本科大学毕业,毕业后由于没找到合适的工作而选择去培训机构培训了比较火爆的前端开发,之后去了上海找工作,但是由于一些原因在从上一家公 ...