Celery非同步排程框架(一)基本使用

昀溪發表於2019-01-31

介紹

之前部門開發一個專案我們需要實現一個定時任務用於收集每天DUBBO介面、域名以及TOMCAT(核心應用)的訪問量,這個後面的邏輯就是使用定時任務去ES介面抓取資料儲存在資料庫中然後前臺進行展示。

點開以後的詳情

在這個專案中使用的定時任務是python-crontab這個東西,它很簡單但是使用起來有些不方便,雖然程式後來也沒有進行修改,但是還是想看看有沒有更好的定時任務框架,後來就發現了Celery這個專案。下面我們看看Celery的架構,讓大家有個整體認識:

下面先來認識一下它的一些元件以及這些元件或者叫做角色的是幹什麼的

Task:任務(Task)就是你要做的事情,例如一個註冊流程裡面有很多工,給使用者發驗證郵件就是一個任務,這種耗時的任務就可以交給Celery去處理,還有一種任務是定時任務,比如每天定時統計網站的註冊人數,這個也可以交給Celery週期性的處理。我們在tasks.py中寫的就是worker要可以執行的任務。

Broker: 在Celery中這個角色相當於資料結構中的佇列,介於生產者和消費者之間經紀人。例如一個Web系統中,生產者是主程式,它生產任務,將任務傳送給 Broker,消費者是 Worker,是專門用於執行任務的後臺服務。Celery本身不提供佇列服務,一般用Redis或者RabbitMQ來實現佇列服務。

Worker: Worker 就是那個一直在後臺執行任務的人,也成為任務的消費者,它會實時地監控佇列中有沒有任務,如果有就立即取出來執行。

Beat: Beat 是一個定時任務排程器,它會根據配置定時將任務傳送給 Broker,等待 Worker 來消費。

Backend: Backend 用於儲存任務的執行結果,每個任務都有返回值,比如傳送郵件的服務會告訴我們有沒有傳送成功,這個結果就是存在Backend中,當然我們並不總是要關心任務的執行結果。

Exchanges:交換器,用於把不同訊息放到不同的訊息佇列中

Queues:訊息佇列
為什麼需要這兩個worker會監控特定的佇列同時也有一個預設的交換器,通常會有多個worker處理不同任務,那如何區分不同訊息屬於哪個worker處理呢這就需要交換器和佇列。通常不需要指定佇列和交換器因為有一個自動路由功能,如果你需要配置更加複雜的路由就需要使用這兩個。預設的queue/exchange/binding的鍵是celery,exchange的型別是direct

安裝

我的環境是Python3.6

# 安裝celery
pip install celery
# 因為我這裡用到redis做後端所以需要安裝redis,但是需要注意雖然我的celery版本是最新的,但是redis驅動你不能用最新的
# 否則任務執行會失敗
pip install redis==2.10.6
# flower元件不是必須的,它是用來對celery進行監控的
pip install flower

Celery入門

第一個任務

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import time
from celery import Celery

# 第一個引數是當前模組名稱
# celery -A mytasks worker --loglevel=info 透過這個執行的時候-A後面的引數要和模組名稱一致,這樣就啟動了一個worker來執行任務
# broker是任務佇列放在哪裡   backend是任務執行結果放在哪裡
app = Celery("mytasks", broker="redis://172.16.48.171:6379/3", backend="redis://172.16.48.171:6379/3")

# 這個裝飾器讓add變成一個非同步任務
@app.task
def add(x, y):
    return x + y

Celery在被使用之前一定要進行例項化,一個例項叫做一個Application也稱作一個app,就像上面定義的app = Celery()。一個例項化的app是執行緒安全的。注意上面的目錄結構,下面啟動一個worker。-A引數是指定celery物件的位置,也就是celery例項的py檔案,預設它會使用celery.py,如果使用了其他名字比如我們這裡就用了mytasks.py,那麼你就要指定具體的這個檔名稱。下面我們啟動任務

celery -A Chatper01.mytasks worker --loglevel=info

下面看看如何排程任務也就是執行任務

在程式碼中如何執行任務呢?

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from Chatper01.mytasks import add

task_id = add.delay(10, 4)  # 立即返回一個任務ID,它將任務序列化後傳送到你指定的broker

try:
    while True:
        if task_id.ready():  # 如果任務結束這裡返回True
            print("Result is: ", task_id.get())
            break
except Exception as err:
    print(err)

呼叫add.delay()函式後會返回一個AsyncReult的物件,透過這個物件可以獲取如下內容:

state 返回任務狀態
task_id 返回任務ID
result 返回任務執行結果,等同於呼叫get()方法
ready() 判斷任務是否完成
info() 獲取任務資訊
wait(seconds) 等待N秒後獲取結果
successful() 判斷任務是否成功

 

 

 

 

 

 

 

 

 

 

對比命令執行和程式碼執行的結果

命令列啟動任務後如何結束呢?Ctrl+C。無論是啟動的任務還是執行任務,只要是命令列啟動的都可以這樣來結束,官方文件也是這樣說的。

呼叫一個任務一個任務其實就是傳送一個訊息到佇列中,worker收到訊息就會進行處理,也就是真正執行訊息對應的函式。Worker如果沒有確認這個訊息被消費或者說確認那麼這個訊息將不會被移除

如何把任務程式碼拆出來呢?

目的是為了單獨一個檔案寫task,一個檔案做celery的初始化工作。

初始化celery

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import
import sys
from celery import Celery

# 這裡多了一個 include引數,這個就是引入你的具體任務的py檔案
app = Celery("Chatper02", broker="redis://172.16.48.171:6379/3", backend="redis://172.16.48.171:6379/3",
             include=["Chatper02.tasks"])

if __name__ == "__main__":
    try:
        app.start()
    finally:
        sys.exit()

在Celery初始化時候,最後一個引數這裡用來include來實現載入task,這裡要注意寫的是tasks.py檔案的路徑。而且需要注意這裡的第一個引用是使用了絕對引用在Python2中必須要這麼寫,但是在Python3中絕對引用是預設設定可以不用寫。絕對路徑匯入後你就可以在程式碼裡使用相對名稱。

任務檔案

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import

from Chatper02.celery import app


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

 這裡就實現了配置和任務分離,啟動方式和之前一樣

如何做配置分離呢?

目的是單獨一個檔案寫Celery的初始化配置資訊,因為它的配置項還是不少的,雖然你可能用不了那麼多因為預設設定基本夠用,但是通常來講結構上還是要做到相對規範。這裡我還是先展示一下目錄結構吧

配置檔案 config.py 它其實就是一個Python檔案

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
http://docs.celeryproject.org/en/latest/userguide/configuration.html#cache-backend-settings
https://blog.csdn.net/libing_thinking/article/details/78812472
"""

# 如果密碼連線就是這樣的 'redis://:password@host:port/db'
BROKER_URL = 'redis://172.16.48.171:6379/3'
# 是否自動重連,預設是 True
BROKER_CONNECTION_RETRY = True
# 重連最大次數,預設是100
BROKER_CONNECTION_MAX_RETRIES = 100
CELERY_RESULT_BACKEND = 'redis://172.16.48.171:6379/3'
# 匯入task 如果不在這裡就需要在 Celery(__name__, include=["Chatper03.tasks"])
CELERY_INCLUDE = "Chatper03.tasks"
# 任務序列化方式  Default: "json"
CELERY_TASK_SERIALIZER = 'json'
# 結果序列化方式  Default: json
CELERY_RESULT_SERIALIZER = 'json'
# 結果過期時間 預設1天,單位秒
CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24
# 指定任務接受的內容型別   Default: {'json'} (set, list, or tuple).
CELERY_ACCEPT_CONTENT = ['json']
# 設定時區  Default: "UTC".
# CELERY_TIMEZONE = 'Asia/Shanghai'
# 是否啟用UTC時間
CELERY_ENABLE_UTC = True

初始化檔案 celery.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import
import sys
from celery import Celery

# 這裡使用了 __name__ 來表示模組檔名稱,可讀性好
app = Celery(__name__)
# 載入配置
app.config_from_object("Chatper03.config")

if __name__ == "__main__":
    try:
        app.start()
    finally:
        sys.exit()

還有另外一種寫法

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import
import sys
from celery import Celery
import Chatper03.config

# 這裡使用了 __name__ 來表示模組檔名稱,可讀性好
app = Celery(__name__)
# 載入配置
# app.config_from_object("Chatper03.config")
app.config_from_object(Chatper03.config)

if __name__ == "__main__":
    try:
        app.start()
    finally:
        sys.exit()

 

這個celery.py也就是app啟動檔案,這裡透過app.config_from_object()載入配置,如果是透過真實的模組載入就提前需要匯入就像第二種寫法,如果是字串形式就不需要匯入就像第一種寫法。

配置引數說明:http://docs.celeryproject.org/en/latest/userguide/configuration.html#cache-backend-settings

任務檔案

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import

from Chatper03.celery import app


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

 啟動方式還是和之前一樣

為什麼要把程式入口檔案叫做celery.py呢?

看下面這張圖,例子1和例子2程式入口檔案不同

我們啟動任務2

是不是發現有區別呢?

如果你的入口檔案叫做celery.py那麼你就可以不指定任務檔案,因為它會先載入celery的配置,然後註冊任務。如果按照上例啟動Chatper01你就不能省略.mytasks 因為這裡有配置。說白了就是它預設找的檔案就是celery.py,透過這個來初始化Celery,初始化Celery靠的是配置資訊,如果你使用了其他名稱就需要指定去哪裡讀取配置資訊。

如何後臺啟動和關閉worker呢

celery multi start  WORKER_NAME –A APP_NAME

啟動需要給worker起一個名字,因為同一個APP也就是任務可以啟動多個,啟動多個的意義就在於分散式。

停止

定時任務

Celery 3.x版本的寫法

入口檔案celery.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import
import sys
from celery import Celery
import Chatper04.config

# 這裡使用率 __name__ 來表示模組檔名稱,可讀性好
app = Celery(__name__)
# 載入配置
app.config_from_object(Chatper04.config)

if __name__ == "__main__":
    try:
        app.start()
    finally:
        sys.exit()

 

配置檔案config.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
http://docs.celeryproject.org/en/latest/userguide/configuration.html#cache-backend-settings
"""

# 如果密碼連線就是這樣的 'redis://:password@host:port/db'
BROKER_URL = 'redis://172.16.48.171:6379/3'
# 是否自動重連,預設是 True
BROKER_CONNECTION_RETRY = True
# 重連最大次數,預設是100
BROKER_CONNECTION_MAX_RETRIES = 100
CELERY_RESULT_BACKEND = 'redis://172.16.48.171:6379/3'
# 匯入task 如果不在這裡就需要在 Celery(__name__, include=["Chatper03.tasks"])
CELERY_INCLUDE = "Chatper04.tasks"
# 任務序列化方式  Default: "json"
CELERY_TASK_SERIALIZER = 'json'
# 結果序列化方式  Default: json
CELERY_RESULT_SERIALIZER = 'json'
# 結果過期時間 預設1天,單位秒
CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24
# 指定任務接受的內容型別   Default: {'json'} (set, list, or tuple).
CELERY_ACCEPT_CONTENT = ['json']
# 設定時區  Default: "UTC".
# CELERY_TIMEZONE = 'Asia/Beijing'
# 是否啟用UTC時間
CELERY_ENABLE_UTC = True

 

任務檔案tasks.py

在配置中CELERYBEAT_SCHEDULE定義了beat定時服務的屬性,當設定好了以後需要更新配置。 

啟動worker

啟動beat服務,它用於定期去執行指定的任務

檢視worker這邊的日誌輸出

Celery 4.x版本的寫法

在4.x中也可以使用上面的寫法。下面唯一有變化的就是tasks.py檔案

啟動方式和之前以後,只是升級到最新4.2.1版本後啟動介面有些變化

啟動beat服務的命令還是一樣的,這裡要加上任務檔案的名稱

Celery 4.x的另外一種寫法

執行work和beat的方法還是一樣的

task裝飾器的一些屬性

bind屬性

在程式裡就可以透過.來獲取比如self.request.task等。當bind=True時,被呼叫的函式傳遞進去的第一個引數就是task例項也就是這裡的self。

base屬性

定義基類,可以用詞來定義回撥函式

呼叫成功會有這樣的動作

關於路由

producer發出呼叫請求(message包含所呼叫任務的相關資訊)—>celery服務啟動時,會產生一個或多個交換機(exchanges),對應的交換機 接收請求message—>交換機根據message內容,將message分發到一個或多個符合條件的佇列(queue)—>每個佇列上都有一個或多個worker在監聽,在監聽到符合條件的message到達後,worker負責進行任務處理,任務處理完被確認後,佇列中的message將被刪除。

啟動worker時它會監聽在預設的佇列是celery,鍵是celery。我們的worker就監聽在這個預設的佇列celery上,生產者呼叫任務時由於也都使用預設值所以exchange根據key來路由訊息,就把訊息路由到其對應的隊裡上,這樣在這裡監聽的worker就會捕捉的該訊息。我們修改一下之前的定時任務的tasks.py檔案,其他不變,這些佇列和路由資訊也可以直接寫到config.py中去。

分別開啟2個終端執行worker

透過上面的截圖可以看到不同的worker監聽在不同的佇列中,下面啟動beat服務

觀察worker結果

在程式碼中如何呼叫呢?這種場景是註冊任務,透過程式碼執行來觸發執行具體任務,而不是透過定時任務的形式。tasks.py程式碼我們只去掉定時任務部分

執行的程式

delay()方法是apply_sync()方法的別名,後者可以接受更多引數。

# countdown : 設定該任務等待一段時間再執行,單位為s;
# eta : 定義任務的開始時間;eta=time.time()+10;
# expires : 設定任務時間,任務在過期時間後還沒有執行則被丟棄;
# retry : 如果任務失敗後, 是否重試;使用true或false,預設為true
# shadow:重新指定任務的名字str,覆蓋其在日誌中使用的任務名稱;
# retry_policy : 重試策略.
# 	max_retries : 最大重試次數, 預設為 3 次.
# 	interval_start : 重試等待的時間間隔秒數, 預設為 0 , 表示直接重試不等待.
# 	interval_step : 每次重試讓重試間隔增加的秒數, 可以是數字或浮點數, 預設為 0.2
# 	interval_max : 重試間隔最大的秒數, 即 透過 interval_step 增大到多少秒之後, 就不在增加了, 可以是數字或者浮點數, 預設為 0.2 .
# routing_key:自定義路由鍵;
# queue:指定傳送到哪個佇列;
# exchange:指定傳送到哪個交換機;
# priority:任務佇列的優先順序,0-9之間;
# serializer:任務序列化方法;通常不設定;
# compression:壓縮方案,通常有zlib, bzip2
# headers:為任務新增額外的訊息;
# link:任務成功執行後的回撥方法;是一個signature物件;可以用作關聯任務

task.apply_async((2,2), 
    compression='zlib',
    serialize='json',
    queue='priority.high',
    routing_key='web.add',
    priority=0,
    exchange='web_exchange')

相關文章