在運營系統中經常用到非同步方式來處理我們的任務,比如將業務上線流程串成任務再寫入佇列,通過後臺作業節點去排程執行。比較典型的案例為騰訊的藍鯨、織雲、雲智慧等平臺。本譯文結合Django+Celery+Redis實現一個定期從Flickr 獲取圖片並展示的簡單案例,方便大家理解實現非同步對列任務的過程。
剛接觸django的時候,我經歷過的最讓人沮喪的事情是需要定期執行一段程式碼。我寫了一個需要每天上午12點執行一個動作的不錯的函式。很簡單是不是?錯了。事實證明,這對我來說是一個巨大的困難點,因為,那時我使用Cpane型別的虛擬主機管理系統,它能專門提供一個很友好,很方便的圖形使用者介面來設定cron作業。
經過反覆研究,我發現了一個很好的解決方案 – Celery,一個用於在後臺執行任務的強大的非同步作業佇列。但是,這也導致了其它的問題,因為我無法找到一系列簡單的指令將celery整合到Django專案中。
當然,我最終還是設法成功搞定了它 – 這正是本文將介紹的內容:如何將celery整合到一個Django專案,建立週期性任務。
該專案利用Python3.4,Django的1.8.2,celery3.1.18和Redis3.0.2.
一、概述
由於大篇幅的文字,為了您的方便,請參閱下表中的每一步的簡要資訊,並獲取相關的程式碼。
步驟 概要 Git標籤
樣板 樣板下載 V1
建立 整合Celery和Django V2
Celery任務 新增基本的Celery任務 V3
週期性任務 新增週期性任務 V4
本地執行 本地執行我們的應用程式 V5
遠端執行 遠端執行我們的應用程式 V5
二、什麼是Celery
“Celery是一個非同步任務佇列/基於分散式訊息傳遞的作業佇列。它側重於實時操作,但對排程的支援也很好。”本文,我們將重點講解週期性執行任務的排程特點。
為什麼這一點有用呢?
回想一下你不得不在將來執行某一特定任務的經歷。也許你需要每隔一小時訪問一個API。或者,也許你需要在這一天結束時傳送一批電子郵件。不論任務大小,Celery都可以使得排程週期性任務變的很容易。
你永遠不希望終端使用者等待那些不必要的頁面載入或動作執行完成。如果你的應用程式工作流的一部分是一個需要很長時間的程式,當資源可用時,你就可以使用Celery在後臺執行這段程式,從而使你的應用程式可以繼續響應客戶端的請求。這樣可以使任務在應用程式的環境之外執行。
三、構建專案
在深入瞭解Celery之前,先從Github庫中獲取開始專案。確保啟用一個虛擬的環境,安裝必要的軟體,並執行遷移。然後啟動伺服器,通過你的瀏覽器導航到http://localhost:8000/。你應當能看到‘恭喜你的第一個Django頁面’。完成後,關閉伺服器。
接下來,我們開始安裝celery。
1 2 |
$ pip install celery==3.1.18 $ pip freeze > requirements.txt |
現在,我們通過簡單的三步將celery整合到django專案中。
步驟一:建立celery.py
在“picha“目錄下,建立celery.py,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from __future__ import absolute_import import os from celery import Celery from django.conf import settings # set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'picha.settings') app = Celery('picha') # Using a string here means the worker will not have to # pickle the object when using Windows. app.config_from_object('django.conf:settings') app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) @app.task(bind=True) def debug_task(self): print('Request: {0!r}'.format(self.request)) |
請注意程式碼中的註釋。
步驟二:引入celery應用
為了確保在django啟動時載入了celery應用,在settings.py旁邊新建__init__.py,並新增以下程式碼到__init__.py中。
1 2 3 4 5 |
from __future__ import absolute_import # 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 |
完成以上步驟後,你的專案目錄應該是這樣的:
1 2 3 4 5 6 7 8 |
├── manage.py ├── picha │ ├── __init__.py │ ├── celery.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── requirements.txt |
步驟三:安裝 Redis作為Celery的“中介軟體”
Celery使用中介軟體在django專案與celery監控者之間傳遞訊息。在本教程中,我們使用redis作為訊息中間代理。
首先,從官方下載頁面或通過brew(BREW安裝Redis)安裝Redis,然後開啟你的終端上,在一個新的終端視窗,啟動伺服器:
1 |
$ redis-server |
你可以通過在終端中輸入如下命令測試Redis是否正常工作。
1 |
$ redis-cli ping |
Redis應該回復PONG – 試試吧!
一旦Redis正常啟動了,把下面的程式碼新增到你的settings.py檔案中:
1 2 3 4 5 6 7 |
# CELERY STUFF BROKER_URL = 'redis://localhost:6379' CELERY_RESULT_BACKEND = 'redis://localhost:6379' CELERY_ACCEPT_CONTENT = ['application/json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = 'Africa/Nairobi' |
你還需要新增Redis的作為Django專案的依賴:
1 2 |
$ pip install redis==2.10.3 $ pip freeze > requirements.txt |
就是這樣了!你現在應該能夠在Django中使用Celery。有關設定Celery與Django的更多資訊,請檢視官方Celery文件。
在繼續下面步驟之前,讓我們進行一些完整性檢查,以確保一切都是正常的。
測試Celery worker已準備好接收任務:
1 2 3 4 5 |
$ celery -A picha worker -l info ... [2015-07-07 14:07:07,398: INFO/MainProcess] Connected to redis://localhost:6379// [2015-07-07 14:07:07,410: INFO/MainProcess] mingle: searching for neighbors [2015-07-07 14:07:08,419: INFO/MainProcess] mingle: all alone |
使用CTRL-C殺死該段程式。現在,測試Celery任務排程程式是否已經準備好:
1 2 3 |
$ celery -A picha beat -l info ... [2015-07-07 14:08:23,054: INFO/MainProcess] beat: Starting... |
在上述完成時再次終止該程式。
1、Celery任務
Celery利用celery呼叫的常規Python函式作為任務。
例如,讓我們把這個基本函式變為celery的任務:
1 2 |
def add(x, y): return x + y |
首先,新增一個裝飾器。
1 2 3 4 5 |
from celery.decorators import task @task(name="sum_two_numbers") def add(x, y): return x + y |
然後你可以通過以下方式利用celery非同步執行該任務:
1 |
add.delay(7, 8) |
很簡單,對不對?
所以,這對於解決類似你要載入一個網頁,而不需要使用者等待一些後臺程式的完成這些型別的任務來說是非常完美的。
讓我們來看一個例子…
讓我們再回到Django專案的版本3,它包括一個接受來自使用者的反饋的應用程式,人們形象地稱之為反饋:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
├── feedback │ ├── __init__.py │ ├── admin.py │ ├── emails.py │ ├── forms.py │ ├── models.py │ ├── tests.py │ └── views.py ├── manage.py ├── picha │ ├── __init__.py │ ├── celery.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── requirements.txt └── templates ├── base.html └── feedback ├── contact.html └── email ├── feedback_email_body.txt └── feedback_email_subject.txt |
安裝新的必要軟體,啟動應用程式,並導航到http://localhost:8000/feedback/。你應該看到如下結果:
讓我們連線celery任務。
2、新增任務
基本上,使用者提交反饋表後,我們希望讓他繼續以他舒服的方式往下進行,而我們在後臺進行處理反饋,傳送電子郵件等等。
要做到這一點,首先新增一個叫tasks.py的檔案到“feedback”目錄:
1 2 3 4 5 6 7 8 9 10 11 12 |
from celery.decorators import task from celery.utils.log import get_task_logger from feedback.emails import send_feedback_email logger = get_task_logger(__name__) @task(name="send_feedback_email_task") def send_feedback_email_task(email, message): """sends an email when feedback form is filled successfully""" logger.info("Sent feedback email") return send_feedback_email(email, message) |
然後按照如下內容更新forms.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from django import forms from feedback.tasks import send_feedback_email_task class FeedbackForm(forms.Form): email = forms.EmailField(label="Email Address") message = forms.CharField( label="Message", widget=forms.Textarea(attrs={'rows': 5})) honeypot = forms.CharField(widget=forms.HiddenInput(), required=False) def send_email(self): # try to trick spammers by checking whether the honeypot field is # filled in; not super complicated/effective but it works if self.cleaned_data['honeypot']: return False send_feedback_email_task.delay( self.cleaned_data['email'], self.cleaned_data['message']) |
大體上,send_feedback_email_task.delay(email, message)的函式過程,併傳送反饋電子郵件等都是在使用者繼續使用該網站的同時作為後臺程式執行。
注:在views.py中的success_url被設定為將使用者重定向到/ 目錄,這個目錄還不存在。我們會在下一節設定這個終點啟動。
3、週期任務
通常情況下,你經常需要安排一個任務在特定的時間執行 – 例如,一個web scraper 可能需要每天都執行。這樣的任務,被稱為週期性任務,很容易建立利用celery啟動。
celery使用“celery beat”來安排定期任務。celery beat定期執行任務,然後由celery worker執行任務。
例如,下面的任務計劃每15分鐘執行一次:
1 2 3 4 5 6 |
from celery.task.schedules import crontab from celery.decorators import periodic_task @periodic_task(run_every=(crontab(minute='*/15')), name="some_task", ignore_result=True) def some_task(): # do something |
讓我們通過往Django專案中新增功能來看一個更強大的例子。
回到Django專案版本4,它包括另一個新的應用程式,叫做photos,這個應用程式使用 Flickr API獲取新照片用來顯示在網站:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
├── feedback │ ├── __init__.py │ ├── admin.py │ ├── emails.py │ ├── forms.py │ ├── models.py │ ├── tasks.py │ ├── tests.py │ └── views.py ├── manage.py ├── photos │ ├── __init__.py │ ├── admin.py │ ├── models.py │ ├── settings.py │ ├── tests.py │ ├── utils.py │ └── views.py ├── picha │ ├── __init__.py │ ├── celery.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── requirements.txt └── templates ├── base.html ├── feedback │ ├── contact.html │ └── email │ ├── feedback_email_body.txt │ └── feedback_email_subject.txt └── photos └── photo_list.html |
安裝新的必要軟體,執行遷移,然後啟動伺服器,以確保一切都是好的。重新測試反饋表。這次,它應該重定向好了。
下一步是什麼?
既然我們需要週期性的呼叫Flickr API,以獲取更多的照片新增到我們的網站,我們可以新增一個celery任務。
4、新增任務
往photos應用中新增一個tasks.py。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from celery.task.schedules import crontab from celery.decorators import periodic_task from celery.utils.log import get_task_logger from photos.utils import save_latest_flickr_image logger = get_task_logger(__name__) @periodic_task( run_every=(crontab(minute='*/15')), name="task_save_latest_flickr_image", ignore_result=True ) def task_save_latest_flickr_image(): """ Saves latest image from Flickr """ save_latest_flickr_image() logger.info("Saved image from Flickr") |
在這裡,我們通過在一個task中包裝這個函式,來實現每15分鐘執行一次save_latest_flickr_image()函式。該@periodic_task裝飾器抽象出程式碼來執行celery任務,使得tasks.py乾淨,易於閱讀!
5、本地執行
準備開始執行了?
在Django應用程式和Redis執行的前提下,開啟兩個新的終端視窗/標籤。在每一個新的視窗中,導航到你的專案目錄,啟用你的虛擬環境,然後執行下面的命令(每個視窗一個):
1 2 |
$ celery -A picha worker -l info $ celery -A picha beat -l info |
當你訪問http://127.0.0.1:8000/ 網址的時候,你現在應該能看到一個圖片。我們的應用程式每15分鐘從Flickr 獲取一張圖片。
通過photos/tasks.py檢視程式碼。點選“Feedback”按鈕傳送一些反饋意見:
以上是通過celery任務執行的。更多的請檢視feedback/tasks.py。
就這樣,你成功的啟動並執行了 Picha專案!
當你本地開發Django專案時,這是一個很好的測試,但是當你需要部署到生產環境- 就像 DigitalOcean時,就不那麼合適了。為此,建議你通過使用Supervisor在後臺作為一個守護程式執行celery worker和排程器。
6、遠端執行
安裝很簡單。從版本庫中獲取版本5(如果你還沒有的話)。然後,SSH到遠端伺服器,並執行:
1 |
$ sudo apt-get install supervisor |
然後,通過在遠端伺服器上“/etc/supervisor/conf.d/” 目錄下新增配置檔案來告知Supervisor celery的workers。在我們的例子中,我們需要兩個這樣的配置檔案 – 一個用於Celery worker,一個是Celery scheduler。
在本地,在專案的根目錄下建立一個“supervisor”的資料夾,然後新增下面的檔案。
Celery Worker: picha_celery.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
; ================================== ; celery worker supervisor example ; ================================== ; the name of your supervisord program [program:pichacelery] ; Set full path to celery program if using virtualenv command=/home/mosh/.virtualenvs/picha/bin/celery worker -A picha --loglevel=INFO ; The directory to your Django project directory=/home/mosh/sites/picha ; If supervisord is run as the root user, switch users to this UNIX user account ; before doing any processing. user=mosh ; Supervisor will start as many instances of this program as named by numprocs numprocs=1 ; Put process stdout output in this file stdout_logfile=/var/log/celery/picha_worker.log ; Put process stderr output in this file stderr_logfile=/var/log/celery/picha_worker.log ; If true, this program will start automatically when supervisord is started autostart=true ; May be one of false, unexpected, or true. If false, the process will never ; be autorestarted. If unexpected, the process will be restart when the program ; exits with an exit code that is not one of the exit codes associated with this ; process’ configuration (see exitcodes). If true, the process will be ; unconditionally restarted when it exits, without regard to its exit code. autorestart=true ; The total number of seconds which the program needs to stay running after ; a startup to consider the start successful. startsecs=10 ; Need to wait for currently executing tasks to finish at shutdown. ; Increase this if you have very long running tasks. stopwaitsecs = 600 ; When resorting to send SIGKILL to the program to terminate it ; send SIGKILL to its whole process group instead, ; taking care of its children as well. killasgroup=true ; if your broker is supervised, set its priority higher ; so it starts first priority=998 |
注:確保更新這些檔案的路徑,以匹配你的遠端伺服器的檔案系統。
基本上,這些supervisor 配置檔案告訴supervisord如何執行並管理我們的’programs’(因為它們是由supervisord呼叫)。
在上面的例子中,我們已經建立了兩個名為“pichacelery”和“pichacelerybeat”的supervisord程式。
現在,只需將這些檔案拷貝到遠端伺服器的/etc/supervisor/conf.d/目錄下。
我們還需要在遠端伺服器上建立上面指令碼中提到的日誌檔案:
1 2 |
$ touch /var/log/celery/picha_worker.log $ touch /var/log/celery/picha_beat.log |
最後,執行以下命令,使 Supervisor 知道它所管理的程式的存在 – 例如,pichacelery和pichacelerybeat:
1 2 |
$ sudo supervisorctl reread $ sudo supervisorctl update |
執行以下命令停止,啟動,和/或檢查pichacelery程式的狀態:
1 2 3 |
$ sudo supervisorctl stop pichacelery $ sudo supervisorctl start pichacelery $ sudo supervisorctl status pichacelery |
你可以通過閱讀官方文件獲取Supervisor的更多資訊。
7、最後提示
1. 千萬不要傳遞Django模型物件到celery任務。為了避免模型物件在傳遞給celery任務之前已經改變了,傳遞celery的主鍵給celery。然後,在執行之前使用主鍵從資料庫中獲取物件。
2. 預設celery排程會在本地建立一些檔案儲存它的排程表。這些檔案是“celerybeat-schedule.db”和“celerybeat.pid”。如果你在使用版本控制系統,比如Git(你應該使用!),請忽略這個檔案,不要將它們新增到你的程式碼庫中,因為它們是為本地執行的程式服務的。
8、下一步
以上就是將celery整合到一個django專案的基本介紹。
想要更多?
1. 深入研究官方celery使用者指南,以瞭解更多資訊。
2. 建立一個Fabfile來設定Supervisor和配置檔案。確保新增命令到reread和 update Supervisor。
3. 從repo中獲取這個專案,並開啟一個Pull 請求來新增一個新的celery任務。
編碼快樂!