Celery - 最佳實踐

原文作者:Deni Bertovic
原文日期:2014-06-18
原文鏈接:Celery - Best Practices

前言

翻譯純屬個人喜愛和收藏,分享以讓更多的人閱讀受益。原文雖然5-6年前寫作,但至今任然適用。爲了更符合中文閱讀習慣,我把部分段落進行了分解或合併。後續我將在原文基礎之上,基於實踐適當添加更多內容。

序言

如果你在從事Django某些方面的工作,那麼你可能有些後臺長時運行任務處理的需求。可能你已經使用了某種任務隊列,而Celery是Python(以及Django)界(還有更多其他的)處理這種事務當前最流行的項目。

在從事幾個用Celery作爲任務隊列的項目中,我收集了幾個最佳實踐並決定記錄下來。不過,這不僅僅是我認爲正確使用的方式,還有些Celery生態提供但並未充分利用的特性。

一、不使用數據庫作爲AMQP Broker

讓我解釋下使用數據庫作爲隊列後端爲什麼不對(除了Celery官網文檔支出的侷限之外)。數據庫並非爲了像消息隊列軟件如RabbitMQ所設計那樣去處理事務,某個時刻它可能在並非那麼多流量或者用戶的生成環境中崩潰。我猜大家使用數據庫最常用的理由是,既然網頁程序以及有一個了,那爲什麼不復用呢,配置簡單而且不用擔心另一個組件(比如RabbitMQ)。

例如一個實際的場景:假設你有4個處理放置於數據庫的後臺任務的worker,這就意味着有4個進程頻繁地從數據庫拉取新任務,先不說每個進程可以有多併發進程。在某個時刻你發現任務處理出現延遲,任務新增量超過處理完成量。自然而然的你會增加worker數量,但是數據庫因爲多worker的請求而變慢,磁盤IO到達極限,網頁程序從而受到影響而變慢,因爲woker基本上就是在DDOS攻擊數據庫。

如果你有個恰當的AMQP比如RabbitMQ這種情況就不會發生。一個,隊列存儲在內存不涉及磁盤;二個,worker不需依託於請求數據庫因爲隊列有方式推送新任務給worker,即便AMQP因爲其他原因而過載,至少它也不會拖慢面向用戶的前端程序。

所以我說即便是開發環境也不要用數據庫作爲broker,更何況有Docker及非常多開箱即用的容器鏡像可以運行RabbitMQ。

二、使用多個隊列(不僅僅默認的隊列)

Celery配置相當簡單,自帶一個默認隊列放置所有的任務,除非你區分開。常見情況就像這樣:

@app.task()
def my_taskA(a, b, c):
    print("doing something here...")

@app.task()
def my_taskB(x, y):
    print("doing something here...")

如此兩個任務都在同一個隊列(如果沒有在celeryconfig.py文件中單獨指定)。我一定能聽見這樣的聲音因爲一個裝飾器就能跑好後臺任務。我的顧慮是taskA和taskB可能在做完全不同的事,並且可能其中一個甚至重要得多,那麼爲什麼把它們都放一個籃子裏呢?即便你只有一個worker同時處理這兩個任務,假設不重要的任務taskB在某個時刻數量總到以至於taskA都獲取不到資源怎麼辦?此刻增加worker數量可能能解決問題,但是worker還是得處理兩個任務,taskB數量太大同樣會導致taskA獲取不到應有的資源。這就引申到下一點。

三、使用特定worker

解決上述問題的方式就是把taskA單獨放到一個隊列,taskB放另一個隊列,並且分配x個worker來處理Q1,其他的worker都給任務密集的Q2。如此你依然可以保證taskB一直有足夠多的worker來處理,同時有幾個指定的worker來處理taskA,不至於讓它等太久。

所以,手工定義好你的隊列:

CELERY_QUEUES = (
    Queue('default', Exchange('default'), routing_key='default'),
    Queue('for_task_A', Exchange('for_task_A'), routing_key='for_task_A'),
    Queue('for_task_B', Exchange('for_task_B'), routing_key='for_task_B'),
)

還有決定什麼任務分配到哪裏的路由

CELERY_ROUTES = {
    'my_taskA': {'queue': 'for_task_A', 'routing_key': 'for_task_A'},
    'my_taskB': {'queue': 'for_task_B', 'routing_key': 'for_task_B'},
}

他們將允許你爲不同任務運行worker:

celery worker -E -l INFO -n workerA -Q for_task_A
celery worker -E -l INFO -n workerB -Q for_task_B

四、使用Celery的錯誤處理機制

我看到業界大多數任務幾乎都沒有錯誤處理意識,如果任務失敗那就失敗。在一些使用場景可能沒問題,但是我看到的大多數任務都會調用三方API因爲網絡錯誤或者其他"資源不可用"而出錯。處理這樣的問題最簡單方式就是重跑任務,因爲也許三方API只是有服務器/網絡錯誤但可以馬上恢復,爲什麼不這麼做呢?

@app.task(bind=True, default_retry_delay=300, max_retries=5)
def my_task_A():
    try:
        print("doing stuff here...")
    except SomeNetworkException as e:
        print("maybe do some clenup here....")
        self.retry(e)

而我更傾向於爲每個任務定義默認重試等待時間,以及最終放棄的最大重試次數(分別對應參數default_retry_delaymax_retries)。我認爲這是錯誤處理的最基本的方式但卻幾乎沒看到使用。當前Celery還提供了更多的錯誤處理方法,大家可以參考官方文檔。

五、使用Flower

Flower項目是個監控Celery任務和worker非常好的工具,它基於網頁可以查看任務進度、細節和worker狀態,帶起更多worker等。可以點擊前面鏈接查看它的全部功能清單。

六、除非真的需要才保存狀態結果

任務狀態是任務成功或者失敗的退出信息,對後續的某種統計有用。關鍵是退出狀態並非任務執行內容的結果,這些結果作爲某種邊際效應更多的是更新數據庫(比如更好友列表)。

我見過的大部分項目都不在乎任務完成後狀態的持續跟蹤,但大部分要麼用默認的sqllite來保存這些東西,更甚者還用了常規的數據庫(PostgreSQL或者其他的)。

爲什麼毫無理由的去消耗網頁程序的數據庫呢?使用celeryconfig.py文件中的CELERY_IGNORE_RESULT = True來規避存儲任務狀態結果吧。

七、不要傳遞數據庫ORM給任務

在本地的一個Python小聚會上分享之後,一些朋友建議添加這條到清單中。什麼意思呢,就是不要傳遞數據庫對象(比如用戶模型)給後臺任務,因爲序列化對象可能包含舊數據。你需要做的就是給任務傳遞用戶ID然後讓任務去數據庫獲取新的用戶對象數據。

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