如果引入調(diào)度,周期性執(zhí)行或者不阻塞請求線程,很多Django應(yīng)用可以得到更好的使用。
在Django應(yīng)用里,有很多方式可以用來調(diào)度任務(wù),但是使用Celery有許多優(yōu)勢。它有很好的支持,伸縮性好,并且能和Django一起工作。它應(yīng)用廣泛,有很多資源可以幫助學(xué)習(xí)和使用它。一旦學(xué)會了,對其他項(xiàng)目也有好處。
Celery 3.0.x
本文檔適用于Celery 3.0.x。之前或之后的版本可能有差別。
Celery簡介
Celery被用來稍后執(zhí)行某些代碼,或用調(diào)度器調(diào)度這些代碼
這有什么用呢?下面有一些例子。
第一個例子,假設(shè)用戶發(fā)來一個頁面請求,然后等待請求完成,瀏覽器加載新頁面。對于他們的請求,你需要運(yùn)行一些代碼,而這些代碼的運(yùn)行時間可能比用戶想要等待網(wǎng)頁的時間要長,但是你并不真得需要在響應(yīng)頁面請求前執(zhí)行那些代碼。這時,你如果使用Celery稍后執(zhí)行這些耗時的代碼,就立即響應(yīng)頁面請求。
當(dāng)你需要連接遠(yuǎn)程服務(wù)器來處理請求時會經(jīng)常遇到這種情況。你的應(yīng)用不能控制遠(yuǎn)程服務(wù)器響應(yīng)的時間,甚至遠(yuǎn)程服務(wù)器可能是關(guān)閉的。
另一種常見情形是需要周期性執(zhí)行某些代碼。比如,你可能每小時查詢最新的天氣預(yù)報并且儲存數(shù)據(jù)。你可以寫個任務(wù)來執(zhí)行這項(xiàng)工作,然后設(shè)置Celery每個小時執(zhí)行一次。這個任務(wù)運(yùn)行并把數(shù)據(jù)存入數(shù)據(jù)庫,然后Web應(yīng)用就可以獲得最新的天氣預(yù)報。
一個任務(wù)(task)只是一個Python函數(shù)。你可以把調(diào)度一個任務(wù)理解為延時調(diào)用哪個函數(shù)。比如,你可以需要 Celery 5分鐘后使用參數(shù)(1, 3, 3)調(diào)用你的函數(shù)task1,或者你可以使函數(shù)batchjob每晚0時執(zhí)行。
我們將會設(shè)置Celery來使你的任務(wù)在和其余應(yīng)用代碼盡可能相似的環(huán)境中執(zhí)行,以使它們使用同樣的數(shù)據(jù)庫和Django設(shè)置。有一些差異需要記住,稍后將會講到。
當(dāng)一個任務(wù)準(zhǔn)備執(zhí)行時,Celery將它放到一個隊(duì)列上,這個隊(duì)列上有其它的將要執(zhí)行的任務(wù)。你可以有很多隊(duì)列,但是這里為了簡單我們假設(shè)只有一個隊(duì)列。
可以這樣說,將一個任務(wù)添加到隊(duì)列上僅僅只是把它加入到一個待辦列表。為了執(zhí)行任務(wù),一些其它的程序--任務(wù)執(zhí)行單元(worker)--將監(jiān)視隊(duì)列上是否有任務(wù)。當(dāng)它發(fā)現(xiàn)隊(duì)列上有任務(wù)時,它會取出并執(zhí)行第一個任務(wù),然后接著監(jiān)視隊(duì)列等待其它任務(wù)。你可以有很多任務(wù)執(zhí)行單元,可能在許多不同的服務(wù)器上,但是我們今天假設(shè)只有一個任務(wù)執(zhí)行單元。
稍后我們會講更多關(guān)于隊(duì)列,任務(wù)執(zhí)行單元和其他尚未提及的重要的程序,上面這些現(xiàn)在已經(jīng)夠用了,讓我們做一些工作。
本地安裝Cerely
安裝本地Django使用的Celery很簡單--只需安裝django-celery:
為Celery配置Django
首先,我們配置Celery在runserver上使用。對于Celery中間件(broker,稍后介紹),將使用Django database broker implementation。現(xiàn)在,你只需知道Celery需要一個中間件,在開發(fā)過程中可以使用Django自帶的(但是在生產(chǎn)環(huán)境中你必須使用一些更健壯的性能更好的)
在Django settings.py文件中:
1.添加這些代碼:
注意:絕對不要在生產(chǎn)環(huán)境中使用Django中間件。在本教程中我們使用它只是為了節(jié)省時間。在生產(chǎn)環(huán)境中,你可以使用RabbitMQ或者Redis。
2.在INSTALLED_APPS中添加djcelery和kombu.transport.django:
3.創(chuàng)建Celery數(shù)據(jù)庫,如果使用South作模式遷移:
否則:
如前所述,一個任務(wù)可以僅僅只是一個Python函數(shù)。但是,Celery必須了解它。當(dāng)Celery和Django一起工作時,這很容易。只需要在你的應(yīng)用中添加一個tasks.py文件,把你的任務(wù)放在那個文件中,并且裝飾它們。這里有一個簡單的tasks.py:
將一個函數(shù)標(biāo)記為任務(wù)并不影響它正常工作。你仍可以如此調(diào)用它:z = add(1, 2)并且它會和以前一樣工作。將它標(biāo)記為任務(wù)可以讓你用其它方式調(diào)用。
調(diào)度任務(wù)
接著上面的簡單例子。我們想立即運(yùn)行任務(wù),并且不想它阻塞當(dāng)前線程。只需通過在任務(wù)的名字后面添加 .delay 即可實(shí)現(xiàn):
這很重要,你的任務(wù)總是導(dǎo)入并且引用相同的包名稱。比如,依賴于你的Python路徑如何設(shè)置,可能這樣指向它myproject.myapp.tasks.add或者myapp.tasks.add?;蛘邚膍yapp.views,你可能這樣導(dǎo)入它 .tasks.add。但是Celery無法知道這些都是同一個任務(wù)。
djcelery.setup_loader()將會使用你的應(yīng)用在INSTALLED_APPS里的包名,加上 .tasks.functionname。確保當(dāng)你調(diào)度你的任務(wù),你也使用同樣的名字導(dǎo)入它,否則可能會出現(xiàn)bugs。
測試
啟動一個任務(wù)執(zhí)行單元
正如我們提過的,一個單獨(dú)的程序,任務(wù)執(zhí)行單元,用來執(zhí)行你的Celery任務(wù)。下面是我們?nèi)绾螁右粋€任務(wù)執(zhí)行單元滿足開發(fā)需要。
首先,打開一個新終端或者新窗口。在終端里,設(shè)置相同的Django開發(fā)環(huán)境--啟動你的虛擬環(huán)境或者把它們添加到你的Python路徑里,這兩種方法都可以使你使用runserver運(yùn)行你的項(xiàng)目。
現(xiàn)在,你可以在那個終端里這樣啟動一個任務(wù)執(zhí)行單元:
任務(wù)執(zhí)行單元將會在那個窗口里運(yùn)行,并且在那里顯示輸出。
運(yùn)行任務(wù)
回到你的第一個窗口,啟動一個Django終端,運(yùn)行你的任務(wù):
前面我們提到過使用Celery來避免對一個頁面請求的響應(yīng)延遲。下面是一個使用這種技術(shù)的簡單的Django事例。
問題處理
嘗試使Celery任務(wù)工作可能會很困難,因?yàn)闀褂煤芏嗖糠?,并且這些部分之間還會相互聯(lián)系。許多常見的小技巧仍然起作用:
先使最簡單的配置能夠工作
使用Python調(diào)試器以及輸出語句來觀察發(fā)生了什么
提升日志級別(比如,將任務(wù)執(zhí)行單元設(shè)置為—loglevel debug)來獲得更多的信息
也有一些Celery專用工具。
Eager scheduling
在你的Django設(shè)置里,可以添加:
然后,Celery就會忽略全部調(diào)度機(jī)制,立即調(diào)用你的代碼。
這就是說,設(shè)置了CELERY_ALWAYS_EAGER = True后,這兩個語句變得一樣:
你可以使用這種方法使你的核心內(nèi)容在引入Celery調(diào)度問題之前開始工作。
查看隊(duì)列
只要你在開發(fā)時使用Django自帶的中間件,你的隊(duì)列就存儲在Django數(shù)據(jù)庫里。這樣你就可以很容易的查看它。在你的應(yīng)用里向admin.py添加幾行:
現(xiàn)在,你可以在/admin/django/message文件夾下查看隊(duì)列上是否有任務(wù)。每一個信息(message)就是一個來自Celery的請求--讓任務(wù)執(zhí)行單元執(zhí)行任務(wù)。信息的內(nèi)容很難理解,但是有時候僅僅只知道你的任務(wù)是否在隊(duì)列里就很有用了。信息會留在數(shù)據(jù)庫里,所以看到很多信息并不意味著你的任務(wù)只在被執(zhí)行。
檢查結(jié)果
任何時候調(diào)度一個任務(wù),Celery都會返回一個AsyncResult對象。你可以保存那個任務(wù),然后稍后使用它檢查任務(wù)是否執(zhí)行完成,是否成功,以及結(jié)果。
另一個常見例子是周期性調(diào)度任務(wù)。Celery使用另一個程序celerybeat來實(shí)現(xiàn)。Celerybeat一直運(yùn)行,等一個調(diào)度任務(wù)到執(zhí)行時間了,celerybeat就會把它加入隊(duì)列去執(zhí)行。
顯而易見,只有一個celerybeat程序可以運(yùn)行(不像任務(wù)執(zhí)行單元,只要你需要,你就可以運(yùn)行任意個)
啟動celerybeat和啟動一個任務(wù)執(zhí)行單元相似。打開另一個窗口,設(shè)置Django環(huán)境,然后:
添加這條配置信息:
現(xiàn)在,你可以打開Django admin文件夾,前往/admin/djcelery/periodictask/文件夾添加計(jì)劃任務(wù)。下面是一些字段的說明:
Name—用來標(biāo)記預(yù)訂的任務(wù)
Task(registered)-只要你在添加任務(wù)后啟動Django至少一次,這里應(yīng)該是任何一個你定義過的任務(wù)。如果你沒有看到你想要的任務(wù),最好檢查原因,修復(fù)它,然后再使用寫一個字段。
Task(custom)-這里你可以鍵入任務(wù)的全稱(例如,myapp.tasks.add),但是最好使用上面registered tasks字段
Enabled-由于某些原因,比如暫停它,你不想要任務(wù)真得運(yùn)行,你可以不選擇它
Interval-如果你想要任務(wù)間隔一段時間重復(fù)運(yùn)行。你可能需要使用綠色的“+”定義一個新的計(jì)劃任務(wù)。這很簡單,例如,每5分鐘運(yùn)行,5設(shè)置為“Every”,“Period”設(shè)置為minutes。
Crontab-如果任務(wù)要在指定時間運(yùn)行,使用crontab替代Interval。使用綠色的“+”,填寫minute, hour, day of week, day of month, and day of year。你可以使用“*”在任何字段替代一個具體值,但是注意-如果你使用“*”在minute字段,你的任務(wù)將會在選定的那個小時內(nèi)的每個分鐘里都執(zhí)行。例如:每天早上7:30執(zhí)行,設(shè)置minute為“30”,hour為“7”,其余字段為“*”。
Arguments-如果需要為任務(wù)傳入?yún)?shù),你可以展開這部分,設(shè)置*args和**kwargs字段。
Execution Oprions-高級設(shè)置,這里我們不會討論。
默認(rèn)計(jì)劃任務(wù)
如果你想你的一些任務(wù)默認(rèn)計(jì)劃任務(wù),不依賴于某人在安裝完你的應(yīng)用后在數(shù)據(jù)庫里配置它們,你可以使用Django fixtures把你的計(jì)劃任務(wù)作為應(yīng)用的初始數(shù)據(jù)。
在你的數(shù)據(jù)庫里配置你想要的計(jì)劃任務(wù)
用json格式保存計(jì)劃任務(wù)
在你的應(yīng)用里創(chuàng)建一個fixtures文件夾
如果你再也不想編輯這些計(jì)劃任務(wù),你可以把你的json文件復(fù)制到fixtures文件夾中命名為initial_data.json。每次syncdb運(yùn)行時,Django都會加載它,如果你在數(shù)據(jù)庫里編輯了計(jì)劃任務(wù),你就會得到錯誤,或者失去更改。(你仍然可以添加新的計(jì)劃任務(wù),你只是不能更改從fixtures文件夾初始數(shù)據(jù)里加載的那些)
如果你想把這些作為初始調(diào)度,把你的文件命名為其他的,當(dāng)配置一個使用你應(yīng)用的網(wǎng)站時加載它就可以了:
提示和技巧
不要把model對象傳遞給任務(wù)
因?yàn)槿蝿?wù)不會立即運(yùn)行,當(dāng)一個任務(wù)運(yùn)行并使用傳入的model對象時,數(shù)據(jù)庫的對應(yīng)紀(jì)錄可能已經(jīng)改變。如果任務(wù)此時對model對象做了一些修改并且保存起來,數(shù)據(jù)庫里的這些改變就會被舊數(shù)據(jù)覆蓋了。
更安全的做法是保存對象,傳遞紀(jì)錄的鍵名,在任務(wù)里重新獲得對象。
執(zhí)行一個任務(wù)的時候調(diào)度另一個完全可行。這可以用于保證第二個任務(wù)在第一個任務(wù)做完一些必要的工作之前不會運(yùn)行。
不要在一個任務(wù)里等待另一個任務(wù)
如果一個任務(wù)等待另一個任務(wù),它的任務(wù)執(zhí)行單元將一直阻塞而不能做其他事情直到等待結(jié)束。這早晚造成死鎖。
如果你的任務(wù)A里想要調(diào)度任務(wù)B,在任務(wù)B結(jié)束之后,做一些其他的工作,最好創(chuàng)建一個任務(wù)C做這些工作,在任務(wù)B結(jié)束時調(diào)度任務(wù)C。
下一步
一旦你理解了這些基礎(chǔ)部分,最好閱讀一下Celery用戶手冊中的部分內(nèi)容。我建議從這些章節(jié)開始;其他的要不和Django沒有關(guān)系,要不太過高級:
在生產(chǎn)環(huán)境里使用Celery
這里描述的Celery配置是為了開發(fā)方便,絕不能應(yīng)用在生產(chǎn)環(huán)境。
為了在生產(chǎn)環(huán)境里使用,最重要的修改是停止使用kombu.transport.djanggo作為中間件,使用RabbitMQ或者其他健壯的可伸縮的等價物替換。
英文原文:https://www.caktusgroup.com/blog/2014/06/23/scheduling-tasks-celery/
譯者:CupKnight