目录
弹性伸缩需实现定时伸缩功能,对比了几个实现方案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