celery介紹
Celery
是由Python
開發、簡單、靈活、可靠的分散式任務佇列,是一個處理非同步任務的框架,其本質是生產者消費者模型,生產者傳送任務到訊息佇列,消費者負責處理任務。Celery
側重於實時操作,但對排程支援也很好,其每天可以處理數以百萬計的任務。特點:
- 簡單:熟悉
celery
的工作流程後,配置使用簡單 - 高可用:當任務執行失敗或執行過程中發生連線中斷,
celery
會自動嘗試重新執行任務 - 快速:一個單程式的
celery
每分鐘可處理上百萬個任務 - 靈活:幾乎
celery
的各個元件都可以被擴充套件及自定製
Celery由三部分構成:
- 訊息中介軟體(Broker):官方提供了很多備選方案,支援
RabbitMQ
、Redis
、Amazon SQS
、MongoDB
、Memcached
等,官方推薦RabbitMQ
- 任務執行單元(Worker):任務執行單元,負責從訊息佇列中取出任務執行,它可以啟動一個或者多個,也可以啟動在不同的機器節點,這就是其實現分散式的核心
- 結果儲存(Backend):官方提供了諸多的儲存方式支援:
RabbitMQ
、Redis
、Memcached
,SQLAlchemy
,Django ORM
、Apache Cassandra
、Elasticsearch
等
工作原理:
- 任務模組
Task
包含非同步任務和定時任務。其中,非同步任務通常在業務邏輯中被觸發併發往訊息佇列,而定時任務由Celery Beat
程式週期性地將任務發往訊息佇列; - 任務執行單元
Worker
實時監視訊息佇列獲取佇列中的任務執行; Woker
執行完任務後將結果儲存在Backend
中;
django應用Celery
django
框架請求/響應的過程是同步的,框架本身無法實現非同步響應。但是我們在專案過程中會經常會遇到一些耗時的任務, 比如:傳送郵件、傳送簡訊、大資料統計等等,這些操作耗時長,同步執行對使用者體驗非常不友好,那麼在這種情況下就需要實現非同步執行。非同步執行前端一般使用ajax
,後端使用Celery
。
專案應用
django
專案應用celery
,主要有兩種任務方式,一是非同步任務(釋出者任務),一般是web請求,二是定時任務
非同步任務redis
1.安裝celery
pip3 install celery
2.celery.py
在主專案目錄下,新建 celery.py
檔案:
import os
import django
from celery import Celery
from django.conf import settings
# 設定系統環境變數,安裝django,必須設定,否則在啟動celery時會報錯
# celery_study 是當前專案名
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'celery_demo.settings.py')
django.setup()
app = Celery('celery_demo')
app.config_from_object('django.conf.settings')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
注意:是和settings.py
檔案同目錄,一定不能建立在專案根目錄,不然會引起celery
這個模組名的命名衝突
同時,在主專案的init.py
中,新增如下程式碼:
from .celery import celery_app
__all__ = ['celery_app']
3.settings.py
在配置檔案中配置對應的redis配置:
# Broker配置,使用Redis作為訊息中介軟體
BROKER_URL = 'redis://127.0.0.1:6379/0'
# BACKEND配置,這裡使用redis
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0'
# 結果序列化方案
CELERY_RESULT_SERIALIZER = 'json'
# 任務結果過期時間,秒
CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24
# 時區配置
CELERY_TIMEZONE='Asia/Shanghai'
更加詳細的配置可檢視官方文件:http://docs.celeryproject.org/en/latest/userguide/configuration.html
4.tasks.py
在子應用下建立各自對應的任務檔案tasks.py
(必須是tasks.py這個名字,不允許修改)
from celery import shared_task
@shared_task
def add(x, y):
return x + y
5.呼叫任務
在 views.py
中,通過 delay
方法呼叫任務,並且返回任務對應的 task_id
,這個id用於後續查詢任務狀態
from celery_app.tasks import add
def index(request):
ar = add.delay(10, 6)
return HttpResponse(f'已經執行celery的add任務呼叫,task_id:{ar.id}')
6.啟動celery
在命令視窗中,切換到專案根目錄下,執行以下命令:
celery worker -A celery_demo -l info
- -A celery_demo:指定專案app
- worker: 表明這是一個任務執行單元
- -l info:指定日誌輸出級別
輸出以下結果,代表啟動celery
成功
更多celery
命令的引數,可以輸入celery --help
7.獲取任務結果
在 views.py
中,通過AsyncResult.get()
獲取結果
def get_result(request):
task_id = request.GET.get('task_id')
ar = result.AsyncResult(task_id)
if ar.ready():
return JsonResponse({"status": ar.state, "result": ar.get()})
else:
return JsonResponse({"status": ar.state, "result": ""})
AsyncResult
類的常用的屬性和方法:
- state: 返回任務狀態,等同
status
; - task_id: 返回
任務id
; - result: 返回任務結果,同
get()
方法; - ready(): 判斷任務是否執行以及有結果,有結果為
True
,否則False
; - info(): 獲取任務資訊,預設為結果;
- wait(t): 等待t秒後獲取結果,若任務執行完畢,則不等待直接獲取結果,若任務在執行中,則
wait
期間一直阻塞,直到超時報錯; - successful(): 判斷任務是否成功,成功為
True
,否則為False
;
程式碼的準備工作都做完了,我們開始訪問瀏覽器127.0.0.1/celery_app/
,得到以下結果
已經執行celery的add任務呼叫,task_id:b1e9096e-430c-4f1b-bbfc-1f0a0c98c7cb
這一步的作用:啟動add
任務,然後放在訊息中介軟體中,這裡我們用的是redis
,就可以通過redis工具檢視,如下
然後我們之前啟動的celery
的worker
程式會獲取任務列表,逐個執行任務,執行結束後會儲存到backend中,最後通過前端ajax
輪詢一個介面,根據task_id
提取任務的結果
接下來我們訪問http://127.0.0.1:8000/celery_app/get_result/?task_id=b1e9096e-430c-4f1b-bbfc-1f0a0c98c7cb
,就能從頁面上檢視到結果,如下
{
"status": "SUCCESS",
"result": 16
}
說明定時任務執行成功,返回結果為16
定時任務
在第一步的非同步任務的基礎上,進行部分修改即可在
1.settings.py
在settings
檔案,配置如下程式碼即可
from celery.schedules import crontab
CELERYBEAT_SCHEDULE = {
'mul_every_10_seconds': {
# 任務路徑
'task': 'celery_app.tasks.mul',
# 每10秒執行一次
'schedule': 10,
'args': (10, 5)
},
'xsum_week1_20_20_00': {
# 任務路徑
'task': 'celery_app.tasks.xsum',
# 每週一20點20分執行
'schedule': crontab(hour=20, minute=20, day_of_week=1),
'args': ([1,2,3,4],),
},
}
引數說明如下:
- task:任務函式
- schedule:執行頻率,可以是整型(秒數),也可以是
timedelta
物件,也可以是crontab
物件,也可以是自定義類(繼承celery.schedules.schedule
) - args:位置引數,列表或元組
- kwargs:關鍵字引數,字典
- options:可選引數,字典,任何
apply_async()
支援的引數 - relative:預設是
False
,取相對於beat
的開始時間;設定為True
,則取設定的timedelta
時間
更加詳細的說明參考官方文件:http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#crontab-schedules
2.啟動celery
分別啟動worker
和beat
celery worker -A celery_demo -l debug
celery beat -A celery_demo -l debug
我們可以看到定時任務會每隔10s就執行任務
執行完的結果會儲存在redis
中
任務繫結
Celery
可通過task
繫結到例項獲取到task
的上下文,這樣我們可以在task
執行時候獲取到task
的狀態,記錄相關日誌等
我們可以想象這樣一個場景,當任務遇到問題,執行失敗時,我們需要進行重試,實現程式碼如下
@shared_task(bind=True)
def add(self, x, y):
try:
logger.info('-add' * 10)
logger.info(f'{self.name}, id:{self.request.id}')
raise Exception
except Exception as e:
# 出錯每4秒嘗試一次,總共嘗試4次
self.retry(exc=e, countdown=4, max_retries=4)
return x + y
說明如下:
- 在裝飾器中加入引數
bind=True
- 在
task
函式中的第一個引數設定為self
self
物件是celery.app.task.Task
的例項,可以用於實現重試等多種功能
接著我們在views.py
檔案中,寫入如下檢視函式
def get_result(request):
task_id = request.GET.get('task_id')
ar = result.AsyncResult(task_id)
if ar.successful():
return JsonResponse({"status": ar.state, "result": ar.get()})
else:
return JsonResponse({"status": ar.state, "result": ""})
接著我們訪問http://127.0.0.1:8000/celery_app/
,建立一個任務id,返回如下結果
已經執行celery的add任務呼叫,task_id:f55dcfb7-e184-4a29-abe9-3e1e55a2ffad
然後啟動celery命令:
celery worker -A celery_demo -l info
我們會發現celery
中的任務會丟擲一個異常,並且重試了4次,這是因為我們在tasks
任務中主動丟擲了一個異常
[2021-06-02 11:27:55,487: INFO/MainProcess] Received task: celery_app.tasks.add[f55dcfb7-e184-4a29-abe9-3e1e55a2ffad] ETA:[2021-06-02 11:27:59.420668+08:00]
[2021-06-02 11:27:55,488: INFO/ForkPoolWorker-11] Task celery_app.tasks.add[f55dcfb7-e184-4a29-abe9-3e1e55a2ffad] retry: Retry in 4s: Exception()
最後我們訪問http://127.0.0.1:8000/celery_app/get_result/?task_id=f55dcfb7-e184-4a29-abe9-3e1e55a2ffad
,查詢任務的結果
{
"status": "FAILURE",
"result": ""
}
由於我們主動丟擲異常(為了模擬執行過程中的錯誤),這就導致了我們的狀態為FAILURE
任務鉤子
Celery
在執行任務時,提供了鉤子方法用於在任務執行完成時候進行對應的操作,在Task
原始碼中提供了很多狀態鉤子函式如:on_success
(成功後執行)、on_failure
(失敗時候執行)、on_retry
(任務重試時候執行)、after_return
(任務返回時候執行)
- 通過繼承
Task
類,重寫對應方法即可,示例:
class MyHookTask(Task):
def on_success(self, retval, task_id, args, kwargs):
logger.info(f'task id:{task_id} , arg:{args} , successful !')
def on_failure(self, exc, task_id, args, kwargs, einfo):
logger.info(f'task id:{task_id} , arg:{args} , failed ! erros: {exc}')
def on_retry(self, exc, task_id, args, kwargs, einfo):
logger.info(f'task id:{task_id} , arg:{args} , retry ! erros: {exc}')
- 在對應的
task
函式的裝飾器中,通過base=MyHookTask
指定
@shared_task(base=MyHookTask, bind=True)
def mul(self, x, y):
......
任務編排
在很多情況下,一個任務需要由多個子任務或者一個任務需要很多步驟才能完成,Celery
也能實現這樣的任務,完成這型別的任務通過以下模組完成:
- group: 並行排程任務
- chain: 鏈式任務排程
- chord: 類似
group
,但分header
和body
2個部分,header
可以是一個group
任務,執行完成後呼叫body
的任務 - map: 對映排程,通過輸入多個入參來多次排程同一個任務
- starmap: 類似map,入參類似
*args
- chunks: 將任務按照一定數量進行分組
1.group
首先在urls.py
中寫入如下程式碼:
path('primitive/', views.test_primitive),
接著在views.py
中寫入檢視函式
from celery import result, group
def test_primitive(request):
lazy_group = group(mul.s(i, i) for i in range(10)) # 生成10個任務
promise = lazy_group()
result = promise.get()
return JsonResponse({'function': 'test_primitive', 'result': result})
在tasks.py
檔案中寫入如下程式碼
@shared_task
def mul(x, y):
return x * y
說明:
通過task
函式的 s
方法傳入引數,啟動任務,我們訪問http://127.0.0.1:8000/celery_app/primitive/
,會得到以下結果
{
"function": "test_primitive",
"result": [
0,
1,
4,
9,
16,
25,
36,
49,
64,
81
]
}
上面這種方法需要進行等待,如果依然想實現非同步的方式,那麼就必須在tasks.py
中新建一個task
方法,呼叫group
,示例如下:
tasks.py
from celery.result import allow_join_result
@shared_task
def first_group():
with allow_join_result():
return group(mul.s(i, i) for i in range(10))().get()
urls.py
path('group_task/', views.group_task),
views.py
def group_task(request):
ar = first_group.delay()
return HttpResponse(f'已經執行celery的group_task任務呼叫,task_id:{ar.id}')
2.chain
預設上一個任務的結果作為下一個任務的第一個引數
def test_primitive(request):
promise = chain(mul.s(2, 2), mul.s(5), mul.s(8))() # 160
result = promise.get()
return JsonResponse({'function': 'test_primitive', 'result': result})
3.chord
任務分割,分為header
和body
兩部分,hearder
任務執行完在執行body
,其中hearder
返回結果作為引數傳遞給body
def test_primitive(request):
# header: [3, 12]
# body: xsum([3, 12])
promise = chord(header=[tasks.add.s(1,2),tasks.mul.s(3,4)],body=tasks.xsum.s())()
result = promise.get()
return JsonResponse({'function': 'test_primitive', 'result': result})
celery管理和監控
celery
通過flower
元件實現管理和監控功能 ,flower
元件不僅僅提供監控功能,還提供HTTP API
可實現對woker
和task
的管理
官網:https://pypi.org/project/flower/
文件:https://flower.readthedocs.io/en/latest
1.安裝flower
pip3 install flower
2.啟動flower
flower -A celery_demo--port=5555
- -A:專案名
- --port: 埠號
3.在瀏覽器輸入:http://127.0.0.1:5555
,能夠看到如下頁面
4.通過api操作
curl http://127.0.0.1:5555/api/workers