目錄
彈性伸縮需實現定時伸縮功能,對比了幾個實現方案python-crontab、celery和APScheduler,最終選定APScheduler.
官方文檔:https://apscheduler.readthedocs.io/en/v3.6.0/index.html
Github源碼:https://github.com/agronholm/apscheduler
一,簡介
Advanced Python Scheduler (APScheduler) 是一個Python庫,可實現延遲調度要執行Python代碼的功能,可以只執行一次,也可以定期執行。可以隨時添加新任務或刪除舊任務。如果將job任務存儲在數據庫中,這些任務還將在重新啓動調度程序後保持它們的狀態並繼續運行。當重新啓動調度程序時,它將運行離線時應該運行的所有job任務。
1,安裝APSchedule 3.6.0
直接pip聯網安裝,或者下載離線包安裝:https://pypi.org/project/APScheduler/3.6.0/ ,手動安裝APScheduler,手動安裝需要安裝依賴包funcsigs、zlocal
$ pip install apscheduler
2,APScheduler四個組件
1,triggers觸發器
包含調度邏輯,每一個job有它自己的觸發器,用於決定job下一次運行時間。除了初始配置外,觸發器完全是無狀態的。
2,job stores作業存儲
存儲被調度的job,默認的job存儲是簡單地把job存儲在內存中,其他的job存儲是保存在數據庫中。Job的數據在保存到持久化存儲時被序列化,並在加載時進行反序列化。job存儲(默認存儲除外)不將job數據保存在內存中,而是充當後臺保存、加載、更新和搜索job的中間人。job存儲永遠不能在調度程序之間共享。
3,executors執行器
負責處理job的運行,通過將job中指定的可調用對象 提交給一個線程或進程池來運行。當job完成時,執行器將會通知調度器,然後調度程序發出相應event。
4,schedulers調度器
一個應用程序中通常只有一個調度器在運行,應用程序開發人員通常不會直接處理job存儲、執行器和觸發器,相反,調度器程序提供了處理這些事件的接口。
配置job存儲和執行器都是在調度器中完成,例如添加、修改和移除job。
二,選擇調度器、作業存儲、執行器和觸發器
1,schedulers調度器選擇
對調度程序的選擇主要取決於當前的編程環境,還有使用APScheduler的目的場景,以下7種調度器可選:
BlockingScheduler : 調度器在當前進程的主線程中運行,也就是會阻塞當前線程。
BackgroundScheduler : 調度器在後臺線程中運行,不會阻塞當前線程。(在沒有使用下面5個框架時使用)
AsyncIOScheduler : 結合 asyncio 模塊(一個異步框架)一起使用。
GeventScheduler : 程序中使用 gevent(高性能的Python併發框架)作爲IO模型,和 GeventExecutor 配合使用。
TornadoScheduler : 程序中使用 Tornado(一個web框架)的IO模型。
TwistedScheduler : 配合 TwistedExecutor使用。
QtScheduler : 配合 Qt 應用使用。
2,job stores作業存儲選擇
要選擇適當的作業存儲,首先要確定是否需要對作業數據持久化。如果總是在應用程序開始時重新創建作業,那麼作業存儲可以選擇默認方式(MemoryJobStore)。否則,選擇對應的持久化存儲方式。jobstore提供對scheduler中job的增刪改查接口,根據存儲backend的不同,分以下幾種:
MemoryJobStore:沒有序列化,jobs就存在內存裏,增刪改查也都是在內存中操作
SQLAlchemyJobStore:所有sqlalchemy支持的數據庫都可以做爲backend,增刪改查操作轉化爲對應backend的sql語句
MongoDBJobStore:用mongodb作backend
RedisJobStore: 用redis作backend
RethinkDBJobStore: 用rethinkdb 作backend
ZooKeeperJobStore:用ZooKeeper做backend
3,executors執行器選擇
如果有使用上面的5個框架之一,通常會選擇對應框架的executor。否則,默認使用的ThreadPoolExecutor就可以滿足大多數場景。如果工作涉及CPU密集型操作,則應該考慮使用ProcessPoolExecutor來充分利用多個CPU內核。也可以同時使用這兩種方法,將進程池執行器添加爲輔助執行器。
最常用的兩種executor :ProcessPoolExecutor 和 ThreadPoolExecutor,其它的還有AsyncIOExecutor、DebugExecutor(一種特殊的執行程序,直接執行可調用的目標,而不是將其延遲給線程或進程)、GeventExecutor、TwistedExecutor。
4,triggers觸發器選擇
當調度一個job時,需要爲它選擇一個trigger觸發器。觸發器決定在運行job時計算日期/時間的邏輯。APScheduler有三種內置的觸發器類型:
1)date: 指定某個確定的時間點,job僅執行一次。
2)interval: 指定時間間隔(fixed intervals)週期性執行。
3)cron: 使用cron風格表達式週期性執行,用於(在指定時間內)定期運行job的場景。
4)combining:還可以將多個觸發器組合成一個觸發器,該觸發器可以按所有參與觸發器約定的時間觸發,也可以在其中任何一個觸發器將觸發的時候觸發。參考combining的AndTrigger和OrTrigger實現。
三,調度器scheduler啓動關閉和配置
1,啓動調度器
啓動調度器只需調用調度器上的start()。除了BlockingScheduler以外的調度程序,此調用將立即返回,你可以繼續應用程序的初始化過程,例如向調度程序添加作業。
對於BlockingScheduler,只需要在完成任何初始化步驟之後調用start()。
注意:啓動調度程序後,不能再更改其設置。
2,關閉調度器
scheduler.shutdown()
默認情況下,調度程序關閉其作業存儲和執行器,並等待所有當前執行的作業完成。如果你不想等,你可以執行:
scheduler.shutdown(wait=False)
這仍然會關閉作業存儲和執行器,但不會等待任何正在運行的任務完成。
3,配置調度器
APScheduler提供了許多不同的方法來配置調度程序。可以使用配置字典,也可以將選項作爲關鍵字參數傳入。還可以先實例化調度器,然後添加作業並配置調度器。通過這種方式,可以爲任何環境獲得最大的靈活性。
1)默認配置:
假設在你的應用程序中運行BackgroundScheduler、默認的job存儲和默認的executors執行程序,如下:
from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler()
此時將得到一個BackgroundScheduler實例,其中MemoryJobStore爲“default”作業存儲方法,ThreadPoolExecutor爲“default”執行器,默認最大線程數爲10。(參考apscheduler\executors\pool.py的ThreadPoolExecutor)
2)高級配置:
若希望使用兩個executor,擁有兩個作業存儲,還希望爲新作業調整默認值並設置不同的時區。配置可實現如下:
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
}
scheduler = BackgroundScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc)
其中,url指定數據庫的連接,運行後會直接在指定的數據庫中添加apscheduler_jobs表,保存job相關信息。
四,job操作
Job是APScheduler的核心,其承接當前需要執行的工作和任務,可以在系統運行過程中動態地進行增加/修改/刪除/查詢等操作。
1,job添加
共有兩種方式進行新增job的操作:
1)基於add_job來動態增加
代碼示例:
sched.add_job(job_function, 'cron', day_of_week='mon-fri', hour=5, minute=30, end_date='2014-05-30')
2)基於修飾器scheduled_job來動態裝飾job的實際函數
代碼示例:
@sched.scheduled_job('cron', id='my_job_id', day='last sun')
def some_decorated_task():
print("I am printed at 00:00:00 on the last Sunday of every month!")
在內置的作業存儲中,只有MemoryJobStore不會序列化作業。在內置的執行器中,只有ProcessPoolExecutor會序列化作業。
注意:如果在應用程序初始化期間在持久性作業存儲中調度作業,則**必須**爲作業定義顯式ID,並使用'replace_existing=True',否則每次重新啓動應用程序時,都會得到一份新的作業副本!
2,job移除
可以通過job對象調用remove刪除。
也可通過scheduler對象,指定job id調用remove_job刪除;scheduler對象還可調用remove_all_jobs刪除所有job。
示例1:
job = scheduler.add_job(myfunc, 'interval', minutes=2)
job.remove()
示例2:
scheduler.add_job(myfunc, 'interval', minutes=2, id='my_job_id')
scheduler.remove_job('my_job_id')
3,job暫停和恢復
可以通過作業實例或調度程序本身對作業執行暫停和恢復操作。當作業暫停時,它的下一個運行時被清除,並且在作業恢復之前不會計算它的進一步運行時。
暫停作業:
apscheduler.job.Job.pause()
apscheduler.schedulers.base.BaseScheduler.pause_job()
恢復作業:
apscheduler.job.Job.resume()
apscheduler.schedulers.base.BaseScheduler.resume_job()
4,job列表獲取
要獲得調度作業處理列表,可以使用get_jobs()方法。它將返回所有job實例。如果只需要獲取特定作業存儲庫中包含的作業,可將作業存儲別名作爲第二個參數。
爲了方便,可以使用print_jobs()方法,該方法將打印出格式化的作業列表、它們的觸發器和下一次運行時。
apscheduler.get_jobs()
5,job修改
可以通過apscheduler.job.Job.modify() 或apscheduler.modify_job()修改除了id之外的job屬性。例如:
job.modify(max_instances=6, name='Alternate name')
如果你想修改job的調度器,也就是說,改變它的觸發器。你可以使用apscheduler.job.Job.reschedule() 或reschedule_job()
scheduler.reschedule_job('my_job_id', trigger='cron', minute='*/5')
五,簡單示例運行
1,APScheduler 啓用3步驟
1)新建scheduler調度器(選擇一種scheduler執行實例化操作)
2)向調度器添加一個job調度任務
3)運行job調度任務
2,blocking 類型調度器示例
演示使用blocking阻塞調度程序來調度每隔3秒執行一次的作業。
from datetime import datetime
import os
from apscheduler.schedulers.blocking import BlockingScheduler
def tick():
print('Tick! The time is: %s' % datetime.now())
if __name__ == '__main__':
scheduler = BlockingScheduler()
scheduler.add_job(tick, 'interval', seconds=3)
print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C'))
try:
scheduler.start()
except (KeyboardInterrupt, SystemExit):
pass
3,background 類型調度器示例
演示使用background後臺調度程序來調度每隔3秒執行一次的作業。
from datetime import datetime
import time
import os
from apscheduler.schedulers.background import BackgroundScheduler
def tick():
print('Tick! The time is: %s' % datetime.now())
if __name__ == '__main__':
scheduler = BackgroundScheduler()
scheduler.add_job(tick, 'interval', seconds=3)
scheduler.start()
print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C'))
try:
# This is here to simulate application activity (which keeps the main thread alive).
while True:
time.sleep(2)
except (KeyboardInterrupt, SystemExit):
# Not strictly necessary if daemonic mode is enabled but should be done if possible
scheduler.shutdown()
4,觸發器示例
1)date觸發器,特定的時間點觸發,作業任務只會執行一次。
# 在 2019-03-29 14:00:00 時刻運行一次 job_func 方法
scheduler.add_job(job_func, 'date', run_date=datetime(2019, 3, 29, 14, 0, 0), args=['text'])
其中,run_date賦值類型可以爲date/datetime對象,或符合ISO_8601時間格式的字符串。其它時間格式及場景格式如下:
sched.add_job(my_job, 'date', run_date=date(2019, 3, 29), args=['text'])
sched.add_job(my_job, 'date', run_date='2019-03-29 14:30:05', args=['text'])
sched.add_job(my_job, args=['text']) #立即運行
2)interval 觸發器,固定時間間隔觸發。
# 在 2019-03-29 14:00:01 ~ 2019-03-29 14:00:10 之間, 每隔兩分鐘執行一次job_func方法。
scheduler.add_job(job_func, 'interval', minutes=2, start_date='2019-03-29 14:00:01' , end_date='2019-03-29 14:00:10')
其中,start_date和end_date賦值類型可以爲date/datetime對象,或符合ISO_8601時間格式的字符串。其它時間格式及場景格式如下:
sched.add_job(job_function, 'interval', hours=2) #持續定時觸發
sched.add_job(job_function, 'interval', hours=1, jitter=120) #使用jitter參數,用於在執行時間中添加一個隨機組件,用於多服務器場景中防止同時運行一個job的場景。此時將額外延遲[-120,+120]
3)cron 觸發器,在特定時間週期性地觸發,和Linux crontab格式兼容。
# 在2019-03-30 00:00:00之前,每週一到週五的5:30(am)觸發
sched.add_job(job_function, 'cron', day_of_week='mon-fri', hour=5, minute=30, end_date='2019-03-30')
其中,start_date和end_date賦值類型可以爲date/datetime對象,或符合ISO_8601時間格式的字符串。其它時間格式及場景格式如下:
sched.add_job(job_function, 'cron', month='6-8,11-12', day='3rd fri', hour='0-3') #在六月七月八月十一月十二月的第三個週五的0點1點2點3點執行
sched.add_job(job_function, CronTrigger.from_crontab('0 0 1-15 may-aug *')) #使用標準crontab表達式
sched.add_job(job_function, 'cron', hour='*', jitter=120) #使用jitter參數,同上
六,其它
1,max_instances參數
限制一個作業併發執行實例的數量。(add_job方法)
默認情況下,每個作業只能同時運行一個實例。這意味着,如果有作業即將運行,但前一個運行尚未完成,則最新的運行將無效。通過在添加作業時使用max_instances關鍵字參數,可以爲調度程序設置允許併發運行的特定作業的最大實例數。
2,misfire_grace_time參數
有時,調度程序可能無法在調度時執行計劃的作業。最常見的情況是,在持久化作業存儲中調度作業,並且在作業應該執行之後關閉並重新啓動調度程序。當這種情況發生時,job被認爲是“失敗的”。調度程序將根據作業的misfire_grace_time選項(可以根據每個作業或調度程序中的全局設置該選項)檢查每個錯過的執行時間,以確定是否仍然應該觸發執行。這可能導致作業連續執行幾次。
如果你的特定用例不希望出現這種行爲,那麼可以使用coalescing 合併將所有這些未執行的操作合併到一起。換句話說,如果爲作業啓用了合併,並且調度程序看到作業的一個或多個隊列執行,它將只觸發一次。對於“bypassed”運行,不會發送無效事件。
如果一個作業的執行由於池中沒有線程或進程可用而延遲,那麼執行器可能會因爲它運行得太晚而跳過它(與它最初指定的運行時相比)。如果在您的應用程序中可能發生這種情況,可以增加執行器中的線程/進程數量,或者將misfire_grace_time設置調整爲更高的值。(add_job方法)
3,事件調度器
可以監聽調度、任務執行情況相關的事件。
def my_listener(event):
if event.exception:
print('The job crashed :(')
else:
print('The job worked :)')
scheduler.add_listener(my_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
4,故障排查
如果調度器沒有按照預期工作,可以將apscheduler日誌記錄器的日誌級別提高到調試級別。
如果還沒有啓用日誌功能,可以這樣做:
import logging
logging.basicConfig()
logging.getLogger('apscheduler').setLevel(logging.DEBUG)
七,附錄
1,整體結構
2,支持與以下5種框架集成
asyncio
gevent
Tornado
Twisted
Qt
參考:
https://apscheduler.readthedocs.io/en/latest/userguide.html
https://github.com/agronholm/apscheduler/tree/master/docs