celery实现任务统一收集、分发执行

    首先解释下目标的概念:celery任务消息会由各种途径(比如手动通过python shell触发、通过tornado触发等)发往统一的一个celery broker,然后任务消息会由不同server上的worker去获取并执行。具体点说就是,借助celery消息路由机制,celery broker中开不同的消息队列来接收相应类型的任务消息,然后不同server上开启worker来处理目标消息队列里面的任务消息,即任务统一收集、分发到不同server上执行。

测试
    项目架构如下:一个服务,一部分task运行在server1上,一部分task运行在server2上,所有的任务都可以通过网页向tornado(部署在server1上)发起、tornado接到网页请求调用相应的task handler、task handler向celery broker相应的queue发任务消息、最后server1上的worker和server2上的worker各自去相应的队列中获取任务消息并执行任务。server1是上海集群的10.121.72.94,server2是济阳集群的10.153.104.76,celery broker是redis数据库:redis://10.121.76.204:17016/1。
    首先来看一下server1上的代码结构,
| start_worker.sh
| proj
    |__init__.py      (空文件)
    |celery.py
    |hotplay_task.py
| hotplay_tornado_server.py
上面的代码包含了响应网页请求的tornado server构建代码、server1上的celery服务。

    先来看server1上的celery调度器,
celery.py
    
    
    
    
#-*-coding=utf-8-*-
from __future__ import absolute_import
from celery import Celery
from kombu import Queue
 
app = Celery("proj",
        broker = "redis://10.121.76.204:17016/1",
             include = ['proj.hotplay_task']
             )
 
app.conf.update(
        CELERY_DEFAULT_QUEUE = 'hotplay_sh_default_queue',
        #CELERY_QUEUES = (Queue('hotplay_jy_queue'),), #该队列是给server2用的,并不需要在这里申明
    )
hotplay_task.py
    
    
    
    
from __future__ import absolute_import
 
import sys
import os
import hashlib
import time
import subprocess
 
 
from proj.celery import app
 
 
reload(sys)
sys.setdefaultencoding('utf-8')
 
sys.path.append(os.path.join(os.path.dirname(__file__), "./"))
 
HOTPLAY_CATCHUP_DIR = '/home/uaa/prog/hotplay_v2/online_task/catch_up'
 
@app.task(bind=True)
def do_init_catchup(self, user_name, album_id, album_name, channel_name):
    print 'start to init catch up of user %s album %s:%s in channel %s'%(user_name, album_id, album_name, channel_name)
    job_args = 'source %s/init_catch_up.sh %s %s %s %s > ./logs/%s_%s.log'%(HOTPLAY_CATCHUP_DIR, user_name, album_id, album_name, channel_name, album_id, user_name)
    print 'job_args:', job_args
    P = subprocess.Popen(job_args,shell=True)  
    rt_code = P.wait()
    if rt_code == 0:
        print 'job success...'
    else:
        print 'job error:%d'%(rt_code)
    #    print 'job error:%d, will retry in 5 min'%(rt_code)
    #    raise self.retry(countdown=300)
 
@app.task(bind=True)
def do_catchup(self, hotplay_id, start_dt, end_dt):
    print 'start to catch up of %s:%s-%s'%(hotplay_id, start_dt, end_dt)
    job_args = 'source %s/catch_up_all_run.sh %s %s %s > ./logs/%s.log 2>&1'%(HOTPLAY_CATCHUP_DIR, hotplay_id, start_dt, end_dt, hotplay_id)
    print 'job_args:', job_args
    P = subprocess.Popen(job_args,shell=True)  
    rt_code = P.wait()
    if rt_code == 0:
        print 'job success...'
    else:
        print 'job error:%d'%(rt_code)
    #    print 'job error:%d, will retry in 5 min'%(rt_code)
    #    raise self.retry(countdown=300)
start_worker.sh
     
     
     
     
nohup celery -A proj worker -n hotplay_default_worker -c 3 -Q hotplay_sh_default_queue -l info &
上面的代码定义了一个celery实例,该实例有两个队列,注册了两个celery task function,最后启动一个worker来处理默认队列 hotplay_sh_default_queue (celery.py中重命名过的默认队列)中的任务消息。
     tornado server是所有celery任务的发起者,server1和server2上celery task都由 tornado server相应的handler发起。
hotplay_tornado_server.py
    
    
    
    
#-*-coding=utf-8-*-
from __future__ import absolute_import
import sys
import os
import tornado.web
import tornado.ioloop
import tornado.httpserver
 
from celery.execute import send_task
from proj.hotplay_task import do_init_catchup, do_catchup
 
reload(sys)
sys.setdefaultencoding('utf-8')
 
 
TORNADO_SERVER_PORT=10501
 
class InitCatchupHandler(tornado.web.RequestHandler):
    def get(self, path):
        user_name = self.get_argument("user_name", None)
        album_id = self.get_argument("album_id",None)
        album_name = self.get_argument("album_name",None)
        channel_name = self.get_argument("channel_name", None)
        print "request user_name+album_id+album_name+channel_name:%s+%s_%s+%s"%(user_name, album_id, album_name, channel_name)
        if album_id == '0':
            self.write('test tornado server init catch up handler. sucess. just return\n')
            return
        
        try:
            self.write("0")
            do_init_catchup.delay(user_name, album_id, album_name, channel_name)
        except:
            self.write("-1")
 
class DoCatchupHandler(tornado.web.RequestHandler):
    def get(self, path):
        hotplay_id = self.get_argument("hotplay_id",None)
        start_dt = self.get_argument("start_dt",None)
        end_dt = self.get_argument("end_dt",None)
        print "request hotplay_id+start_dt+end_dt:%s+%s+%s"%(hotplay_id, start_dt, end_dt)
        if hotplay_id == '0':
            self.write('test tornado server catch up handler. sucess. just return\n')
            return
        
        try:
            self.write("0")
            do_catchup.delay(hotplay_id, start_dt, end_dt)
        except:
            self.write("-1")
 
class DoCatchupJYHandler(tornado.web.RequestHandler):
    def get(self, path):
        hotplay_id = self.get_argument("hotplay_id",None)
        start_dt = self.get_argument("start_dt",None)
        end_dt = self.get_argument("end_dt",None)
        print "request jy hotplay_id+start_dt+end_dt:%s+%s+%s"%(hotplay_id, start_dt, end_dt)
        #if hotplay_id == '0':
        #    self.write('test tornado server catch up handler. sucess. just return\n')
        #    return
        send_task('tasks.test1', args=[hotplay_id, start_dt, end_dt], queue='hotplay_jy_queue') #tasks.test1是server2上celery任务函数的file_name.func_name
#file_name是任务函数所在文件相对于celery worker的路径
        #try:
        #    self.write("0")
        #    do_catchup.delay(hotplay_id, start_dt, end_dt)
        #except:
        #    self.write("-1")
 
application = tornado.web.Application(
            [
            (r"/init_catchup/(.*)", InitCatchupHandler),
            (r"/do_catchup/(.*)", DoCatchupHandler),
            (r"/do_catchup_jy/(.*)", DoCatchupJYHandler),
            ], 
            template_path = "template", static_path="static"
            )
            
if __name__ == '__main__':
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(TORNADO_SERVER_PORT)
    tornado.ioloop.IOLoop.instance().start()
代码中定义了3个handler,前两个负责在接收到相应的网页请求后,发起server1上定义的两个task function任务消息,消息发往 celery broker的默认队列 hotplay_sh_default_queue (使用task_name.delay函数发出的请求会加入到默认队列,使用task_name.apply_async或send_task函数则可以指定目标队列),最后由server1上的worker执行。网页请求的格式类似——http://10.121.72.94:10501/do_catchup_jy/?hotplay_id=pxftest&start_dt=2015-08-12&end_dt=2015-08-14。 第3个handler发起一个名为 tasks.test1 的任务消息,发往celery broker的另一个队列 hotplay_jy_queue tasks.test1 任务并没有在server1上的celery调度器中实现(也叫注册),而是放在了server2上,相应的,处理 队列 hotplay_jy_queue 的worker也在 server2上运行。这里,由于 tasks.test1task function没有注册在server1上,所以使用send_task函数来发送任务消息;这是因为task_name.delay、task_name.apply_async函数发送任务请求需要先import task_name相应的python function,而send_task函数发送任务消息其实就相当于往celery broker发送一个字符串类似的任务请求、不需要调用事先写好的task function,然后该字符串类似的任务消息由worker获取、worker根据任务消息去寻找实际的task function来执行。这种机制也是celery实现任务统一收集、分发执行的基础。
    
    来看server2上的celery调度器,
|tasks.py  (注意,要和tornado server中send_task()函数用的file_name一样)
|start_server.sh
由于只是功能测试,写得比较简单,
tasks.py
    
    
    
    
#-*-coding=utf-8-*-
from __future__ import absolute_import
from celery import Celery
from kombu import Queue
 
app = Celery("test",
        broker = "redis://10.121.76.204:17016/1"
    #         include = ['test.tasks']
             )
 
app.conf.update(
        CELERY_DEFAULT_QUEUE = 'hotplay_sh_default_queue', #可省略,但不能和server1的配置不一样
        CELERY_QUEUES = (Queue('hotplay_jy_queue'),),
    )
 
@app.task()
def test1(hotplay_id, start_dt, end_dt): #注意,名字要和tornado_server中send_task()函数用的func_name名字一样
    print 'hotplay_id is %s, stat from %s to %s'%(hotplay_id, start_dt, end_dt)
start_server.sh
    
    
    
    
celery -A tasks worker -n hotplay_jy_worker -c 2 -Q hotplay_jy_queue -l info
server2上调度器主要就是开了一个worker来取tornado server发往 hotplay_jy_queue 队列的任务并执行,当然,任务在哪里执行、相应的任务函数就应该放在哪里。此外,server2和server1上的celery实例app的消息队列配置应该保持一致,因为它们是对同一个celery broker的配置。

总结
    最后总结下上面项目架构的实现:所有的celery任务都由tornado server发起,统一由 celery broker收集、不过 分别由celery broker的 hotplay_sh_default_queue hotplay_jy_queue 两个消息队列接收,最后分别由server1和server2上的worker去执行。
     在上面的项目架构中,tornado server是和server1上的celery调度器放在一起的,这是有必要的,因为send_task函数发送任务消息的时候,至少应该要知道celery broker等信息,而这些信息在server1的celery调度器上有(请注意 hotplay_tornado_server.py from proj . hotplay_task import do_init_catchup , do_catchup 语句,该语句不仅import两个任务函数,还获取了celery实例app的信息,从而获得了 celery broker等配置信息) 。在这之后,如果有其他任务要集成进来,直接在 hotplay_tornado_server.py中增加相应的handler(调用send_task函数向目标队列发送相应的任务消息,目标队列不需要在server1上申明)、并在其他server上写好相应的celery调度器(申明消息队列、实现celery task function、开启worker)即可。 这时, tornado server负责所有任务(不止是本文提到的3个任务)的触发(通过网页触发比较方便)、然后使用 send_task函数往某一个固定的celery broker发送任务消息、不同种类的任务消息发到celery broker上特定的消息队列,每种任务的执行由任务部署的服务器上的celery调度器(就和server2上的调度器)完成,由各个服务器上的celery调度器的worker会到自己目标队列中取任务消息来执行。这样做的好处是:一个broker搞定所有任务,不过有多少种不同的任务、broker上就会有多少个消息队列。

后续
    上文总结中提到tornado server需要和server1上的celery调度器放在一起,以获取celery broker的信息,经过尝试,tornado server是可以完全独立出来的。
     在tornado server的py文件中添加以下代码:
from celery import Celery
app = Celery(broker = "redis://10.121.76.204:17016/1",)
接着,改 send_task ('tasks.test1', args=[hotplay_id, start_dt, end_dt],queue='hotplay_jy_queue')
app.send_task('tasks.test1', args=[hotplay_id, start_dt, end_dt], queue='hotplay_jy_queue')
然后,就可以去掉下面两行了:
from celery.executeimport send_task
from proj.hotplay_taskimport do_init_catchup, do_catchup
这样子, tornado server就可以完全独立出来运行,而不必再和任何任务绑在一起以获得celery broker的信息,因为celery broker的信息直接写在 tornado server的代码里了。当然, hotplay_tornado_server.py 代码经过上面的修改、完全独立出来后, do_init_catchup . delay ( user_name , album_id , album_name , channel_name ) do_catchup . delay ( hotplay_id , start_dt , end_dt ) 需要用send_task函数改写,
app.send_task('proj.hotplay_task.do_init_catchup', args=[user_name, album_id, album_name, channel_name])    #send to default queue: hotplay_default_sh_queue
app.send_task('proj.hotplay_task.do_catchup', args=[hotplay_id, start_dt, end_dt])
    最后说明一下, tornado server完全独立出来的好处: 如果不完全独立出来,那么和 tornado server放在一起的celery调度器需要修改的话,则celery worker和 tornado server也需要重启( tornado server代码调用了 celery调度器的任务函数以及broker信息,所以要重启 ), tornado server至少和一个 celery调度器存在耦合 完全独立后, 解除了tornado server代码和celery调度器之间的耦合,这时tornado server中使用send_task函数发送任务消息、无需经过实际实现的celery任务函数,所以任何celery调度器的改动(只要别改任务函数名和任务函数的参数)都无需重启tornado server、而只要重启celery worker即可,也就是说任务的提交和任务的执行完全分离开来了。

参考:
http://www.avilpage.com/2014/11/scaling-celery-sending-tasks-to-remote.html
https://groups.google.com/forum/#!topic/celery-users/E37wUyOcd3I
http://programming.nullanswer.com/question/29340011
http://www.imankulov.name/posts/celery-for-internal-api.html

你可能感兴趣的:(tornado,celery,分布式任务调度)