Python 定時框架 APScheduler技術原理

1.APscheduler是什麼

Python定時任務框架APScheduler,Advanced Python Scheduler (APScheduler) 是一個輕量級但功能強大的進程內任務調度器,作用爲在指定的時間規則執行指定的作業(時間規則:指定的日期時間、固定時間間隔以及類似Linux系統中Crontab的方式);並且該框架可以進行持久化配置,保證在項目重啓或者崩潰恢復後仍然能夠恢復之前的作業繼續運行。

2.APScdeduler的基本概念

2.1 Job 作業

包含要執行的任務(函數),任務執行所需要的參數,以及調度器執行該任務所需要的一些額外的配置信息,比如什麼時間點執行?任務信息保存在哪裏,內存還是數據庫等?使用線程執行還是進程執行?

Job的屬性包括:

  • id:指定作業的唯一ID
  • name:指定作業的名字
  • trigger:觸發器,分爲date,interval,cron三種觸發器,主要的作用就是根據設置的時間規則就是計算出任務的下一次觸發時間。
  • executor:執行器,負責處理作業的運行,通常使用的是線程池或進程池。當作業完成時,執行器將會通知調度器。
  • max_instances:每個job在同一時刻能夠運行的最大實例數,默認情況下爲1個,可以指定爲更大值,這樣即使上個job還沒運行完同一個job又被調度的話也能夠再開一個線程執行
  • next_run_time:Job下次的執行時間,創建Job時可以指定一個時間[datetime],不指定的話則默認根據trigger獲取觸發時間。
  • misfire_grace_time:Job的延遲執行時間,例如Job的計劃執行時間是21:00:00,但因服務重啓或其他原因導致21:00:31才執行,如果設置此key爲40,則該job會繼續執行,否則將會丟棄此job。
  • coalesce:Job是否合併執行,是一個bool值。例如scheduler停止20s後重啓啓動,而job的觸發器設置爲5s執行一次,因此此job錯過了4個執行時間,如果設置爲是,則會合併到一次執行,否則會逐個執行。
  • func:Job執行的函數。
  • args:Job執行函數需要的位置參數。
  • kwargs:Job執行函數需要的關鍵字參數。

2.2 Trigger 觸發器

Trigger綁定到Job,在scheduler調度篩選Job時,根據觸發器的規則計算出Job的觸發時間,然後與當前時間比較確定此Job是否會被執行,總之就是根據trigger規則計算出下一個執行時間。

目前APScheduler支持觸發器:

  • DateTrigger: 指定日期時間執行一次
  • IntervalTrigger: 固定時間間隔執行,支持每秒、每分、每時、每天、每週
  • CronTrigger: 類似Linux系統的Crontab定時任務

DateTrigger和IntervalTrigger很好理解,使用也比較簡單,這裏重點說一下CronTrigger觸發器。

CronTrigger觸發器的參數選項如下:

字段 類型 說明
year int | string 年,4位數字
month int | string 月 (範圍1-12或者jan– dec)
day int | string 日 (範圍1-31)
week int | string 周 (範圍1-53)
day_of_week int | string 星期幾 (範圍0-6 或者 mon - sun)
hour int | string 時 (範圍0-23)
minute int | string 分 (範圍0-59)
second int | string 秒 (範圍0-59)
start_date datetime | string 最早開始日期(包含)
end_date datetime | string 最晚結束時間(包含)
timezone datetime.tzinfo | string 指定時區

CronTrigger可用的表達式:

表達式 參數類型 描述
* 所有 通配符。例:minutes=*即每分鐘觸發
* / a 所有 每隔時長a執行一次。例:minutes="* / 3" 即每隔3分鐘執行一次
a - b 所有 a - b的範圍內觸發。例:minutes=“2-5”。即2到5分鐘內每分鐘執行一次
a - b / c 所有 a - b範圍內,每隔時長c執行一次。
xth y 第幾個星期幾觸發。x爲第幾個,y爲星期幾
last x 一個月中,最後一個星期的星期幾觸發
last 一個月中的最後一天觸發
x, y, z 所有 組合表達式,可以組合確定值或上述表達式

注:

1.當省略時間參數時,在顯式指定參數之前的參數會被設定爲*,之後的參數會被設定爲最小值,week 和day_of_week的最小值爲*。比如,設定day=1, minute=20等同於設定year=’*’, month=’*’, day=1, week=’*’, day_of_week=’*’, hour=’*’, minute=20, second=0,即每個月的第一天,且當分鐘到達20時就觸發。

2.如果要實現每隔一段時間執行一次的任務,有時候使用CronTriger並不合適,例如我們設定hour=’*/15’,希望任務每隔15小時執行一次,但實際上,這個表達式的意思是0-23小時內,每隔15小時執行一次,也就是每天的0:00, 15:00會執行一次; 並不會按照預期一樣0:00執行一次,15:00執行一次,第二天的6:00再執行一次…

三種觸發器的使用示例:

from datetime import datetime
from apscheduler.schedulers.blocking import BlockingScheduler


def task():
    print("Hello World")


if __name__ == '__main__':

    scheduler = BlockingScheduler()

    # DateTrigger 2020-12-12 00:00:00執行一次
    scheduler.add_job(task, 'date', id='date_job', run_date=datetime(2020, 12, 12, 0, 0, 0))

    # IntervalTrigger 每隔兩秒執行一次
    scheduler.add_job(task, 'interval', id='interval_job', seconds=2)

    # CronTrigger 每週一到週五的5-10小時區間內,每兩個小時的半點執行 一次
    scheduler.add_job(task, 'cron', id='cron_job_01', day_of_week='mon_fri', hour='5-10/2', minute=30)

    # CronTrigger 6,7,8,11,12月的第三個週五的 01:00, 02:00, 03:00執行
    scheduler.add_job(task, 'cron', id='cron_job_02', month='6-8, 11-12', day='3rd fri', hour='1-3')

    scheduler.start()

2.3 Executor 執行器

負責處理作業的運行,通常使用的是ThreadPoolExecutor或ProcessPoolExecutor。當作業完成時,執行器將會通知調度器。

大多數情況下, 執行器 選擇 ThreadPoolExecutor 就夠用了,但如果涉及到CPU密集的作業,就可以選擇ProcessPoolExecutor,以充分利用多核CPU。當然也可以同時配置使用兩個執行器,將進程池 ProcessPoolExecutor 調度器作爲你的第二個執行器。

2.4 Jobstore 作業存儲器

保存要調度的任務,其中除了默認的作業存儲是把作業保存在內存中,其他的作業存儲是將作業保存在數據庫中。一個作業的數據將在保存在持久化的作業存儲之前,會對作業執行序列化操作,當重新讀取作業時,再執行反序列化操作。

目前APScheduler支持的Jobstore:

  • MemoryJobStore
  • MongoDBJobStore
  • RedisJobStore
  • RethinkDBJobStore
  • SQLAlchemyJobStore
  • ZooKeeperJobStore

2.5 Scheduler 調度器

負責將上面幾個組件聯繫在一起,一般在應用中只有一個調度器,程序開發者不會直接操作觸發器、作業存儲或執行器,而是利用調度器提供了處理這些合適的接口,作業存儲和執行器的配置都是通過在調度器中完成的。

在我們的使用過程中,選擇合適的 調度器 是根據我們的開發環境以及實際應用來決定的,根據IO模型的不同,主要有下面一些常見的調度器:

  • BlockingScheduler:適合於只在進程中運行單個任務的情況
  • BackgroundScheduler:適合於不運行使用其他框架時,並希望在程序後臺執行的情況
  • AsyncIOScheduler:適合於使用asyncio框架的情況
  • GeventScheduler: 適合於使用gevent框架的情況 TornadoScheduler: 適合於使用Tornado框架的應用
  • TwistedScheduler: 適合使用Twisted框架的應用 QtScheduler: 適合使用QT的情況

BlockingScheduler和BackgroundScheduler的區別:

BackgroundScheduler繼承於BlockingScheduler,只不過BackgroudScheduler執行輪詢的操作時單開了一個線程,因此不會阻塞主程序的運行。

# APScheduler源碼
class BlockingScheduler(BaseScheduler):
    """
    A scheduler that runs in the foreground
    """
    _event = None

    def start(self, *args, **kwargs):
        self._event = Event()
        super().start(*args, **kwargs)
        self._main_loop()

    def _main_loop(self):
        wait_seconds = TIMEOUT_MAX
        while self.state != STATE_STOPPED:
            self._event.wait(wait_seconds)
            self._event.clear()
            wait_seconds = self._process_jobs()
            
class BackgroundScheduler(BlockingScheduler):
    """
    A scheduler that runs in the background using a separate thread
    """

    _thread = None

    def _configure(self, config):
        self._daemon = asbool(config.pop('daemon', True))
        super()._configure(config)

    def start(self, *args, **kwargs):
        self._event = Event()
        BaseScheduler.start(self, *args, **kwargs)
        self._thread = Thread(target=self._main_loop, name='APScheduler')
        self._thread.daemon = self._daemon
        self._thread.start()

調度器可以通過如下三種方式進行配置:

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(), 
    'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
}
executors = {
    'default': ThreadPoolExecutor(20),
    'processpool': ProcessPoolExecutor(5)
}
job_defaults = {
    'coalesce': False, #任務不進行歸併
    'max_instances': 3	#任務最大實例數爲3
}
scheduler = BackgroundScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc)
from apscheduler.schedulers.background import BackgroundScheduler


# The "apscheduler." prefix is hard coded
scheduler = BackgroundScheduler({
    'apscheduler.jobstores.mongo': {
         'type': 'mongodb'
    },
    'apscheduler.jobstores.default': {
        'type': 'sqlalchemy',
        'url': 'sqlite:///jobs.sqlite'
    },
    'apscheduler.executors.default': {
        'class': 'apscheduler.executors.pool:ThreadPoolExecutor',
        'max_workers': '20'
    },
    'apscheduler.executors.processpool': {
        'type': 'processpool',
        'max_workers': '5'
    },
    'apscheduler.job_defaults.coalesce': 'false',
    'apscheduler.job_defaults.max_instances': '3',
    'apscheduler.timezone': 'UTC',
})
from pytz import utc

from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.executors.pool import ProcessPoolExecutor


jobstores = {
    'mongo': {'type': 'mongodb'},
    'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
}
executors = {
    'default': {'type': 'threadpool', 'max_workers': 20},
    'processpool': ProcessPoolExecutor(max_workers=5)
}
job_defaults = {
    'coalesce': False,
    'max_instances': 3
}
scheduler = BackgroundScheduler()

# 添加任務

scheduler.configure(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc)

2.6 Scheduler 事件監聽機制

Event是APScheduler在進行某些操作時觸發相應的事件,用戶可以自定義一些函數(Listener)來監聽這些事件,當觸發某些Event時,做一些具體的操作(比如給用戶發郵件、短信等)。
常見的比如。Job執行異常事件 EVENT_JOB_ERROR。Job執行時間錯過事件EVENT_JOB_MISSED。

目前APScheduler定義的Event:

  • EVENT_SCHEDULER_STARTED
  • EVENT_SCHEDULER_START
  • EVENT_SCHEDULER_SHUTDOWN
  • EVENT_SCHEDULER_PAUSED
  • EVENT_SCHEDULER_RESUMED
  • EVENT_EXECUTOR_ADDED
  • EVENT_EXECUTOR_REMOVED
  • EVENT_JOBSTORE_ADDED
  • EVENT_JOBSTORE_REMOVED
  • EVENT_ALL_JOBS_REMOVED
  • EVENT_JOB_ADDED
  • EVENT_JOB_REMOVED
  • EVENT_JOB_MODIFIED
  • EVENT_JOB_EXECUTED
  • EVENT_JOB_ERROR
  • EVENT_JOB_MISSED
  • EVENT_JOB_SUBMITTED
  • EVENT_JOB_MAX_INSTANCES

3.APScheduler的工作流程

在這裏插入圖片描述
當一個任務被添加到調度器,會根據指定的時間規則,計算出該任務下一次執行的時間,保存到作業存儲器當中(內存或者數據庫)。在調度器的主循環中,會反覆檢查作業儲存器當中是否有到期要執行的任務,如果有就調度執行。

在主循環中,如果不間斷地輪詢作業存儲器,但是實際上沒有要執行的任務,會造成資源的浪費。因此Apscheduler在實現上,每次遍歷作業存儲器的時候,會找出所有任務當中離當前時間最近的任務,計算出這個任務距離下一次執行的時間間隔。這個時候,主循環就可以睡眠,等時間到了再喚醒。

4.APScheduler的使用實例

import pymongo
import logging
import datetime
import random
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
from apscheduler.jobstores.memory import MemoryJobStore
from apscheduler.jobstores.mongodb import MongoDBJobStore

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S',
    filename='log.txt',
    filemode='a'
)

job_stores = {
    'mongodb': MongoDBJobStore(
        client=pymongo.MongoClient("mongodb://localhost:27017/"),
        database='Apscheduler',
        collection='SchedulerJobInfo'
    ),
    'default': MemoryJobStore()
}

executors = {
    'process': ProcessPoolExecutor(5),
    'default': ThreadPoolExecutor(20),
}

job_defaults = {
    'coalesce': False,  # 相同任務不進行歸併
    'max_instances': 1  # 單個任務的同一時刻只能執行一個實例
}


def task(arg):
    print(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), arg)
    return 1 / 0 if random.choice([True, False]) else None


def task_remove(arg):
    scheduler.remove_job('interval_task')
    print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), arg)


def task_pause(arg):
    scheduler.pause_job('interval_task')
    print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), arg)


def task_resume(arg):
    scheduler.resume_job('interval_task')
    print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), arg)


def listen(event):
    if event.exception:
        print("任務執行出錯!!!")
    else:
        print("任務成功執行...")


if __name__ == '__main__':
    scheduler = BlockingScheduler(
        jobstores=job_stores,
        executors=executors,
        job_defaults=job_defaults
    )
    scheduler.add_listener(listen, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
    scheduler._logger = logging
    # 指定時間執行一次
    scheduler.add_job(task, 'date', id='date_task', args=["一次性任務"], jobstore='mongodb',
                      run_date=datetime.datetime.now() + datetime.timedelta(seconds=10))
    # 間隔觸發器:每隔5s執行一次
    scheduler.add_job(task, 'interval', id='interval_task', seconds=5, args=["循環任務"], jobstore='mongodb')
    # cron觸發器:每16s執行一次
    scheduler.add_job(task, 'cron', id='cron_task', second="*/16", args=["定時任務"], jobstore='mongodb')
    # 暫停任務
    scheduler.add_job(task_pause, id='pause_task', args=['暫停間隔執行任務'], jobstore='mongodb',
                      next_run_time=datetime.datetime.now() + datetime.timedelta(seconds=20))
    # 恢復任務
    scheduler.add_job(task_resume, id='resume_task', args=['恢復間隔執行任務'], jobstore='mongodb',
                      next_run_time=datetime.datetime.now() + datetime.timedelta(seconds=30))
    # 刪除任務
    scheduler.add_job(task_remove, id='remove_task', args=['刪除間隔執行任務'], jobstore='mongodb',
                      next_run_time=datetime.datetime.now() + datetime.timedelta(seconds=40))

    scheduler.start()

調度器剛啓動時mongodb數據庫記錄:
在這裏插入圖片描述
運行結果:
在這裏插入圖片描述
任務結束後,存儲器中已完成的任務會被刪除:
在這裏插入圖片描述
注:

定期執行任務、暫停間隔執行任務、恢復間隔執行任務、刪除間隔執行任務,都是一次性任務,執行結束後,作業存儲器中相應的記錄會被刪除。同時,因爲間隔執行任務最後被刪除了,所以作業存儲器中就只剩下每隔16s執行一次的定時任務了。

通過運行結果也可以看出,通過CronTrigger設置的每隔16s(second=’*/16’)執行一次的任務,只會在每分鐘的0s,16s,32,48s執行,不會真的嚴格按照每隔16s執行一次,如果要實現嚴格的間隔執行,請使用IntervalTrigger。

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