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_taskimport 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
 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Celery是一个Python分布式任务队列,它可以帮助我们异步地执行任务,而且还可以定时执行任务。下面是使用Django的Celery框架来实现定时任务执行的步骤: 1. 安装Celery。可以使用pip工具来安装Celery。 ```bash pip install celery ``` 2. 配置Django项目。在Django项目的settings.py文件中加入以下配置: ```python INSTALLED_APPS = [ # ... 'django_celery_results', 'django_celery_beat', # ... ] CELERY_BROKER_URL = 'redis://localhost:6379' CELERY_RESULT_BACKEND = 'django-db' CELERY_ACCEPT_CONTENT = ['application/json'] CELERY_RESULT_SERIALIZER = 'json' CELERY_TASK_SERIALIZER = 'json' ``` 其中,CELERY_BROKER_URL是指定Celery使用Redis作为消息代理,CELERY_RESULT_BACKEND是指定Celery的结果存储方式为Django数据库,CELERY_ACCEPT_CONTENT、CELERY_RESULT_SERIALIZER和CELERY_TASK_SERIALIZER则是指定消息的序列化方式。 3. 创建定时任务。在Django项目的tasks.py文件中定义需要定时执行任务。例如: ```python from celery import shared_task @shared_task def hello(): print('Hello, World!') ``` 4. 配置定时任务。在Django项目的settings.py文件中加入以下配置: ```python CELERY_BEAT_SCHEDULE = { 'hello_task': { 'task': 'tasks.hello', 'schedule': 10.0, }, } ``` 其中,'hello_task'是任务的名称,'task'指定了任务的函数名,'schedule'则指定了任务执行间隔时间。 5. 启动Celery。在Django项目的根目录下执行以下命令启动Celery: ```bash celery -A your_project_name worker -l info -B ``` 其中,-A参数指定了Celery要加载的Django项目的名称,-B参数表示启用定时任务。 6. 测试定时任务是否生效。等待10秒钟后,可以在控制台看到输出了'Hello, World!',表示定时任务已经成功执行。 以上就是使用Django的Celery框架来实现定时任务执行的步骤。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值