首先解釋下目標的概念: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