celery實戰要點

作爲一個Celery使用重度用戶,看到Celery Best Practices這篇文章,不由得菊花一緊。乾脆翻譯出來,同時也會加入我們項目中celery的實戰經驗。

至於Celery爲何物,看這裏Celery

通常在使用Django的時候,你可能需要執行一些長時間的後臺任務,沒準你可能需要使用一些能排序的任務隊列,那麼Celery將會是一個非常好的選擇。

當把Celery作爲一個任務隊列用於很多項目中後,作者積累了一些最佳實踐方式,譬如如何用合適的方式使用Celery,以及一些Celery提供的但是還未充分使用的特性。

1,不要使用數據庫作爲你的AMQP Broker

數據庫並不是天生設計成能用於AMQP broker的,在生產環境下,它很有可能在某時候當機(PS,當掉這點我覺得任何系統都不能保證不當吧!!!)。

作者猜想爲啥很多人使用數據庫作爲broker主要是因爲他們已經有一個數據庫用來給web app提供數據存儲了,於是乾脆直接拿來使用,設置成Celery的broker是很容易的,並且不需要再安裝其他組件(譬如RabbitMQ)。

假設有如下場景:你有4個後端workers去獲取並處理放入到數據庫裏面的任務,這意味着你有4個進程爲了獲取最新任務,需要頻繁地去輪詢數據庫,沒準每個worker同時還有多個自己的併發線程在幹這事情。

某一天,你發現因爲太多的任務產生,4個worker不夠用了,處理任務的速度已經大大落後於生產任務的速度,於是你不停去增加worker的數量。突然,你的數據庫因爲大量進程輪詢任務而變得響應緩慢,磁盤IO一直處於高峯值狀態,你的web應用也開始受到影響。這一切,都因爲workers在不停地對數據庫進行DDOS。

而當你使用一個合適的AMQP(譬如RabbitMQ)的時候,這一切都不會發生,以RabbitMQ爲例,首先,它將任務隊列放到內存裏面,你不需要去訪問硬盤。其次,consumers(也就是上面的worker)並不需要頻繁地去輪詢因爲RabbitMQ能將新的任務推送給consumers。當然,如果RabbitMQ真出現問題了,至少也不會影響到你的web應用。

這也就是作者說的不用數據庫作爲broker的原因,而且很多地方都提供了編譯好的RabbitMQ鏡像,你都能直接使用,譬如這些

對於這點,我是深表贊同的。我們系統大量使用Celery處理異步任務,大概平均一天幾百萬的異步任務,以前我們使用的mysql,然後總會出現任務處理延時太嚴重的問題,即使增加了worker也不管用。於是我們使用了redis,性能提升了很多。至於爲啥使用mysql很慢,我們沒去深究,沒準也還真出現了DDOS的問題。

2,使用更多的queue(不要只用默認的)

Celery非常容易設置,通常它會使用默認的queue用來存放任務(除非你顯示指定其他queue)。通常寫法如下:


 
  1. @app.task()

  2. def my_taskA(a, b, c):

  3. print("doing something here...")

  4.  
  5. @app.task()

  6. def my_taskB(x, y):

  7. print("doing something here...")

這兩個任務都會在同一個queue裏面執行,這樣寫其實很有吸引力的,因爲你只需要使用一個decorator就能實現一個異步任務。作者關心的是taskA和taskB沒準是完全兩個不同的東西,或者一個可能比另一個更加重要,那麼爲什麼要把它們放到一個籃子裏面呢?(雞蛋都不能放到一個籃子裏面,是吧!)沒準taskB其實不怎麼重要,但是量太多,以至於重要的taskA反而不能快速地被worker進行處理。增加workers也解決不了這個問題,因爲taskA和taskB仍然在一個queue裏面執行。

3,使用具有優先級的workers

爲了解決2裏面出現的問題,我們需要讓taskA在一個隊列Q1,而taskB在另一個隊列Q2執行。同時指定x workers去處理隊列Q1的任務,然後使用其它的workers去處理隊列Q2的任務。使用這種方式,taskB能夠獲得足夠的workers去處理,同時一些優先級workers也能很好地處理taskA而不需要進行長時間的等待。

首先手動定義queue


 
  1. CELERY_QUEUES = (

  2. Queue('default', Exchange('default'), routing_key='default'),

  3. Queue('for_task_A', Exchange('for_task_A'), routing_key='for_task_A'),

  4. Queue('for_task_B', Exchange('for_task_B'), routing_key='for_task_B'),

  5. )

然後定義routes用來決定不同的任務去哪一個queue


 
  1. CELERY_ROUTES = {

  2. 'my_taskA': {'queue': 'for_task_A', 'routing_key': 'for_task_A'},

  3. 'my_taskB': {'queue': 'for_task_B', 'routing_key': 'for_task_B'},

  4. }

最後再爲每個task啓動不同的workerscelery worker -E -l INFO -n workerA -Q for_task_A celery worker -E -l INFO -n workerB -Q for_task_B

在我們項目中,會涉及到大量文件轉換問題,有大量小於1mb的文件轉換,同時也有少量將近20mb的文件轉換,小文件轉換的優先級是最高的,同時不用佔用很多時間,但大文件的轉換很耗時。如果將轉換任務放到一個隊列裏面,那麼很有可能因爲出現轉換大文件,導致耗時太嚴重造成小文件轉換延時的問題。

所以我們按照文件大小設置了3個優先隊列,並且每個隊列設置了不同的workers,很好地解決了我們文件轉換的問題。

4,使用Celery的錯誤處理機制

大多數任務並沒有使用錯誤處理,如果任務失敗,那就失敗了。在一些情況下這很不錯,但是作者見到的多數失敗任務都是去調用第三方API然後出現了網絡錯誤,或者資源不可用這些錯誤,而對於這些錯誤,最簡單的方式就是重試一下,也許就是第三方API臨時服務或者網絡出現問題,沒準馬上就好了,那麼爲什麼不試着重試一下呢?


 
  1. @app.task(bind=True, default_retry_delay=300, max_retries=5)

  2. def my_task_A():

  3. try:

  4. print("doing stuff here...")

  5. except SomeNetworkException as e:

  6. print("maybe do some clenup here....")

  7. self.retry(e)

作者喜歡給每一個任務定義一個等待多久重試的時間,以及最大的重試次數。當然還有更詳細的參數設置,自己看文檔去。

對於錯誤處理,我們因爲使用場景特殊,例如一個文件轉換失敗,那麼無論多少次重試都會失敗,所以沒有加入重試機制。

5,使用Flower

Flower是一個非常強大的工具,用來監控celery的tasks和works。

這玩意我們也沒怎麼使用,因爲多數時候我們都是直接連接redis去查看celery相關情況了。貌似挺傻逼的對不,尤其是celery在redis裏面存放的數據並不能方便的取出來。

6,沒事別太關注任務退出狀態

一個任務狀態就是該任務結束的時候成功還是失敗信息,沒準在一些統計場合,這很有用。但我們需要知道,任務退出的狀態並不是該任務執行的結果,該任務執行的一些結果因爲會對程序有影響,通常會被寫入數據庫(例如更新一個用戶的朋友列表)。

作者見過的多數項目都將任務結束的狀態存放到sqlite或者自己的數據庫,但是存這些真有必要嗎,沒準可能影響到你的web服務的,所以作者通常設置CELERY_IGNORE_RESULT = True去丟棄。

對於我們來說,因爲是異步任務,知道任務執行完成之後的狀態真沒啥用,所以果斷丟棄。

7,不要給任務傳遞 Database/ORM 對象

這個其實就是不要傳遞Database對象(例如一個用戶的實例)給任務,因爲沒準序列化之後的數據已經是過期的數據了。所以最好還是直接傳遞一個user id,然後在任務執行的時候實時的從數據庫獲取。

對於這個,我們也是如此,給任務只傳遞相關id數據,譬如文件轉換的時候,我們只會傳遞文件的id,而其它文件信息的獲取我們都是直接通過該id從數據庫裏面取得。

最後

後面就是我們自己的感觸了,上面作者提到的Celery的使用,真的可以算是很好地實踐方式,至少現在我們的Celery沒出過太大的問題,當然小坑還是有的。至於RabbitMQ,這玩意我們是真沒用過,效果怎麼樣不知道,至少比mysql好用吧。

最後,附上作者的一個Celery Talk https://denibertovic.com/talks/celery-best-practices/

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