分佈式隊列神器 Celery

昨天在一個自動化項目中看到代碼引用Celery這個模塊,我一臉懵逼,不知道這是個何物。當然,遇到不會的肯定是第一時間問度娘啦。

Celery 是什麼?

Celery 是一個由 Python 編寫的簡單、靈活、可靠的用來處理大量信息的分佈式系統,它同時提供操作和維護分佈式系統所需的工具。
Celery 專注於實時任務處理,支持任務調度。
說白了,它是一個分佈式隊列的管理工具,我們可以用 Celery 提供的接口快速實現並管理一個分佈式的任務隊列。

概念的東西都是很懸的東西,也是看完一次都不知道它在說啥。那問題來了,是如何應用Celery的呢?

快速入門

首先,我們要理解 Celery 本身不是任務隊列,它是管理分佈式任務隊列的工具,或者換一種說法,它封裝好了操作常見任務隊列的各種操作,我們用它可以快速進行任務隊列的使用與管理,當然你也可以自己看 rabbitmq 等隊列的文檔然後自己實現相關操作都是沒有問題的

Celery 是語言無關的,雖然它是用 Python 實現的,但他提供了其他常見語言的接口支持。只是如果你恰好使用 Python 進行開發那麼使用 Celery 就自然而然了。

想讓 Celery 運行起來我們要明白幾個概念:

Brokers

brokers 中文意思爲中間人,在這裏就是指任務隊列本身,Celery 扮演生產者和消費者的角色,brokers 就是生產者和消費者存放/拿取產品的地方(隊列)

常見的 brokers 有 rabbitmq、redis、Zookeeper 等

Result Stores / backend

顧名思義就是結果儲存的地方,隊列中的任務運行完後的結果或者狀態需要被任務發送者知道,那麼就需要一個地方儲存這些結果,就是 Result Stores 了

常見的 backend 有 redis、Memcached 甚至常用的數據都可以。

Workers

就是 Celery 中的工作者,類似與生產/消費模型中的消費者,其從隊列中取出任務並執行

Tasks

就是我們想在隊列中進行的任務咯,一般由用戶、觸發器或其他操作將任務入隊,然後交由 workers 進行處理。

理解以上概念後,我們可以通過一些實例來加深理解。這裏我們用 redis 當做 celery 的 broker 和 backend。

安裝 Celery 和 redis 以及 python 的 redis 支持:

apt-get install redis-server
pip install redis
pip install celery

然後,我們需要寫一個task:

#/usr/bin/python
from celery import Celery

app=Celery('tasks',backend='redis://127.0.0.1:6379/0',broker='redis://127.0.0.1:6379/0') #配置好celery的backend和broker

@app.task #普通函數裝飾爲 celery task
def add(x,y):
    return x + y

OK,到這裏,broker 我們有了,backend 我們有了,task 我們也有了,現在就該運行 worker 進行工作了,在 tasks.py 所在目錄下運行:

celery -A tasks worker --loglevel=info

意思就是運行 tasks 這個任務集合的 worker 進行工作(當然此時broker中還沒有任務,worker此時相當於待命狀態)

最後一步,就是觸發任務啦,最簡單方式就是再寫一個腳本然後調用那個被裝飾成 task 的函數:

#/usr/bin/python
import time
from tasks import add

result=add.delay(4,4) #不要直接 add(4, 4),這裏需要用 celery 提供的接口 delay 進行調用
while  not result.ready():
    time.sleep()

print 'task done:{0}'.format(result.get())

運行此腳本

python trigger.py 
#task done:8

delay 返回的是一個 AsyncResult 對象,裏面存的就是一個異步的結果,當任務完成時result.ready() 爲 true,然後用 result.get() 取結果即可。

進階用法

經過快速入門的學習後,我們已經能夠使用 Celery 管理普通任務,但對於實際使用場景來說這是遠遠不夠的,所以我們需要更深入的去了解 Celery 更多的使用方式。

首先來看之前的task:

@app.task  #普通函數裝飾爲 celery task
def add(x, y):
    return x + y

這裏的裝飾器app.task實際上是將一個正常的函數修飾成了一個 celery task 對象,所以這裏我們可以給修飾器加上參數來決定修飾後的 task 對象的一些屬性。

首先,我們可以讓被修飾的函數成爲 task 對象的綁定方法,這樣就相當於被修飾的函數 add 成了 task 的實例方法,可以調用 self 獲取當前 task 實例的很多狀態及屬性。

其次,我們也可以自己複寫 task 類然後讓這個自定義 task 修飾函數 add ,來做一些自定義操作。

根據任務狀態執行不同操作

任務執行後,根據任務狀態執行不同操作需要我們複寫 task 的 on_failure、on_success 等方法:

#/usr/bin/python
from celery import Celery

app=Celery('tasks',backend='redis://127.0.0.1:6379/0',broker='redis://127.0.0.1:6379/0')
class MyTask(app.Task):
    def on_success(self,retval,task_id,args,kwargs):
        print 'task done: {0}'.format(retval)
        return super(MyTask,self).on_success(retval,task_id,args,kwargs)
    def on_failure(self,exc,task_id,args,kwargs,einfo):
        print 'task fail, reason:{0}'.format(exc)
        return super(MyTask,self).on_failure(exc,task_id,args,kwargs,einfo)

@app.task(base=MyTask)
def add(x,y):
    return x + y

然後繼續運行 worker:

celery -A tasks worker --loglevel=info

運行腳本,得到:

再修改下tasks:

@app.task
def add(x,y):
    raise KeyError
    return x + y

重新運行 worker,再運行 trigger.py:


可以看到,任務執行成功或失敗後分別執行了我們自定義的 on_failure、on_success

綁定任務爲實例方法

#/usr/bin/python
from celery import Celery
from celery.utils.log import get_task_logger

app=Celery('tasks',backend='redis://127.0.0.1:6379/0',broker='redis://127.0.0.1:6379/0')
logger=get_task_logger(__name__)
@app.task(bind=True)
def add(self,x,y):
    logger.info(self.request.__dict__)
    return x + y

然後,重新如下tasks.py和trigger.py


任務狀態回調

實際場景中得知任務狀態是很常見的需求,對於 Celery 其內建任務狀態有如下幾種:

參數 說明
PENDING 任務等待中
STARTED 任務已開始
SUCCESS 任務執行成功
FAILURE 任務執行失敗
RETRY 任務將被重試
REVOKED 任務取消

當我們有個耗時時間較長的任務進行時一般我們想得知它的實時進度,這裏就需要我們自定義一個任務狀態用來說明進度並手動更新狀態,從而告訴回調當前任務的進度,具體實現:


#/usr/bin/python
from celery import Celery
import time

app=Celery('tasks',backend='redis://127.0.0.1:6379/0',broker='redis://127.0.0.1:6379/0')

@app.task(bind=True)
def test_mes(self):
    for i in xrange(1,11):
        time.sleep(0.1)
        self.update_state(state="PROGRESS",meta={'p':i*10})

    return 'finish'

然後修改 trigger.py :

# coding:utf-8
#/usr/bin/python
import time
import sys
from tasks import test_mes

def pm(body):
    res=body.get('result')
    if body.get('status') == 'PROGRESS':
        sys.stdout.write(u'\r任務進度:{0}%'.format(res.get('p')))
        sys.stdout.flush()
    else:
        print '\r'
        print res
r=test_mes.delay()
print r.get(on_message=pm,propagate=False)

然後運行任務:


定時/週期任務

Celery 進行週期任務也很簡單,只需要在配置中配置好週期任務,然後在運行一個週期任務觸發器( beat )即可:
新建 Celery 配置文件 celery_config.py:

#/usr/bin/python
from datetime import timedelta
from celery.schedules import crontab

CELERYBEAT_SCHEDULE = {
    'ptask': {
        'task': 'tasks.period_task',
        'schedule': timedelta(seconds=5),
    },
}
 
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0'

配置中 schedule 就是間隔執行的時間,這裏可以用 datetime.timedelta 或者 crontab 甚至太陽系經緯度座標進行間隔時間配置

如果定時任務涉及到 datetime 需要在配置中加入時區信息,否則默認是以 utc 爲準。例如中國可以加上:

CELERY_TIMEZONE = 'Asia/Shanghai'

然後在 tasks.py 中增加要被週期執行的任務:

#/usr/bin/python
from celery import Celery


app=Celery('tasks',backend='redis://127.0.0.1:6379/0',broker='redis://127.0.0.1:6379/0')
app.config_from_object('celery_config')

@app.task(bind=True)
def period_task(self):
    print "period task done:{0}".format(self.request.id)

然後重新運行 worker,接着再運行 beat:

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