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
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章