在 Django 中使用 Celery 來進行耗時操作

weixin_33866037發表於2017-03-25

最近在實驗室中做一個專案(心酸啊,本想安安靜靜的清清閒閒的畢業的,沒想到。。。),要把實驗室發過的論文中的一些演算法整合到一個 Web 伺服器上,使用者可以上傳資料,還可以在 Web 上看到計算結果的視覺化圖表。整個伺服器的後臺是使用 Django 框架來搭建的,這些演算法需要處理一定量的資料,使用了 numpy,pandas,scipy 等數值計算庫,每一組資料的處理有時候需要跑好幾個小時。為了合理的排程這些演算法,我們這裡使用了 Celery

1. Django 處理 Request 的基本流程

2060588-d04cb0fcefc67633.png
Django 流程示意圖

上面的這一張是網路上的 Django 處理 request 的流程示意圖。大致意思就是:

瀏覽器發起 http 請求 ----> http handling(request 解析) ----> url 匹配(正則匹配找到對應的 View) ----> 在View中進行邏輯的處理與資料計算(包括呼叫 Model 類進行資料庫的增刪改查)----> 將資料推送到 template,返回對應的 template/response。

對於一些簡單的操作,可以放在 View 中處理。在View處理任務時使用者處於等待狀態,直到頁面返回結果。但是對於一些複雜的操作,則在 View 中應該先返回 response,再在後臺處理任務。使用者無需等待。當任務處理完成時,我們可以再通過 Ajax 之類的方式告知使用者。

Celery 就是基於 Python 開發的一個分散式任務佇列框架,支援使用任務佇列的方式在分佈的機器/程式/執行緒上執行任務排程。

2. Celery

2060588-1286901af0a02642.png
Celery 的基本架構

上圖是 Celery 的基本架構,它採用典型的生產生--消費者模式,主要由三部分組成:broker(訊息佇列)、workers(消費者:處理任務)、backend(儲存結果)。實際應用中,使用者從 Web 前端發起一個請求,我們只需要將請求所要處理的任務丟入任務佇列 broker 中,由空閒的 worker 去處理任務即可,處理的結果會暫存在後臺資料庫 backend 中。我們可以在一臺機器或多臺機器上同時起多個 worker 程式來實現分散式地並行處理任務。

3. 安裝 Celery

安裝過程就是直接按照官網上的文件安裝即可。我這裡用的均是目前的最新穩定版。

  • macOS Sierra 10.12.3
  • Django 1.10
  • Celery 4.0.2

在早前版本的 Celery 中,有一個專門供 Django 使用的 Celery 版本:django-celery。但是在現在 Celery 已經統一為一個版本,所以直接安裝原生的 Celery 即可:

pip install celery

Celery 推薦使用 RabbitMQRedis,Amazon SQS,Zookeeper,這幾個作為 broker,但是隻有前兩個支援在生產環境使用。下面的表格對比了幾種 broker。

Name Status Monitoring Remote Control
RabbitMQ Stable Yes Yes
Redis Stable Yes Yes
Amazon SQS Stable No No
Zookeeper Experimental No No

我是使用 Redis 作為 broker 的。除了安裝 redis 之外,還應該安裝 redis 的 python 支援庫。

安裝 Redis:

brew install redis

安裝 redis 的 python 支援庫:

pip install redis

輸入 redis-server 來開啟 redis。當你看見下面的圖案時,就說明成功開啟了 redis。redis 預設監聽 6379 埠。開啟之後可以用 ctrl+c 來退出。

2060588-c9790b8ba65dfc8f.jpeg
開啟 redis

4. 把 Celery 配置到 Django 上

假設你有一個專案 proj:

- proj/
  - proj/__init__.py
  - proj/settings.py
  - proj/urls.py
- manage.py

Celery 建議在 proj/proj/celery.py 上定義一個 Celery 的例項。

檔案 proj/proj/celery.py:

from __future__ import absolute_import, unicode_literals
import os
from celery import Celery

# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proj.settings')

app = Celery('proj')

# Using a string here means the worker don't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
#   should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')

# Load task modules from all registered Django app configs.
app.autodiscover_tasks()


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

然後再在proj/proj/__init__.py做一些配置。

檔案 proj/proj/__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']

完成上面的步驟之後,可以在命令列輸入:

celery worker -A proj -l info

正常情況下,應該會出現類似於下圖的輸出。

2060588-696c018d8e25beee.jpeg
開啟 celery 並與 redis 連線

ok,接下來,為了讓 celery 中執行的任務的結果返回我們的 Django,我們還應該安裝 django-celery-results

pip install django-celery-results

再在 proj/proj/settings.py: 中做如下的設定:

檔案proj/proj/settings.py:

# Celery 設定
CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_BACKEND = 'django-db'
CELERY_TIMEZONE = 'Asia/Shanghai' 

INSTALLED_APPS = [
    ...
    ...
    'django_celery_results'
]

再 migrate 一下:

migrate django_celery_results

5. 加入一個耗時任務

在你的 app 的目錄下,新建一個 tasks.py 檔案。在裡面加入一個耗時的任務:

from __future__ import absolute_import, unicode_literals
from celery import shared_task

# 模擬一個耗時操作
@shared_task
def longtime_test():
   ...
  # 在這裡進行一些耗時操作
   ...

views.py 中,寫成這樣:

def test_view(request):
    # do something
    longtime_test.delay()
    return render(request, 'template.html', {'data': data})

這樣之後,就會先返回 html 模版,再在後臺計算資料了。

相關文章