1.何为消息队列?
在不同系统(或物理设备)之间,应用软件之间,程序进程之间,常常会有各种互相的信息传递;为保证消息传递的可靠性,对所传消息引入一个保存的容器:一方面用来接收发送者产生的信息,一方面在接收者正常的情况下完成消息的派送,并在无法接收消息时对信息进行存储,然后在适当的时机完成信息的派送。一般称该容器为消息队列。
2.消息队列解决什么问题?
对于部分需要较长时间处理的任务类型,采用传统的同步处理方式会带来较长时间的性能损耗或是不良的用户体验,如上传文件处理,图像压缩,发送邮件等,此类实时性要求不是特别高的任务类型,采用异步消息队列的模式可以提升处理效率。
3.Celery简介
一个基于消息通信的任务调度分发处理框架,由python语言实现。具体工作过程可以归纳如下:
1.预先定义好一系列需要执行的操作(对应python中的函数),称之为consumers,将其注册到Celery的task中。
2.启动Celery服务后,相关的workers会在各自单独的进程中持续监听消息队列,一旦接收到调用消息,
worker就会根据相关配置找到consumer执行,即执行步骤1中定义的函数。
3.任务的发起者(或称之为producer),在调用相关worker之前,需先导入Celery配置环境,然后向指定的worker发送相关消息。
4.Celery中的消息接收和分发采用第三方消息中间件,如: RabbitMQ、Redis,MongoDB等,称之为broker。同时,这些消息中间件也可用来存放worker的执行结果(称为backend)。
4.Celery基本使用
1. 安装:
celery的Python包:pip install celery
celery依赖的消息中间件,安装方式参考:RabbitMQ,Redis,MongoDB
2. 基本用法(以MongoDB作为消息中间件):
1 2 3 4 5 6 7 8 9 10 11 12
|
#建立main.py,输入以下内容: from celery import Celery
app = Celery(name=‘task', broker='mongodb://localhost:27017/workers_tasks') #name为app实例的名称,非必须,broker为消息中间件的连接地址 #本示例采用本地mongoldb,在启动服务前需确保mongodb已经启动并可正常连接。若采用远程服务器,则配置成如下格式: #'mongodb://userid:password@hostname:port/database_name' #broker属于app实例配置部分,可通过app.config_from_object(object),app.config_from_envvar(env_name),app.conf.update()等方式配置
#定义一个函数,注册到task @app.task def sayhi(): return 'hi'
|
3.启动方式:
终端模式进入到main.py所在目录,执行
1
|
celery -A main worker --loglevel=debug
|
一般出现以下画面表示启动成功。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
[tasks] . celery.backend_cleanup . celery.chain . celery.chord . celery.chord_unlock . celery.chunks . celery.group . celery.map . celery.starmap . main.sayhi [2016-09-24 00:36:39,215: DEBUG/MainProcess] | Worker: Starting Pool [2016-09-24 00:36:39,233: DEBUG/MainProcess] ^-- substep ok [2016-09-24 00:36:39,234: DEBUG/MainProcess] | Worker: Starting Consumer [2016-09-24 00:36:39,234: DEBUG/MainProcess] | Consumer: Starting Connection [2016-09-24 00:36:39,248: INFO/MainProcess] Connected to mongodb://localhost:27017/workers_tasks [2016-09-24 00:36:39,249: DEBUG/MainProcess] ^-- substep ok [2016-09-24 00:36:39,249: DEBUG/MainProcess] | Consumer: Starting Events [2016-09-24 00:36:39,259: DEBUG/MainProcess] ^-- substep ok [2016-09-24 00:36:39,259: DEBUG/MainProcess] | Consumer: Starting Heart [2016-09-24 00:36:39,262: DEBUG/MainProcess] ^-- substep ok [2016-09-24 00:36:39,262: DEBUG/MainProcess] | Consumer: Starting Tasks [2016-09-24 00:36:39,266: DEBUG/MainProcess] ^-- substep ok [2016-09-24 00:36:39,266: DEBUG/MainProcess] | Consumer: Starting Control [2016-09-24 00:36:39,273: DEBUG/MainProcess] ^-- substep ok [2016-09-24 00:36:39,273: DEBUG/MainProcess] | Consumer: Starting event loop [2016-09-24 00:36:39,273: WARNING/MainProcess] celery@Work ready. [2016-09-24 00:36:39,273: DEBUG/MainProcess] basic.qos: prefetch_count->32
|
此时启动了一个worker实例,[tasks]中显示的为监听该worker的consumers,包括我们在main.py中定义的sayhi。
相关说明:-A表示启动应用实例,main是包含celery实例的文件或是包(在本示例中为main.py),worker为启动实例的运行模式,该模式下所有定义于main中的task将全部进入监听模式。
—loglevel=debug:指定log级别为debug模式,此时会有较多的信息列印至终端,便于分析问题。
在生产环境中,常需要以守护进程的方式在后台运行,可使用supervisor
注:通过celery help可以查看更多的启动命令提示。
4.调用方式:
另起一终端环境,进入main.py所在目录,启动python解释环境,执行以下代码:
1 2 3 4 5 6 7 8 9 10
|
>>> import main >>> main.sayhi.delay() <AsyncResult: 7dbd1acb-cd88-402f-bc6c-8c8b386ff544> >>>
#此时在启动celery服务的终端中,可以看到如下类似消息,表示调用任务成功 [2016-09-24 01:21:23,650: INFO/MainProcess] Received task: main.sayhi[7dbd1acb-cd88-402f-bc6c-8c8b386ff544] [2016-09-24 01:21:23,651: DEBUG/MainProcess] TaskPool: Apply <function _fast_trace_task at 0x110d802a8> (args:('main.sayhi', '7dbd1acb-cd88-402f-bc6c-8c8b386ff544', [], {}, {'utc': True, u'is_eager': False, 'chord': None, u'group': None, 'args': [], 'retries': 0, u'delivery_info': {u'priority': 0, u'redelivered': None, u'routing_key': u'celery', u'exchange': u'celery'}, 'expires': None, u'hostname': 'celery@Work', 'task': 'main.sayhi', 'callbacks': None, u'correlation_id': u'7dbd1acb-cd88-402f-bc6c-8c8b386ff544', 'errbacks': None, 'timelimit': (None, None), 'taskset': None, 'kwargs': {}, 'eta': None, u'reply_to': u'3057b462-bedc-3c51-9ea9-83094c8771e7', 'id': '7dbd1acb-cd88-402f-bc6c-8c8b386ff544', u'headers': {}}) kwargs:{}) [2016-09-24 01:21:23,653: DEBUG/MainProcess] Task accepted: main.sayhi[7dbd1acb-cd88-402f-bc6c-8c8b386ff544] pid:34352 [2016-09-24 01:21:23,653: INFO/MainProcess] Task main.sayhi[7dbd1acb-cd88-402f-bc6c-8c8b386ff544] succeeded in 0.000943564984482s: hi
|
注:调用worker时,要采用delay方法触发异步调用。delay为apply_async方法的简易版本,可满足常规需求。
如需特殊调用方式,可参考官方文档关于apply_async的相关说明。
在目前情况下,无法获取到函数的返回结果,若需对结果进行存档或是追踪,需要设置backend属性,在main.py中添加以下代码
1 2 3
|
#该配置信息指定worker执行完成后,存储结果状态信息的位置。 app.conf.update(CELERY_RESULT_BACKEND='mongodb://localhost:27017/', CELERY_MONGODB_BACKEND_SETTINGS={'database': 'celery_dev_db', 'taskmeta_collection': 'workers_tasks_result'})
|
重新启动Celery服务,在调用程序终端中执行以下语句
1 2 3 4 5
|
>>> res = main.sayhi.delay() >>> res.ready() #worker执行完毕,返回True,否则为False True >>> res.get() #返回worker函数的返回值 'hi'
|
注:一般情况下不会采用上述的方式来确认worker的返回结果,因为此方式将异步调用变成了同步调用,失去使用celery的意义。
在需要追踪结果的情况下,可以采用producer轮询backend或是由worker发送消息至producer。
Celery进阶
a.定时任务:
celery可以支持定时任务,预先配置好相关参数,启动服务后可以自动执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
# 示例,在main.py中增加以下内容 from datetime import timedelta
add_schedule = { 'add-every-5-seconds': {'task': 'main.add', 'schedule': timedelta(seconds=5), 'args': (1, 2)} } app.conf.update(CELERYBEAT_SCHEDULE=add_schedule)
@app.task def add(x,y): return x+y
# 以上内容创建一个定时5s自动执行的任务,调用add函数,参数为(1,2)
# 启动方式,通过beat参数执行:celery -A main beat —-loglevel=debug # 也可以与worker同时启动:celery -A main worker -B —loglevel=debug
# 在需要对定时任务做更精确的控制的情况下,可以采用celery.schedules中的crontab来实现 # 例如,将上述间隔5s的add任务,改变为每周一的早上7:30执行,可以采用如下配置方式: # from celery.schedules import crontab # add_schedule = { # "add-every-monday-morning": {“task”:”main.add”,”schedule":crontab(hour=7, minute=30, day_of_week=1), "args":(1, 2)} # } # app.conf.update(CELERYBEAT_SCHEDULE=add_schedule)
|
注:设置定时任务时需注意CELERY_TIMEZONE相关设置,详细设置参考
b.工作流
celery可以对task进行调用顺序的设置,形成一个任务执行流,对于要加入任务流的函数,需将其进行’签名’操作(或称为建立子任务)。
下面以main.py中的add函数来作为示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
>>> from celery import signature # new_add作为一个add签名,参数为(2,2),调用new_add 10s后执行add(2,2) >>> new_add=signature('main.add', args=(2, 2), countdown=10) >>> new_add main.add(2, 2) >>> new_add.delay() <AsyncResult: e5afb013-f907-4111-a9df-d3917237f2fb>
#以上代码可以采用subtask实现相同效果: >>> new_add=add.subtask((2,2),countdown=10) >>> new_add main.add(2, 2)
#还有一种简写方式: >>> new_add = add.s(2,2) >>> new_add main.add(2, 2)
#签名支持部分参数传入 >>> new_add1 = add.s(2) >>> res = new_add1.delay(3) >>> res.get() 5 >>>
|
上述签名的应用方式看起来似乎用处不大,但签名可以通过delay和apply_async传入参数的方式调用,这样可以将任务组装成工作流。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
# 通过link将签名任务链接到当前执行的任务 >>> res=add.apply_async((5,2),link=add.s(9)) >>> res.get() 7 >>> res.children[0].get() 16 >>> list(res.collect()) [(<AsyncResult: 371f21c7-d546-45ef-b5b7-dea124114754>, 7), (<AsyncResult: 24938b03-ad1a-41dd-91de-61bbc5cce851>, 16)] >>>
# 通过chain组合多个子任务 >>> from celery import chain >>> res = chain(add.s(1, 2), add.s(3), add.s(4)).delay() >>> res.get() 10 >>>
#通过group组合多个子任务 >>> from celery import group >>> res = group(add.s(1, 1), add.s(2, 2), add.s(3, 3), add.s(4, 4)).delay() >>> res.get() [2, 4, 6, 8] >>>
|
关于工作流的应用,celery还有内置chord,map,starmap,chunks等内置方法,详细使用方式参考官方文档
c.消息分发与任务调度的实现机制:
producer发出调用请求(message包含所调用任务的相关信息)—>celery服务启动时,会产生一个或多个交换机(exchanges),对应的交换机 接收请求message—>交换机根据message内容,将message分发到一个或多个符合条件的队列(queue)—>每个队列上都有一个或多个worker在监听,在监听到符合条件的message到达后,worker负责进行任务处理,任务处理完被确认后,队列中的message将被删除。
celery支持所有AMQP(Advanced Message Queuing Protocol:高级消息队列协议)路由机制,可以通过配置的方式,执行相关的
消息路由路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
# 在main.py中增加以下代码
from kombu import Exchange, Queue
#增加两个task,test_queue_1与test_queue_2 @app.task def test_queue_1(): return 'queue1'
@app.task def test_queue_2(): return 'queue2'
# queue_1与queue_2为消息队列名称 # Exchange:为交换机实例,具有不同的类型。详细参考 # routing_key:用来告知exchange将task message传送至相对应的queue
queue = ( Queue('queue_1', Exchange('Exchange1', type='direct'), routing_key='queue_1_key'), Queue('queue_2', Exchange('Exchange2', type='direct'), routing_key='queue_2_key') ) route = { 'main.test_queue_1': {'queue': 'queue_1', 'routing_key': 'queue_1_key'}, 'main.test_queue_2': {'queue': 'queue_2', 'routing_key': 'queue_2_key'} }
app.conf.update(CELERY_QUEUES=queue, CELERY_ROUTES=route)
######### # 为直观的观察效果,开启终端1运行以下命令,监听queue_1队列: celery -A main worker -Q queue_1 --loglevel=debug
# 开启终端2运行以下命令,监听queue_2队列: celery -A main worker -Q queue_2 --loglevel=debug
# 此时调用main.py中的test_queue_1和test_queue_2,会发现task被分发到各个对应的celery worker服务。 # 对于没有被队列接收的sayhi函数,通过sayhi.apply_async(queue='queue_1’)可以将任务分发到queue_1
|
celery的广播模式(该模式仅仅在broker采用RabbitMQ,Redis才可以使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
# 在main.py中,将broker='mongodb://localhost:27017/workers_tasks' 改为broker='amqp://',同时启动本地rabbitmq服务 # 添加以下代码, from kombu.common import Broadcast
@app.task def broadcast(): return 'broadcast'
queue_bor = ( Broadcast('broadcast_tasks’), #此处设置消息队列broadcast为广播模式,及该队列上的消息会发送至所有监听它的worker Queue('broadcast_tasks'), ) queue_route = { 'main.broadcast': {'queue': 'broadcast_tasks'}, } app.conf.update(CELERY_QUEUES=queue_bor, CELERY_ROUTES=queue_route)
# 按以上配置后,同时在两个终端启动celery服务,通过broadcast.delay()调用task时,会发现两个celery实例均有执行broadcast函数。
|
基于上述内容,可以应付大部分的异步消息处理的情况。若需进一步深入,请参阅官方文档