【異步/定時任務】Django中使用Celery實現異步和定時任務【原創】

參考

官方文檔Celery在Django上
官方文檔Celery
Celery中文文檔
Github地址
Celery全面學習筆記
分佈式任務隊列Celery入門與進階


一、概要

Celery是由Python開發、簡單、靈活、可靠的分佈式任務隊列,其本質是生產者消費者模型,生產者發送任務到消息隊列,消費者負責處理任務。


Celery側重於實時操作,但對調度支持也很好,其每天可以處理數以百萬計的任務。特點:

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

應用場景舉例:

  • web應用:當用戶在網站進行某個操作需要很長時間完成時,我們可以將這種操作交給Celery執行,直接返回給用戶,等到Celery執行完成以後通知用戶,大大提好網站的併發以及用戶的體驗感。
  • 任務場景:比如在運維場景下需要批量在幾百臺機器執行某些命令或者任務,此時Celery可以輕鬆搞定。
  • 定時任務:向定時導數據報表、定時發送通知類似場景,雖然Linux的計劃任務可以幫我實現,但是非常不利於管理,而Celery可以提供管理接口和豐富的API。

Celery由以下四部分構成:任務模塊(Task)、消息中間件(Broker)、任務執行單元Worker、結果存儲(Backend)

  • 任務模塊Task:包含異步任務和定時任務,異步任務通常在業務邏輯中被觸發併發往任務隊列,而定時任務由Celery Beat進程週期性的將任務發往任務隊列
  • 消息中間件Broker:即爲任務調度隊列,接收任務生產者發來的消息(即任務),將任務存入隊列。Celery本身不提供隊列服務,官方提供了很多備選方案,支持RabbitMQ、Redis、Amazon SQS等,官方推薦RabbitMQ。
  • 任務執行單元Worker:Worker是任務執行單元,負責從消息隊列中取出任務執行,它可以啓動一個或者多個,也可以啓動在不同的機器節點,這就是其實現分佈式的核心。實時監控消息隊列,獲取隊列中調度的任務並執行它。
  • 結果存儲Backend:用於存儲任務的執行結果。官方提供了諸多的存儲方式支持:RabbitMQ、 Redis、Memcached、SQLAlchemy、Django ORM、Apache Cassandra、Elasticsearch

工作原理:

  • 任務模塊Task包含異步任務和定時任務。其中,異步任務通常在業務邏輯中被觸發併發往消息隊列,而定時任務由Celery Beat進程週期性地將任務發往消息隊列;
  • 任務執行單元Worker實時監視消息隊列獲取隊列中的任務執行;
  • Woker執行完任務後將結果保存在Backend中;

二、環境

  • Python:3.7
  • Django:2.2.4
  • Celery:4.3

注意:Celery 4.x和3.x版本相差很大,建議使用4.x,另外,如果是Django 1.8之前的版本,只能使用Celery 3.x


三、依賴

需要安裝的依賴有:

pip install celery
pip install redis
# 或者是直接:pip install "celery[redis]"

pip install mysqlclient
pip install django-celery-results

以上的依賴中,只有Celery是必須的,其他的依賴都不是必須,如下:

  • redis:非必須,默認Celery是使用RabbitMQ作爲Broker中間人的並且默認安裝好RabbitMQ依賴了,我自己習慣用Redis,所以需要安裝Redis依賴
  • mysqlclient:非必須,默認Celery是不保存任務結果的,如果想要查看任務的結果並且可以保存到數據庫中,所以必須安裝mysqlclient連接數據庫
  • django-celery-results:非必須,默認Celery是不保存任務結果的,如果想要查看任務的結果並且保存到數據庫中,就必須安裝該依賴,如果不想保存到數據庫的話,也可以使用Redis來進行保存

中間人:

  • RabbitMQ:默認
  • Redis
  • Amazon SQS

注意:Celery之前的版本是支持以數據庫作爲中間人的,但是4.x不支持數據庫作爲中間人,具體可以參考 官方文檔


四、初始化

1. 新建一個應用

新建一個應用,應用名隨意,這裏的應用名爲tasks

python manage startapp tasks

setting.py中增加應用:

INSTALLED_APPS = (
    ...,
    'tasks',
    'django_celery_results',
)

執行遷移

python manage.py migrate django_celery_results

2. setting.py同級目錄修改init.py

setting.py同級目錄修改__init__.py:

from __future__ import absolute_import, unicode_literals
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app

__all__ = ('celery_app',)

3. setting.py同級目錄新建celery.py

setting.py同級目錄新建celery.py:

from __future__ import absolute_import, unicode_literals
import os
from celery import Celery
from django.conf import settings

# 獲取當前文件夾名,即爲該Django的項目名
project_name = os.path.split(os.path.abspath('.'))[-1]
project_settings = '%s.settings' % project_name

# 設置環境變量
os.environ.setdefault('DJANGO_SETTINGS_MODULE', project_settings)

# 實例化Celery
app = Celery(project_name)

# 使用Django的settings文件配置celery
app.config_from_object('django.conf:settings', namespace='CELERY')

# Celery加載所有註冊的應用
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)


@app.task(bind=True)
def debug_task(self):
    print('Request: {0!r}'.format(self.request))

注意:設置環境變量的時候

os.environ.setdefault('DJANGO_SETTINGS_MODULE', project_settings)

project_settings一定是項目名.settings,如果項目名和目錄名不一樣的話,則不能使用上面的方式來獲取當前文件夾名字了


4. 修改setting.py

setting.py中最下面新增:

# Broker的地址
CELERY_BROKER_URL = 'redis://127.0.0.1:6379'

# 任務執行返回結果
CELERY_RESULT_BACKEND = 'django-db'

# celery內容等消息的格式設置
CELERY_ACCEPT_CONTENT = ['application/json', ]
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'

# celery時區設置,使用settings中TIME_ZONE同樣的時區
CELERY_TIMEZONE = TIME_ZONE

更多的配置可以參考 官方文檔


Redis中間人的配置格式爲:

redis://:password@hostname:port/db_number

默認使用的是 localhost 的 6379 端口中 0 數據庫


Celery默認是不保存任務的結果,但是支持保存任務的功能,內置一些任務的後端結果存儲:

  • 數據庫:如果想要使用數據庫的話,需要安裝django-celery-results,並且CELERY_RESULT_BACKEND需要設置爲:django-db,默認存在 django_celery_results_taskresult 表裏面
  • Memcached
  • RPC(RabbitMQ/AMQP)
  • Redis:格式跟Redis中間人的配置格式一致,使用Redis的話不需要安裝django-celery-results

五、測試

1. 新增任務

在tasks應用下新增tasks.py:

from __future__ import absolute_import, unicode_literals
from celery import shared_task
import datetime

@shared_task
def add(x, y):
    res = x + y
    time_format = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print('當前時間爲:' + time_format + ' ,兩個數相加的結果爲:')
    print(res)
    return res

@shared_task
def mul(x, y):
    res = x * y
    time_format = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print('當前時間爲:' + time_format + ' ,兩個數相乘的結果爲:')
    print(res)
    return res

@shared_task
def xsum(numbers):
    res = sum(numbers)
    print(res)
    return res

最終的目錄結構如下:

項目名
│
├─manage.py
│
├─tasks
│ │ admin.py
│ │ apps.py
│ │ models.py
│ │ tests.py
│ │ views.py
| | tasks.py
│ └ __init__.py
│
├─templates
│
└─項目名
    │ settings.py
    │ urls.py
    │ wsgi.py
    | celery.py
    └ __init__.py

2. 啓動Worker

在命令行下執行:

celery -A 項目名 worker -l info
# 或者是
celery -A 項目名 worker --loglevel=info

注意:如果是在Windows 10下,並且是Celery 4.x,請使用下面的命令啓動,否則在測試的時候會報錯,具體見最下面的問題集。

celery -A 項目名 worker --pool=solo -l info

3. 測試

創建一個異步任務來測試,在命令行下執行:

python manage.py shell


from tasks.tasks import add, mul, xsum

res = add.delay(2,3)
res.get()

看看有沒有結果輸出,或者是可以在Admin管理頁面的Celery Results裏面看到結果


delay() 是 apply_async() 的快捷方法,即執行異步任務,異步任務由Worker來進行處理,可以通過控制檯輸出的日誌進行查看執行情況。
調用任務會返回一個 AsyncResult 的實例,用於檢測任務的狀態,等待任務完成獲取返回值(如果任務執行失敗,會拋出異常)。


其他的命令還有:

from tasks.tasks import add, mul, xsum

# 調用異步任務
t = add.delay(2, 3)

# 同步拿結果
t.get()

# 同步拿結果,並且設置獲取結果的超時時間,超過1秒拿不到結果則報錯
t.get(timeout=1)

# 檢查任務是否完成
t.ready()

# 如果出錯,獲取錯誤結果,不觸發異常
t.get(propagate=False)
t.traceback

更多的命令可以查看 官方文檔


六、定時任務

1. 概要

在Django中實現定時任務,基本上有兩套方案:

  • Celery原生:比較簡單,在配置中使用CELERY_BEAT_SCHEDULE來人爲加載定時任務
  • 後臺管理任務:稍微複雜一點,安裝django-celery-beat,在Admin後臺中管理和配置定時任務

注意:Celery 4.x和3.x的定時任務不兼容,建議使用4.x


2.Celery原生

使用Celery原生方案很簡單, 不需要安裝其他的依賴,只需要在配置中配置CELERY_BEAT_SCHEDULE即可


配置

在setting.py最後面加上:

from celery.schedules import crontab
from datetime import timedelta

# 定時任務
CELERY_BEAT_SCHEDULE = {
    # 每十秒執行一次add方法
    'add-every-10-seconds': {
        'task': 'tasks.tasks.add',

        # 多長時間執行一次
        'schedule': 10.0, # 支持直接用數字表示秒數
        # 'schedule': timedelta(seconds=10), # 可以用timedelta對象

        # 必要的參數,這裏指add()的參數
        'args': (16, 26)
    },
    # 每個週一的20:57分執行一次mul方法
    'add-every-monday-morning': {
        'task': 'tasks.tasks.mul',
        'schedule': crontab(hour=20, minute=57, day_of_week=1),
        'args': (16, 16),
    },
}

更多的用法可以參考 官方文檔

啓動

命令行上執行:

# 啓動worker
celery -A 項目名 worker --pool=solo -l info

# 啓動定時任務beat
celery -A 項目名 beat -l info

即可在命令行上看到輸出結果,或者是可以在Admin管理頁面的Celery Results裏面看到結果


3. 後臺管理任務

使用後臺管理任務的話,可以更方便的使用Django Admin進行所有定時任務的管理以及查看每次任務的結果,而且把任務存到Django的數據庫裏面去,不過相比起原生方案,步驟比較多且麻煩,而且需要安裝django-celery-beat,django-celery-beat會在admin後臺生成定時任務模塊進行管理,模塊爲:PERIODIC TASKS,一般爲Periodic tasks,保存在django_celery_beat_periodictask 表


配置

安裝django-celery-beat

pip install django-celery-beat

setting.py中增加應用:

INSTALLED_APPS = (
    ...,
    'django_celery_beat',
)

執行遷移,會自動創建一些表:

python manage.py migrate django_celery_beat

登錄Django Admin後,可以看到多了PERIODIC TASKS模塊,在Periodic tasks裏面配置定時任務即可,配置定時任務很簡單,這裏就跳過了


啓動

命令行上執行:

# 啓動worker
celery -A 項目名 worker --pool=solo -l info

# 啓動定時任務beat
celery -A 項目名 beat -l info -S django
# 或者是
celery -A 項目名 beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler

即可在命令行上看到輸出結果,或者是可以在Admin管理頁面的Celery Results裏面看到結果,並且啓動之後,會在根目錄下生成celerybeat.pid文件


注意:使用django-celery-beat來管理後臺任務的時候,啓動beat需要在後面加上-S參數來指定調度器


注意:這兩個方案也可以同時使用,啓動的時候指定-S參數即可同時使用的,如果一起使用的話,原生設置的定時任務,會被同步到Django Admin後臺裏面的定時任務去


如果想要省事的話,也可以把–scheduler django_celery_beat.schedulers:DatabaseScheduler寫到配置文件setting.py中去,即

# 定時任務來源 從數據庫中讀取
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'

然後啓動定時任務的時候,就可以省略後面了,即:

celery -A 項目名 beat -l info

注意:如果setting.py中的CELERY_TIMEZONE設置的是非UTC的話,比如說’Asia/Shanghai’,且USE_TZ設置爲False的話,那麼運行定時任務的時候會報錯,這個時候需要在配置中加上以下設置可以避免報錯,具體的報錯可以參考最下面的問題集。

# 避免時區的問題
CELERY_ENABLE_UTC = False
DJANGO_CELERY_BEAT_TZ_AWARE = False

七、問題集

1. 報錯:ValueError(‘not enough values to unpack (expected 3, got 0)’)

參考:
https://blog.csdn.net/qq_30242609/article/details/79047660
https://segmentfault.com/q/1010000010560344
https://github.com/celery/celery/issues/4178


在Windows平臺下,並且是Celery 4.x,在命令行上執行後Worker可能會有報錯,報錯如下:

Task handler raised error: ValueError('not enough values to unpack(expected 3, got 0)')
Traceback (most recent call last):
  File "C:\Users\boli.hong\AppData\Roaming\Python\Python37\site-packages\billiard\pool.py", line 362, in workloop
    result = (True, prepare_result(fun(*args, **kwargs)))
  File "C:\Users\boli.hong\AppData\Roaming\Python\Python37\site-packages\celery\app\trace.py", line 546, in _fast_trace_task
    tasks, accept, hostname = _loc
ValueError: not enough values to unpack (expected 3, got 0)

原因:
看別人描述大概就是說Celery 4.x不支持Windows平臺了,所以Windows下運行就會出現這個問題


解決方法:

  • 第一種方法(建議):–pool=solo,只需要啓動Worker的時候,加上參數即可,即celery -A 項目名 worker --pool=solo -l info
  • 第二種方法:eventlet,需要先pip install eventlet,然後啓動Worker的時候加上-P參數,celery -A 項目名 worker -l info -P eventlet

2. MySQL backend does not support timezone-aware datetimes when USE_TZ is False.

當setting.py中的CELERY_TIMEZONE設置的是非UTC的話,比如說’Asia/Shanghai’,且USE_TZ設置爲False的話,那麼啓動定時任務的時候會報錯,報錯信息如下:

 raise ValueError("MySQL backend does not support timezone-aware datetimes when USE_TZ is False.")
ValueError: MySQL backend does not support timezone-aware datetimes when USE_TZ is False.

解決方法:

  • 第一種方法:setting.py中的USE_TZ需要設置爲True(不推薦)
  • 第二種方法(推薦):setting.py中增加:
# 避免時區的問題
CELERY_ENABLE_UTC = False
DJANGO_CELERY_BEAT_TZ_AWARE = False

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