Django(41)詳解非同步任務框架Celery

Silent丿丶黑羽 發表於 2021-06-02

celery介紹

  Celery是由Python開發、簡單、靈活、可靠的分散式任務佇列,是一個處理非同步任務的框架,其本質是生產者消費者模型,生產者傳送任務到訊息佇列,消費者負責處理任務。Celery側重於實時操作,但對排程支援也很好,其每天可以處理數以百萬計的任務。特點:

  • 簡單:熟悉celery的工作流程後,配置使用簡單
  • 高可用:當任務執行失敗或執行過程中發生連線中斷,celery會自動嘗試重新執行任務
  • 快速:一個單程式的celery每分鐘可處理上百萬個任務
  • 靈活:幾乎celery的各個元件都可以被擴充套件及自定製

Celery由三部分構成:

  • 訊息中介軟體(Broker):官方提供了很多備選方案,支援RabbitMQRedisAmazon SQSMongoDBMemcached 等,官方推薦RabbitMQ
  • 任務執行單元(Worker):任務執行單元,負責從訊息佇列中取出任務執行,它可以啟動一個或者多個,也可以啟動在不同的機器節點,這就是其實現分散式的核心
  • 結果儲存(Backend):官方提供了諸多的儲存方式支援:RabbitMQ、 RedisMemcached,SQLAlchemy, Django ORMApache CassandraElasticsearch

Django(41)詳解非同步任務框架Celery
工作原理:

  1. 任務模組Task包含非同步任務和定時任務。其中,非同步任務通常在業務邏輯中被觸發併發往訊息佇列,而定時任務由Celery Beat程式週期性地將任務發往訊息佇列;
  2. 任務執行單元Worker實時監視訊息佇列獲取佇列中的任務執行;
  3. 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成功
Django(41)詳解非同步任務框架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工具檢視,如下
Django(41)詳解非同步任務框架Celery
然後我們之前啟動的celeryworker程式會獲取任務列表,逐個執行任務,執行結束後會儲存到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

分別啟動workerbeat

celery worker -A celery_demo -l debug 
celery beat -A celery_demo -l debug

我們可以看到定時任務會每隔10s就執行任務
Django(41)詳解非同步任務框架Celery
執行完的結果會儲存在redis
Django(41)詳解非同步任務框架Celery
 

任務繫結

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(任務返回時候執行)

  1. 通過繼承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}')
  1. 在對應的task函式的裝飾器中,通過 base=MyHookTask 指定
@shared_task(base=MyHookTask, bind=True)
def mul(self, x, y):
	......

 

任務編排

  在很多情況下,一個任務需要由多個子任務或者一個任務需要很多步驟才能完成,Celery也能實現這樣的任務,完成這型別的任務通過以下模組完成:

  • group: 並行排程任務
  • chain: 鏈式任務排程
  • chord: 類似group,但分headerbody2個部分,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

任務分割,分為headerbody兩部分,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可實現對wokertask的管理
官網: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,能夠看到如下頁面
Django(41)詳解非同步任務框架Celery

4.通過api操作

curl http://127.0.0.1:5555/api/workers