任務隊列/任務調度:Celery和APScheduler(內含APScheduler實例)

Celery VS APScheduler:

celery: celery是一個專注於實時處理和任務調度的任務隊列,任務就是消息(消息隊列使用rabbitmq或者redie),消息中的有效載荷中包含要執行任務的全部數據。我們通常將celery作爲一個任務隊列來使用,但是celery也有定時任務的功能,但是celery無法在flask這樣的系統中動態的添加定時任務,而且單獨爲定時任務功能而搭建celery顯得過於重量級。

apscheduler: apscheduler是基於Quartz的一個Python定時任務框架,提供了基於日期、固定時間間隔以及crontab類型的任務,並且可以持久化作業。APScheduler算是在實際項目中最好用的一個工具庫,不僅可以在程序中動態的添加和刪除定時任務,還支持持久化




消息隊列(redis,rabbitmq)

上面的兩者都可以看做是任務隊列,任務隊列是邏輯模型,消息隊列是通信模型
任務隊列:將抽象的任務發送到執行的worker的組件,用於處理任務。
消息隊列MQ:是一種能實現生產者到消費者的單向通信模型,用於通知,傳遞消息。
任務隊列可以需要消息隊列來實現,比如redis作爲celery的broker。




APScheduler

首先安裝pip install apscheduler

APScheduler有四個組件:

  1. triggers: 觸發器,用於設定觸發任務的條件,觸發器包含了調度的邏輯,每個任務都有自己的觸發器決定該任務下次運行的時間。
  2. job stores: 任務儲存器,用於存放任務,把任務放在內存或者數據庫中,一個
  3. executors: 執行器,用於執行任務,可以設定執行模式爲單線程或者線程池,任務完畢後,執行器會通知調度器
  4. schedulers: 調度器,上面的三個組件都是參數,使用這三個參數創建調度器實例來運行

調度器的選擇:

根據不同的開發需求,選擇對應的調度器組件

  1. BlockingScheduler 阻塞式調度器:適用於只跑調度器的程序。
  2. BackgroundScheduler 後臺調度器:適用於非阻塞的情況,調度器會在後臺獨立運行。
  3. AsyncIOScheduler AsyncIO調度器,適用於應用使用AsnycIO的情況。
  4. GeventScheduler Gevent調度器,適用於應用通過Gevent的情況。
  5. TornadoScheduler Tornado調度器,適用於構建Tornado應用。
  6. TwistedScheduler Twisted調度器,適用於構建Twisted應用。
  7. QtScheduler Qt調度器,適用於構建Qt應用。

任務儲存器的選擇:

如果運行的任務是無狀態的,選擇默認的任務儲存器MemoryJobStore即可。

如果需要程序關閉或者重啓的時候,保存任務的狀態,那麼需要持久化的任務儲存器比如SQLAlchemyJobStore配合postgres作爲後臺數據庫。


執行器的選擇:

默認的ThreadPoolExecutor線程池執行器方案可以滿足大部分需求
如果程序是計算密集型,推薦使用ProcessPoolExecutor進程池執行器使用多核能力,還可以混合使用兩種


配置任務觸發器:

一共有三種內置的觸發器:

  1. date 日期,觸發任務運行的具體時間
  2. interval 間隔,觸發任務運行的時間間隔
  3. cron 週期,觸發任務運行的週期,較複雜,check it in google
from datetime import date
from apscheduler.schedulers.blocking import BlockingScheduler

sched = BlockingScheduler()

def my_job(text):
    print(text)

# date觸發器
sched.add_job(my_job, 'date', run_date=date(2009, 11, 6), args=['text'])
# interval觸發器
sched.add_job(job_function, 'interval', hours=2)


sched.start()

對於cron,是一個強大的類crontab表達式:

# 任務會在6月、7月、8月、11月和12月的第三個週五,00:00、01:00、02:00和03:00觸發
sched.add_job(job_function, 'cron', month='6-8,11-12', day='3rd fri', hour='0-3')


現在創建一個調度器例子:

from apscheduler.schedulers.background import BackgroundScheduler

scheduler = BackgroundScheduler()

# 因爲是非阻塞的後臺調度器,所以程序會繼續向下執行

這樣就可以創建了一個後臺調度器。這個調度器有一個名稱爲default的MemoryJobStore(內存任務儲存器)和一個名稱是default且最大線程是10的ThreadPoolExecutor(線程池執行器)。

然後我們創建一個更復雜的調度器:

from pytz import utc

from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.jobstores.mongodb import MongoDBJobStore
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor


jobstores = {
    'mongo': MongoDBJobStore(), # 名字爲mongo的MongoDBJobStore
    'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite') # 名字爲default的任務儲存器
}
executors = {
    'default': ThreadPoolExecutor(20), # 最大線程20的線程池
    'processpool': ProcessPoolExecutor(5)
}
job_defaults = {
    'coalesce': False,  # 默認爲新任務關閉合並模式
    'max_instances': 3  # 設置新任務默認最大實例數爲3
}
scheduler = BackgroundScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc)

啓動調度器:
啓動只需要調用start(),非阻塞調度器都會立即返回,可以繼續運行之後的代碼,比如添加任務。對於阻塞調度器BlockingScheduler,程序會阻塞在start的位置。並且,調度器啓動後就不能修改配置了。

添加任務:
add_job()或者通過裝飾器scheduled_job(),需要注意的是,如果是從數據庫讀取任務,那麼必須爲每一個任務定義一個明確的ID,並且使用replace_existing=True屬性,否則每次重啓程序的時候,都會得到一份新的任務拷貝,因爲任務的狀態不會被保存。

移除任務:
remove_job()參數爲任務ID或者任務儲存器名字,或者在通過add_job()創建的任務實例上調用remove()方法。第二種方法更好,但是必須是創建任務實例的時候被保存在變量中,如果使用裝飾器創建的任務,那麼只能選擇第一種方法。

scheduler.add_job(myfunc, 'interval', minutes=2, id='my_job_id')
scheduler.remove_job('my_job_id')

暫停和恢復任務:
暫停:
apscheduler.job.Job.pause()
apscheduler.schedulers.base.BaseScheduler.pause_job()
恢復:
apscheduler.job.Job.resume()
apscheduler.schedulers.base.BaseScheduler.resume_job()

獲取任務列表:
通過get_jobs()就可以獲得一個可修改的任務列表。get_jobs()第二個參數可以指定任務儲存器名稱,那麼就會獲得對應任務儲存器的任務列表。

修改任務:
通過apscheduler.job.Job.modify()或modify_job(),你可以修改任務當中除了id的任何屬性。

關閉調度器:
scheduler.shutdown()
scheduler.shutdown(wait=False)



下面介紹一個使用APScheduler的實例

首先構造一個類繼承APScheduler:

class AsyncScheduler(APScheduler):
    def __init__(self, replace_job=False, scheduler=None, app=None):
        self.replace_job = replace_job
        super().__init__(scheduler, app)

    def __add_job(self, job_id, func, args=None, **kw):
        kw['replace_existing'] = self.replace_job
        try:
            if args:
                kw['args'] = args
            ret = self.add_job(job_id, func, **kw)
            reason = 'OK'
        except ConflictingIdError as e:
            ret = None
            reason = str(e)

        return ret, reason

    def add_cron_job(self, job_id, func, args=None, **kw):
        kw['trigger'] = CronTrigger(**kw)
        return self.__add_job(job_id, func, args, **kw)

    def add_interval_job(self, job_id, func, args=None, **kw):
        kw['trigger'] = IntervalTrigger(**kw)
        return self.__add_job(job_id, func, args, **kw)

    def add_once_job(self, job_id, func, args=None, **kw):
        kw['trigger'] = DateTrigger(**kw)
        return self.__add_job(job_id, func, args, **kw)
        
g_scheduler = AsyncScheduler()

然後使用這個APScheduler類:

# 一次性添加異步任務,第一個參數是任務id,第二個參數是func即要完成的具體任務
g_scheduler.add_once_job('manual job', start_sync)

# 添加有間隔的定時任務,參數包含:任務ID,具體任務,定時相關參數
g_scheduler.add_interval_job('auto job', start_sync, args=(1,), weeks=0, days=0, hours=0, minutes=0, seconds=0,start_date=start_date)

# 暫停當前的異步任務,並且從任務調度裏面刪除該任務
stop_sync()
g_scheduler.delete_job('manual job')  # 根據任務ID刪除該任務



Celery

rabbitmq和celery的區別 他們是兩個層面的東西,celery是一個分佈式的隊列,基本工作是管理分配任務到不同的服務器,但是服務器之間如何通信是celery不能解決的,所以rabbitmq作爲一個消息隊列管理工具被引入到和celery集成,現在crabbitmq在celery中扮演broker的角色,就是消息代理。

celery架構:
在這裏插入圖片描述
上面的異步任務模塊其實也是任務發佈者,所以產生任務的方式有兩種,一種是發佈者發佈任務(web應用), 另一種是任務調度按期發佈任務(定時任務)

使用的時候其實是從上到下的,broker是一個消息傳輸的中間件,每當程序調用celery異步任務的時候,會向broker發送消息,然後celery會監聽到這個消息,進行任務的執行,至於最下面的backend(數據庫,一般使用redis)用於儲存這些消息和celery執行的一些消息和結果。

celery是一個自帶電池的任務隊列
首先celery需要一個發送和接收消息的解決方案,起通常以獨立服務形式出現,稱之爲消息中間人:RabbitMQ,Redis

安裝Celery:
pip install celery

應用:
首先需要一個celery實例,這個實例用於開始做任何事情比如創建任務,管理。。它可以被其他的模塊導入,創建一個task.py

from celery import Celery
app = Celery('tasks', broker='amqp://guest@localhost//')
@app.task
def add(x, y):
    return x + y

celery的第一個參數是當前模塊的名稱(必須),第二個參數是中間人關鍵字參數也就是中間人URL。

運行celery職程服務器:
celery -A tasks worker --loglevel=info

保存結果:
如果我們想要保持追蹤任務的狀態,celery需要在某個地方儲存或者發送這些狀態,可以從幾個內建的後端選擇,

app = Celery('tasks', backend='amqp', broker='amqp://')

後端通過backend參數來指定,如果選擇使用配置模塊,則通過CELERY_RESULT_BACKEND選項來設置。

配置:
配置可以直接在應用上設置,也可以使用一個獨立的配置模塊。可以一次性設置對個選項,可以使用update

app.conf.update(
    CELERY_TASK_SERIALIZER='json',
    CELERY_ACCEPT_CONTENT=['json'],  # Ignore other content
    CELERY_RESULT_SERIALIZER='json',
    CELERY_TIMEZONE='Europe/Oslo',
    CELERY_ENABLE_UTC=True,
)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章