官网地址:Advanced Python Scheduler — APScheduler 3.9.0.post1.post1 documentation
Quartz(java开发)的python版本实现。APScheduler提供了基于时间,固定时间点和crontab方式的任务调用方案, 可以当作一个跨平台的调度工具来使用。
概念
任务
四种组件
-
triggers(决定下一次执行任务的时间)
- Triggers contain the scheduling logic. Each job has its own trigger which determines when the job should be run next. Beyond their initial configuration, triggers are completely stateless.
-
job stores(存放预定执行的任务,默认配置将任务放在内存中,可以配置存放在各种数据库中)
- Job stores house the scheduled jobs. The default job store simply keeps the jobs in memory, but others store them in various kinds of databases. A job’s data is serialized when it is saved to a persistent job store, and deserialized when it’s loaded back from it. Job stores (other than the default one) don’t keep the job data in memory, but act as middlemen for saving, loading, updating and searching jobs in the backend. Job stores must never be shared between schedulers.
-
executors(处理任务执行的对象,将任务提交给线程池或者进程池,任务完成时,executor会通知scheduler,然后scheduler发送合适的事件)
- Executors are what handle the running of the jobs. They do this typically by submitting the designated callable in a job to a thread or process pool. When the job is done, the executor notifies the scheduler which then emits an appropriate event.
-
schedulers(调度程序,通常应用程序只有一个scheduler在运行。开发人员通常是用scheduler提供的接口去操作trigger、job stores、executor,而不直接操作)
- Schedulers are what bind the rest together. You typically have only one scheduler running in your application. The application developer doesn’t normally deal with the job stores, executors or triggers directly. Instead, the scheduler provides the proper interface to handle all those. Configuring the job stores and executors is done through the scheduler, as is adding, modifying and removing jobs.
scheduler
BlockingScheduler: use when the scheduler is the only thing running in your processBackgroundScheduler: use when you’re not using any of the frameworks below, and want the scheduler to run in the background inside your applicationAsyncIOScheduler: use if your application uses the asyncio moduleGeventScheduler: use if your application uses geventTornadoScheduler: use if you’re building a Tornado applicationTwistedScheduler: use if you’re building a Twisted applicationQtScheduler: use if you’re building a Qt application
要选择适当的作业存储,您需要确定是否需要作业持久性。如果您总是在应用程序开始时重新创建作业,那么您可能可以使用默认值(MemoryJobStore)。但是,如果您需要您的作业在调度器重启或应用程序崩溃时持续存在,那么您的选择通常取决于在编程环境中使用什么工具。但是,如果您可以自由选择,那么PostgreSQL后台上的SQLAlchemyJobStore是推荐的选择,因为它具有强大的数据完整性保护。
同样地,如果您使用上面的框架之一,则通常为您选择执行器。否则,默认的ThreadPoolExecutor对于大多数用途应该已经足够了。如果工作负载涉及到CPU密集型操作,则应该考虑使用ProcessPoolExecutor来使用多个CPU内核。您甚至可以同时使用这两个工具,将进程池执行程序添加为辅助执行程序。
trigger
内置的三种trigger类型
-
date(执行一次)
- use when you want to run the job just once at a certain point of time
-
interval(定期执行)
- use when you want to run the job at fixed intervals of time
-
cron(定时执行)
- use when you want to run the job periodically at certain time(s) of day
也可以将多个触发器组合成一个触发器,该触发器可以在所有参与触发器商定的时间触发,也可以在任何触发器触发时触发。For more information, see the documentation for combining triggers.
公共参数:
jobstore='redis':指定存储任务的位置,值为jobstores中的keyid:任务的id,不设置会自动生成,不允许与其他任务的id重复args: 执行任务的函数的参数列表,值为list类型,执行任务的函数如果需要传参,通过此参数配置
date trigger
date 是最基本的一种调度,job 只会执行一次,它表示特定的时间点触发,其参数如下所示:
run_date(datetime|str): job 要运行的时间,如果 run_date 为空,则默认取当前时间timezone(datetime.tzinfo|str):指定run_date的时区
示例
def my_job(text):
print(text)
# job 将在 2009 年 11 月 6 日 16:30:05 运行
sched.add_job(my_job, "date", run_date=datetime(2009, 11, 6, 16, 30, 5), args=["text"])
# 另一种写法
sched.add_job(my_job, "date", run_date="2009-11-06 16:30:05", args=["text"])
interval trigger
interval 表示周期性触发触发,其参数如下
weeks(int):间隔礼拜数days(int):间隔天数hours(int):间隔小时数minutes(int):间隔分钟数seconds(int):间隔秒数start_date(datetime|str):周期执行的起始时间点end_date(datetime|str):最后 可能 触发时间timezone(datetime.tzinfo|str):计算 date/time 类型时需要使用的时区jitter(int|None):最多提前或延后执行 job 的 偏振 秒数(防止如多个服务器在同一时间运行某个 job 时会非常有用)
如果
start_date为空,则默认是datetime.now() + interval作为起始时间。 如果start_date是过去的时间,trigger 不会追溯触发多次,而是根据过去的起始时间计算从当前时间开始下一次的运行时间。
示例
def my_job(text):
print(text)
# job_function 每两个小时执行一次,同时添加了 jitter 可以增加随机性
# 防止如多个服务器在同一时间运行某个 job 时会非常有用
sched.add_job(job_function, 'interval', hours=2, jitter=120, start_date="2010-10-10 09:30:00", end_date="2014-06-15 11:00:00")
cron trigger
相关资料:Linux Crontab 定时任务 | cron表达式
cron 提供了和 Linux crontab 格式兼容的触发器,是功能最为强大的触发器,其参数如下所示:
year(int|str)- 4 位年份month(int|str)- 2 位月份(1-12)day(int|str)- 一个月内的第几天(1-31)week(int|str)- ISO 礼拜数(1-53)day_of_week(int|str)- 一周内的第几天(0-6 或者 mon, tue, wed, thu, fri, sat, sun)hour(int|str)- 小时(0-23)minute(int|str)- 分钟(0-59)second(int|str)- 秒(0-59)start_date(datetime|str)- 最早可能触发的时间(date/time),含该时间点end_date(datetime|str)- 最后可能触发的时间(date/time),含该时间点timezone(datetime.tzinfo|str)- 计算 date/time 时所指定的时区(默认为 scheduler 的时区)jitter(int|None)- 最多提前或延后执行 job 的 偏振 秒数
一周的开始时间总是周一!
对于 cron trigger 来说,它的强大在于可以在每个参数字段上指定各种不同的表达式来确定下一个执行时间,类似于 Unix 的 cron 程序。但和 crontab 表达式不同的是,你可以忽略不需要的字段,其行为如下 大于你显式指定的最小参数字段的参数默认都为 * ,而小于的则默认为最小值(week 和 day_of_week 除外)。 这是 ApScheduler 2.0 修正后的默认行为,在此之前忽略的字段始终默认为 * 。如 day=1, minute=20 等同于 year="*", month="*", day=1, week="*", day_of_week="*", day_of_week="*", hour="*", minute=20, second=0 。
下表列出了从年份到秒可以使用的表达式,可以在单个字段中使用逗号隔开多个表达式:
| 表达式 | 应用字段 | 描述 |
|---|---|---|
| * | any | 通配符 |
| */a | any | 可被 a 整除的通配符 |
| a-b | any | 在 a-b 范围内的通配符 |
| a-b/c | any | 在 a-b 范围内可被 c 整除的通配符 |
| xth y | day | 表示一个月内的第 x 个礼拜的星期 y |
| last x | day | 表示一个月内最后的星期 x 触发 |
| last | day | 表示月末当天触发 |
| x,y,z | any | 组合表达式,用于组合以上的表达式 |
cron trigger 使用所谓的 walk clock 时间,因此如果所选时区遵守 DST(Daylight saving time 夏令时),那么在进入或退出夏令时时间时可能会导致意外发生。为了避免这个问题建议使用 UTC 时间,或提前预知并规划好执行的问题。
示例:
sched = apscheduler_init()
def job_function():
print "Hello World"
# job_function 会在 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')
# 可以使用装饰器模式(每月最后一个星期天执行)
@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!")
# 或者直接使用 crontab 表达式
sched.add_job(job_function, CronTrigger.from_crontab('0 0 1-15 may-aug *'))
sched.start()
开始
下载
pip install apscheduler
配置scheduler
步骤:
1.配置jobstores 2.配置executors 3.实例化scheduler
from apscheduler.jobstores.redis import RedisJobStore
from apscheduler.executors.pool import ThreadPoolExecutor,ProcessPoolExecutor
from apscheduler.schedulers.background import BackgroundScheduler#不会阻塞
from apscheduler.schedulers.blocking import BlockingScheduler #此.start()会一直阻塞
def apscheduler_init():
REDIS = {
'host': '',
'port': '6379',
'db': 0, #将任务持久化到redis的哪个数据库
# 'password':'', #若redis开启了密码验证则需要
}
# 1.设置任务的存储位置,可多个
jobstores = {
'redis': RedisJobStore(**REDIS)
}
# 2.设置定时任务运行的线程和进程,可选配置
executors = {
'default': ThreadPoolExecutor(10), # 默认线程数
'processpool': ProcessPoolExecutor(3), #默认进程
}
# 3.通过配置实例化scheduler
scheduler = BlockingScheduler(jobstores=jobstores, executors=executors,timezone='Asia/Shanghai')
return scheduler
scheduler = apscheduler_init() #全局变量初始化
启动
scheduler示例调用start()方法 如果scheduler是非BlockingScheduler类型实例出的,调用start()方法会立即返回不会阻塞。
if __name__ == '__main__':
import datetime
# 添加任务,一般是通过http接口动态增加删除任务
scheduler.add_job(
work,
'date', # 只执行一次
jobstore='redis', #任务存储的对象,如果没指明会用'default'
run_date=datetime(year=2009,month=11,day=6,hour=16,minute=30,second=5), #任务运行的时间 或者写成 run_date="2022-06-01 17:00:00"
timezone='Asia/Shanghai', # run_date的时区
)
# 添加一个interval类型的任务
scheduler.add_job(
work1,# 执行任务的函数
'interval',#任务类型
seconds=2, #每隔2s执行一次 (可选 weeks,days,hours,minutes,seconds)
jobstore='redis', # 选择任务存储的对象 jobstores中的key
start_date=str(datetime.datetime.now()).split(".")[0], #任务开始时间
# end_date="2022-05-30 15:25:00" #任务结束时间 默认一直运行
id='1', #设置任务id,其就是存在reids中的key,不可重复,默认会自动生成
args=[str(datetime.datetime.now()).split(".")[0]], #函数参数,列表类型
)
#重启后会自动从redis中获取定时任务
scheduler.start() #启动 如果是BlockingScheduler类型会被一直阻塞
# time.sleep(20000)
使用
uWSGI 中使用 APScheduler:uWSGI 使用了一些技巧来禁用掉 GIL 锁,但多线程的使用对于 APScheduler 的操作来说至关重要。为了修复这个问题,你需要使用 --enalbe-threads 选项来重新启用 GIL 。
添加job
相关示例看配置scheduler和启动部分
方法1:scheduler实例调用add_job()方法
# 方法原型
def add_job(self, func, trigger=None, args=None, kwargs=None, id=None, name=None,
misfire_grace_time=undefined, coalesce=undefined, max_instances=undefined,
next_run_time=undefined, jobstore='default', executor='default',
replace_existing=False, **trigger_args):
job_kwargs = {
'trigger': self._create_trigger(trigger, trigger_args),
'executor': executor,
'func': func,
'args': tuple(args) if args is not None else (),
'kwargs': dict(kwargs) if kwargs is not None else {},
'id': id,
'name': name,
'misfire_grace_time': misfire_grace_time,
'coalesce': coalesce,
'max_instances': max_instances,
'next_run_time': next_run_time
}
job_kwargs = dict((key, value) for key, value in six.iteritems(job_kwargs) if
value is not undefined)
job = Job(self, **job_kwargs)
# Don't really add jobs to job stores before the scheduler is up and running
with self._jobstores_lock:
if self.state == STATE_STOPPED:
self._pending_jobs.append((job, jobstore, replace_existing))
self._logger.info('Adding job tentatively -- it will be properly scheduled when '
'the scheduler starts')
else:
self._real_add_job(job, jobstore, replace_existing)
return job
方法2:使用装饰器scheduler实例.scheduled_job()
#装饰器原型
def scheduled_job(self, trigger, args=None, kwargs=None, id=None, name=None,
misfire_grace_time=undefined, coalesce=undefined, max_instances=undefined,
next_run_time=undefined, jobstore='default', executor='default',
**trigger_args):
def inner(func):
self.add_job(func, trigger, args, kwargs, id, name, misfire_grace_time, coalesce,
max_instances, next_run_time, jobstore, executor, True, **trigger_args)
return func
return inner
你可以 随时 调度 scheduler 里的 job 。如果添加 job 时,scheduler 尚未运行,job 会被临时地进行排列,直到 scheduler 启动之后,它的首次运行时间才会被确切地计算出来。
注意:
如果你希望使用 executor 或 job store 来序列化 job ,那么 job 必须满足以下两个条件:
- (被调度的)目标里的可调用对象必须时全局可访问的
- 可调用对象的任何参数都可以被序列化
重要事项
如果你调度的 job 在一个持久化的 job store 里,当你初始化你的应用程序时,你 必须 为 job 定义一个显式的 ID 并使用
replace_existing=True,否则每次你的应用程序重启时你都会得到那个 job 的一个新副本。
提示
如果想马上运行 job ,请在添加 job 时省略 trigger 参数。
移除 job
当从 scheduler 中移除一个 job 时,它会从关联的 job store 中被移除,不再被执行。有两种途径可以移除 job :
- 通过 job 的 ID 以及 job store 的别名来调用
remove_job()方法 - 对你在
add_job()中得到的 job 实例调用remove()方法
后者看起来更方便,实际上它要求你必须将调用 add_job() 得到的 Job 实例存储在某个地方。而对于通过 scheduled_job() 装饰器来调度 job 的就只能使用第一种方法。
如果一个 job 完成了调度(例如它的触发器不会再被触发),它会自动被移除。
例如:
job = scheduler.add_job(myfunc, 'interval', minutes=2)
job.remove()
相似的,可以使用显式地 job ID:
scheduler.add_job(myfunc, 'interval', minutes=2, id='my_job_id')
scheduler.remove_job('my_job_id')
暂停和恢复 job
通过 Job 实例或者 scheduler 本身你可以轻易地暂停和恢复 job 。当一个 job 被暂停,它的下一次运行时间将会被清空,同时不再计算之后的运行时间,直到这个 job 被恢复。
暂停一个 job ,使用以下方法:
而恢复一个 job ,则可以:
获取作业调度列表
可以使用 get_jobs 方法来获得机器上可处理的作业调度列表。方法会返回一个 Job 实例的列表,如果你仅仅对特定的 job store 中的 job 感兴趣,可以将 job store 的别名作为第二个参数。
更方便的做法时,使用 print_jobs() 来格式化输出作业列表以及它们的触发器和下一次的运行时间。
修改 job
通过 apscheduler.job.Job.modify() 或者 modify_job() 方法均可修改 job 的属性。你可以根据 id 修改该任何 Job 的属性。
例如:
job.modify(max_instances=6, name='Alternate name')
如果你想重新调度一个 job (这意味着要修改其 trigger),你可以使用 apscheduler.job.Job.reschedule() 或 reschedule_job() 方法。这些方法都会为 job 构建新的 trigger ,然后根据新的 trigger 重新计算其下一次的运行时间:
scheduler.reschedule_job('my_job_id', trigger='cron', minute='*/5')
终止 scheduler
以下方法可以终止 scheduler:
scheduler.shutdown()
默认情况下,scheduler 会终止其 job store 以及 executor ,然后等待所有目前执行的 job 完成后(自行终止)。如果你不想等待,可以这样:
scheduler.shutdown(wait=False)
这样依旧会终止 job store 和 executor ,但不会等待任何运行中的任务完成。
暂停/恢复 job 的运行
你可以用以下方法暂停被调度的 job 的运行:
scheduler.pause()
这会导致 scheduler 再被恢复之前一直处于休眠状态:
scheduler.resume()
如果没有进行过唤醒,也可以对处于暂停状态的 scheduler 执行 start 操作:
scheduler.start(paused=True)
这样可以让你有机会在那些不想要的 job 运行之前将它们排除掉。
限制作业的并发执行实例数目
默认情况下,每个 job 同时只会有一个实例在运行。这意味着如果一个 job 到达计划运行时间点时,前一个 job 尚未完成,那么这个 job 最近的一次运行计划将会 misfire(错过)。可以通过在添加 job 时指定 max_instances 关键字参数来设置具体 job 的最大实例数目,以便 scheduler 随后可以并发地执行它。
错过的作业执行以及合并操作(coalescing)
有时候 scheduler 无法在被调度的 job 的计划运行时间点去执行这个 job 。常见的原因是这个 job 是在持久化的 job store 中,恰好在其打算运行的时刻 scheduler 被关闭或重启了。这样,这个 job 就被定义为 misfired (错过)。scheduler 稍后会检查 job 每个被错过的执行时间的 misfire_grace_time 选项(可以单独给每个 job 设置或者给 scheduler 做全局设置),以此来确定这个执行操作是否要继续被触发。这可能到导致连续多次执行。
如果这个行为不符合你的实际需要,可以使用 coalescing 来回滚所有的被错过的执行操作为唯一的一个操作。如果对 job 启用了 coalescing ,那么即便 scheduler 在队列中看到这个 job 一个或多个执行计划,scheduler 都只会触发一次。
注意
如果因为进程(线程)池中没有可用的进程(线程)而导致 job 的运行被推迟了,那么 executor 会直接跳过它,因为相对于原计划的执行时间来说实在太晚了。如果在你的应用程序中出现了这种情况,你可以增加 executor 的线程(进程)的数目,或者调整 misfire_grace_time ,设置一个更高的值。
scheduler 事件
你可以为 scheduler 绑定事件监听器(event listen)。Scheduler 事件在某些情况下会被触发,而且它可能携带有关特定事件的细节信息。为 add_listener() 函数提供适当的掩码参数(mask argument)或者是将不同的常数组合到一起,可以监听特定类型的事件。可调用的 listener 可以通过 event object 作为参数而被调用。
留意文档里 events 模块中对于目前已有的事件以及其属性的特殊描述。
例如:
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)
故障排查
如果 scheduler 没有如预期般正常运行,可以尝试将 apscheduler 的 logger 的日志级别提升到 DEBUG 等级。
如果你还没有在一开始就将日志启用起来,那么你可以:
import logging
logging.basicConfig()
logging.getLogger('apscheduler').setLevel(logging.DEBUG)
这会提供 scheduler 运行时大量的有用信息。