Celery 進階使用

IT之一小佬發表於2020-12-29

Celery 進階使用

在應用程式使用

我的專案結構:

celery.py

from __future__ import absolute_import, unicode_literals
from celery import Celery

app = Celery("celery_official", broker='redis://127.0.0.1/0', backend='redis://127.0.0.1/1', include=[
    'task'
])

app.conf.update(
    result_expires=3600,
)
if __name__ == '__main__':
    app.start()

在此程式中,建立了 Celery 例項,如果需要使用 Celery,匯入即可。

  • 該 broker 引數為指定的中間人(Broker)URL。

  • 該 backend 引數為指定的結果後端 URL。

    它主要用於跟蹤任務的狀態的資訊,預設情況下禁用結果後端,在此例項中已經開啟了該功能,主要便於後續檢索,可能在會在程式中使用不同的結果後端。每一個後端都有不同的優點和缺點,如果不需要結果,最好禁用。也可以通過設定 @task(ignore_result=True) 引數,針對單個任務禁用。

  • 該 include 引數是程式啟動時倒入的模組列表,可以該處新增任務模組,便於職程能夠對應相應的任務。

tasks.py

from __future__ import absolute_import, unicode_literals
from celery_tasks.celery import app


@app.task
def add(x, y):
    return x + y

@app.task
def mul(x, y):
    return x * y

@app.task
def xsum(numbers):
    return sum(numbers)

執行職程(Worker)

Celery 程式可以用於啟動職程(Worker):

$ celery -A proj worker -l info

當職程(Worker)開始執行時,可以看到一部分日誌訊息:

 -------------- celery@10-MGCJNCZCVYVA v4.4.6 (cliffs)
--- ***** -----
-- ******* ---- Windows-10-10.0.18362-SP0 2020-12-29 14:32:40
- *** --- * ---
- ** ---------- [config]
- ** ---------- .> app:         celery_tasks1:0x1e9d5cfc9e8
- ** ---------- .> transport:   redis://127.0.0.1:6379/0
- ** ---------- .> results:     redis://127.0.0.1/1
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
 -------------- [queues]
                .> celery           exchange=celery(direct) key=celery


[tasks]
  . celery_tasks.tasks.add
  . celery_tasks.tasks.mul
  . celery_tasks.tasks.xsum

[2020-12-29 14:32:40,614: INFO/MainProcess] Connected to redis://127.0.0.1:6379/0
[2020-12-29 14:32:40,630: INFO/MainProcess] mingle: searching for neighbors
[2020-12-29 14:32:41,458: INFO/SpawnPoolWorker-4] child process 472 calling self.run()
[2020-12-29 14:32:41,489: INFO/SpawnPoolWorker-1] child process 480 calling self.run()
[2020-12-29 14:32:41,505: INFO/SpawnPoolWorker-3] child process 19324 calling self.run()
[2020-12-29 14:32:41,536: INFO/SpawnPoolWorker-2] child process 22296 calling self.run()
[2020-12-29 14:32:41,692: INFO/MainProcess] mingle: all alone
[2020-12-29 14:32:41,724: INFO/MainProcess] celery@10-MGCJNCZCVYVA ready.
  • broker 為 Celery 程式中指定的中間人(Broker)的連線URL,也可以通過 -b 選項在命令列進行設定其他的中間人(Broker)。

  • concurrency 為同時處理任務的工作程式數量,所有的程式都被佔滿時,新的任務需要進行等待其中的一個程式完成任務才能執行進行任務。

        預設的併發數為當前計算機的 CPU 數,可以通過設定 celery worker-c 項進行自定義設定併發數。沒有推薦的併發數,因為最佳的併發數取決於很多因素,如果任務主要是 I/O 限制,可以進行增加併發數,經過測試,設定超過兩倍的 CPU 數量效果不是很好,很有可能會降低效能。

        包括預設的 prefork 池,Celery 也支援在單個執行緒中使用 Eventlet、Gevent。

  • Events 選項設定為啟用狀態時, Celery 會開啟監控事件來進行監視 職程(Worker)。一般情況用於監控程式,如 Flower 和 實時 Celery 監控。

  • Queues 為職程(Worker)任務佇列,可以告訴職程(Worker)同時從多個任務佇列中進行消費。通常用於將任務訊息路由到特定的職程(Worker)、提升服務質量、關注點分離、優先順序排序的常用手段。

可以通過 --help 引數進行檢視完整的使用列表:

celery worker --help

 

停止職程(Worker)

使用 Ctrl + c 就可以停止職程(Worker)

 

後臺執行

在生產環境中,如果需要後臺執行職程(Worker),可以參閱 守護程式:Daemonization

可以使用 celery multi 命令在後臺啟動一個或多個職程(Worker):

$ celery multi start w1 -A proj -l info

celery multi v4.0.0 (latentcall)

> Starting nodes...

> w1.halcyon.local: OK

也可以進行重啟:

$ celery multi restart w1 -A proj -l info

celery multi v4.0.0 (latentcall)

> Stopping nodes...

> w1.halcyon.local: TERM -> 64024

> Waiting for 1 node.....

> w1.halcyon.local: OK

> Restarting node w1.halcyon.local: OK

celery multi v4.0.0 (latentcall)

> Stopping nodes...

> w1.halcyon.local: TERM -> 64052

停止執行:

$ celery multi stop w1 -A proj -l info

stop 命令是非同步的,所以不會等待職程(Worker)關閉。可以通過 stopwait 命令進行停止執行,可以保證在退出之前完成當前正在執行的任務:

$ celery multi stopwait w1 -A proj -l info

 

關於 --app 引數

使用 --app 引數可也指定執行的 Celery 應用程式例項,格式必須為 module.path:attribute

但如果只設定包名,它將進行搜尋app例項,順序如下:

用 --app=celery_tasks:

  1. 名為 celery_tasks.app 的屬性

  2. 名為 celery_tasks.celery 的屬性

  3. 模組 celery_tasks中值為 Celery 應用程式的任何屬性,如果還沒有找到,將嘗試檢索名為 celery_tasks.celery的子模組

  4. 名為 celery_tasks.celery.app 的屬性

  5. 名為 celery_tasks.celery.celery 的屬性

  6. 模組 celery_tasks.celery 中值為 Celery 應用程式的任何屬性

  7. 在此方案模仿文件中使用的例項,即 針對單個模組包含的celery_tasks:app ,以及 大型專案的 celery_tasks.celery:app

程式呼叫

你可以使用 delay() 方法進行呼叫:

>>> add.delay(2, 2)

delay() 實際上為 apply_async() 的快捷使用:

>>> add.apply_async((2, 2))

apply_async() 可以指定呼叫時執行的引數,例如執行的時間,使用的任務佇列等:

>>> add.apply_async((2, 2), queue='lopri', countdown=10)

上面的例項中,任務被下發到 lopri 佇列中,任務下發之後會在最早10秒內執行。 直接呼叫任務函式進行執行任務,不會傳送任何任務訊息:

>>> add(2, 2)

4

delay() apply_async() 以及 apply(__call__) 為 Celery 呼叫的API,也可以用於簽名。

每一個任務被呼叫時會賦值一個的任務ID(UUIID)。

delay()apply_async() 方法會返回一個 AsyncResult 例項,可以用於進行跟蹤任務狀況。如果進行跟蹤任務狀態,需要設定一個結果後端,以便於儲存。

預設情況下禁用結果,因為沒有一個適合所有應用程式的結果後端,對於大量的任務來說,儲存返回內容不是非常有用的,所以該預設值是一個比較合理的。另外,結果後端不是用於監控任務以及職程(Worker),Celery 有專用的事物訊息來進行監控。

如果配置了結果後端,可以獲取任務的返回值:

>>> res = add.delay(2, 2)

>>> res.get(timeout=1)

4

也可以通過 id 屬性進行獲取任務的ID:

>>> res.id

d6b3aea2-fb9b-4ebc-8da4-848818db9114

如果任務執行引發異常,可以進行檢查異常以及溯源,預設情況下 result.get() 會丟擲異常:

>>> res = add.delay(2)

>>> res.get(timeout=1)


Traceback (most recent call last):

File "<stdin>", line 1, in <module>

File "/opt/devel/celery/celery/result.py", line 113, in get

interval=interval)

File "/opt/devel/celery/celery/backends/rpc.py", line 138, in wait_for

raise meta['result']

TypeError: add() takes exactly 2 arguments (1 given)

如果不希望 Celery 丟擲異常,可以通過設定 propagate 來進行禁用:

>>> res.get(propagate=False)

TypeError('add() takes exactly 2 arguments (1 given)',)

在這種情況下,他可以返回引發錯誤的例項,需要檢查任務是否執行成功還是失敗,可以通過在結果例項中使用對應的方法:

>>> res.failed()

True

>>> res.successful()

False

如何知道任務是否執行失敗?可以通過檢視任務的 state 進行檢視:

>>> res.state

'FAILURE'

一個任務只能有當前只能有一個狀態,但他的執行過程可以為多個狀態,一個典型的階段是:

PENDING -> STARTED -> SUCCESS

啟動狀態是一種比較特殊的狀態,僅在 task_track_started 啟用設定或 @task(track_started=True)的情況下才會進行記錄。 掛起狀態實際上不是記錄狀態,而是未知任務ID的預設狀態,可以從此例項中看到:

>>> from proj.celery import app

>>> res = app.AsyncResult('this-id-does-not-exist')

>>> res.state

'PENDING'

重試任務比較複雜,為了證明,一個任務會重試兩次,任務的階段為:

PENDING -> STARTED -> RETRY -> STARTED -> RETRY -> STARTED -> SUCCESS

 

Canvas:設計工作流程

        通過上面的例項學了使用 delay 方法進行呼叫任務,有時候可能希望將任務呼叫的簽名傳遞給另外一個程式或其他函式的引數,Celery 提供了一共交簽名的東西。

        簽名通過一種方式進行封裝任務呼叫的引數以及執行選項,便於傳遞給他的函式,甚至通過序列化通過網路傳送。

        可以將 add 使用的引數作為任務建立的簽名,倒數計時為 10 秒,如下所示(2,2):

add.signature((2,2),countdown=10)
celery_tasks.tasks.add(2, 2)

也可以通過一個快捷的方式進行操作:

add.s(2,2)
celery_tasks.tasks.add(2, 2)

再次呼叫API ...

簽名例項支援呼叫API:這就意味著可以使用 delayapply_async 方法。 但區別就在於簽名例項已經指定了引數簽名,該 add 任務有兩個引數,需要指定兩個引數的簽名才能夠成一個完整的簽名例項:

>>> s1 = add.s(2, 2)

>>> res = s1.delay()

>>> res.get()

4

也可以建立不完整的簽名來進行建立,我稱之為 partials 的內容:

# incomplete partial: add(?, 2)

>>> s2 = add.s(2)

s2 為一個不完整的簽名,需要另外一個引數,可以通過呼叫簽名解決:

# resolves the partial: add(8, 2)

>>> res = s2.delay(8)

>>> res.get()

10

在這裡,設定了設定了引數值為 8,它位於引數值為 2 的簽名,形成了完整的 add(8,2) 簽名。 也可以設定新的參值,新設定的引數會覆蓋原有的引數值:

>>> s3 = add.s(2, 2, debug=True)

>>> s3.delay(debug=False) # debug is now False.

如上所述,簽名支援呼叫API:

  • sig.apply_async(args=(), kwargs={}, **options)

    使用可選的部分引數和部分關鍵字引數呼叫簽名以及支援部分執行選項。

  • sig.delay(_args, *_kwargs)

    快捷版本的 apply_async ,任何引數都將作為簽名中的引數,關鍵字引數將與任何現有鍵合併。

    這些看起來比較有用,但是可以用來做什麼?為了解決這個問題,就需要介紹 canvas 原語.....

原語

這些原語本身就是簽名物件,可以通過任何進行組合,形成複雜的工作流。

讓我們一起來看一些例子:

組:Groups

一個 group 並行呼叫任務列表,返回一個特殊的結果例項,可以將結果作為一個列表進行檢視,並且通過索引進去獲取返回值。

from celery import group
from celery_tasks.tasks import add
group(add.s(i,i) for i in range(10))().get()
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Partial group

g = group(add.s(i) for i in range(10))
g(10).get()
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

鏈:Chains

可以將任務連結在一起,在一個人返回後進行呼叫另外一個任務:

from celery import chain
from celery_tasks.tasks import add, mul
# (4 + 4) * 8
chain(add.s(4, 4) | mul.s(8))().get()
64

或 partial chain

>>> # (? + 4) * 8
g = chain(add.s(4) | mul.s(8))
g(4).get()
64

鏈也可以這樣寫:

>>> (add.s(4, 4) | mul.s(8))().get()
64

和絃:Chords

和絃是一個帶有回撥的組:

>>> from celery import chord

>>> from proj.tasks import add, xsum

>>> chord((add.s(i, i) for i in xrange(10)), xsum.s())().get()

90

連結到其他任務的組將自動轉換為和絃:

>>> (group(add.s(i, i) for i in xrange(10)) | xsum.s())().get()

90

這些原語都是簽名的型別,可以根據需要進行組合,例如:

>>> upload_document.s(file) | group(apply_filter.s() for filter in filters)

 

路由

Celery 支援 AMQP 中提供的所有路由,可以將訊息傳送到指定的任務佇列路由。

通過 task_routes 可以設定一個按名稱分配的路由任務佇列,將所有的內容集中存放在一個位置:

app.conf.update(

task_routes = {

    'proj.tasks.add': {'queue': 'hipri'},

},

)

可以在程式是使用 queue 引數進行指定佇列:

>>> from proj.tasks import add

>>> add.apply_async((2, 2), queue='hipri')

可以通過設定執行職程(Worker)時指定職程(Worker)從某個佇列中進行消費(celery worker -Q):

$ celery -A proj worker -Q hipri

也可以通過“,”作為分割符進行設定多個佇列,例如,可以將預設佇列和 hipri 佇列一起通過職程(Worker)進行消費,其中預設佇列 celery 由於歷史原因被命名:

$ celery -A proj worker -Q hipri,celery

佇列名稱的順序不分前後,職程(Worker)給予佇列分配的權重是相同的。

執行效果截圖:

 

遠端控制

使用 RabbitMQ(AMQP)、Redis 或 Qpid 作為中間人(Broker),可以在執行時控制和檢查職程(Worker)。 例如,當前職程(Worker)正在處理的任務:

$ celery -A proj inspect active

這是通過廣播訊息實現的,叢集中所有職程(Worker)都會所有收到遠端控制發出的指令。 也可以通過 --destination 選項指定一個或多個職程(Worker)進行操作,使用“,”進行分割職程(Worker)主機列表:

$ celery -A proj inspect active --destination=celery@example.com

如果沒有提供目的地,那麼每個工作人員都將採取行動並回復請求。

celery inspect 命令不能修改程式,只能進行檢視職程(Worker)概況以及統計資訊,可以通過 help 進行檢視:

$ celery -A proj inspect --help

celery control 命令可以檢視實際上改變了工作在執行時的狀況:

$ celery -A proj control --help

例如,可以強制職程(Worker)啟用事件訊息(用於監控任務以及職程(Worker)):

$ celery -A proj control enable_events

啟動事件後,可以啟動事件轉儲程式,進行檢視職程(Worker)目前執行的狀況:

$ celery -A proj events --dump

或者可以啟動 curses 介面:

$ celery -A proj events

當監控完畢之後,可以禁用事件:

$ celery -A proj control disable_events

celery status 命令可以遠端控制並且顯示叢集中職程(Worker)的列表:

$ celery -A proj status

 

時區

        內部和訊息中的所有的時間和日期使用的都是 UTC 時區。 當職程(Worker)收到訊息時,例如倒數計時設定,會將 UTC 時間轉換為本地時間。如果需要使用與系統不同的時區,可以通過 timezone進行配置:

app.conf.timezone = 'Europe/London'

優化

        預設情況下,預設的配置項沒有針對吞吐量進行優化,預設的配置比較合適大量短任務和比較少的長任務。 如果使用的中間人是 RabbitMQ,可以將換成 librabbitmq 模組(通過 C 語言實現的AMQP客戶端):

$ pip install librabbitmq

 

 

相關文章