在工作中,常常会有周期性的工作任务,可以采用 Linux 系统自带的 crond 结合命令行实现。但如果要使用代码的形式,则需要使用自身习惯的语言进行开发。

在测试中使用最多的语言是 Python,这次以 python 实现定时任务作为例子进行讲解。


1. 利用循环的形式while True: + sleep()实现定时任务

time 模块中的 sleep() 函数在 python 中常用又实用,可以令运行中的线程暂停一定时间后再继续执行。所谓暂停,即令当前线程进入阻塞状态,当达到 sleep() 函数规定的时间后,再由阻塞状态转为就绪状态,等待 CPU 调度。基于这样的特性可以通过 while死循环+sleep() 的方式实现简单的定时任务。

代码示例:

import datetime
import time
def time_printer():
now = datetime.datetime.now()
ts = now.strftime('%Y-%m-%d %H:%M:%S')
print('do func time :', ts)
def loop_monitor():
whileTrue:
time_printer()
time.sleep(6)# 暂停6秒
if __name__ =="__main__":
loop_monitor()

主要缺点:

● 只能设定间隔,不能指定具体的时间,比如每天早上9:00。

● time.sleep 是一个阻塞函数,也就是说 sleep 这一段时间内程序什么也不能操作。


2. 运用Timeloop库进行定时任务

Timeloop 是一个简单的库,使用 decorator 模式在线程中运行标记函数,可用于运行多周期任务。

示例代码:

import time
from timeloop importTimeloop
from datetime import timedelta
tl =Timeloop()
@tl.job(interval=timedelta(seconds=3))
def sample_job_every_3s():
print"3s job current time : {}".format(time.ctime())
@tl.job(interval=timedelta(seconds=5))
def sample_job_every_5s():
print"5s job current time : {}".format(time.ctime())
@tl.job(interval=timedelta(seconds=10))
def sample_job_every_10s():
print"10s job current time : {}".format(time.ctime())


3. 利用内置模块sched实现定时任务

sched 模块使用通用事件调度器,把延迟函数放到需要定时执行任务代码中的调度器类使用,等待指定时间后执行。同时支持多线程应用程序,在每个任务执行后会立刻调用延时函数,以确保其他线程也能执行。class sched.scheduler(timefunc, delayfunc) 这个类定义了调度事件的通用接口,它需要外部传入两个参数,timefunc 是一个没有参数的返回时间类型数字的函数(常用使用的如 time 模块里面的 time ),delayfunc 应该是一个需要一个参数来调用、与 timefunc 的输出兼容、并且作用为延迟多个时间单位的函数(常用的如time模块的sleep)。

代码示例:

import datetime
import time
import sched
def time_printer():
now = datetime.datetime.now()
ts = now.strftime('%Y-%m-%d %H:%M:%S')
print('do func time :', ts)
loop_monitor()
def loop_monitor():
s = sched.scheduler(time.time, time.sleep)# 生成调度器
s.enter(5,1, time_printer,())
s.run()
if __name__ =="__main__":
loop_monitor()

scheduler对象主要方法:

● enter(delay, priority, action, argument),安排一个事件来延迟delay个时间单位。

● cancel(event):从队列中删除事件。如果事件不是当前队列中的事件,则该方法将跑出一个ValueError。

● run():运行所有预定的事件。这个函数将等待(使用传递给构造函数的delayfunc()函数),然后执行事件,直到不再有预定的事件。


4. 利用调度模块schedule实现定时任务

schedule 是一个第三方轻量级的任务调度模块,可以按照秒、分、小时、日期或者自定义事件执行时间。schedule 允许用户使用简单、人性化的语法以预定的时间间隔定期运行Python函数(或其它可调用函数)。先来看代码,是不是不看文档就能明白什么意思?

import schedule
import time
def job():
print("I'm working...")
schedule.every(10).seconds.do(job)
schedule.every(10).minutes.do(job)
schedule.every().hour.do(job)
schedule.every().day.at("10:30").do(job)
schedule.every(5).to(10).minutes.do(job)
schedule.every().monday.do(job)
schedule.every().wednesday.at("13:15").do(job)
schedule.every().minute.at(":17").do(job)
whileTrue:
schedule.run_pending()
time.sleep(1)

装饰器:通过 @repeat() 装饰静态方法

import time
from schedule import every, repeat, run_pending
@repeat(every().second)
def job():
print('working...')
whileTrue:
run_pending()
time.sleep(1)

传递参数:

import schedule
def greet(name):
print('Hello', name)
schedule.every(2).seconds.do(greet, name='Alice')
schedule.every(4).seconds.do(greet, name='Bob')
whileTrue:
schedule.run_pending()

装饰器同样能传递参数:

from schedule import every, repeat, run_pending
@repeat(every().second,'World')
@repeat(every().minute,'Mars')
def hello(planet):
print('Hello', planet)
whileTrue:
run_pending()

取消任务:

import schedule
i =0
def some_task():
global i
i +=1
print(i)
if i ==10:
schedule.cancel_job(job)
print('cancel job')
exit(0)
job = schedule.every().second.do(some_task)
whileTrue:
schedule.run_pending()

运行一次任务:

import time
import schedule
def job_that_executes_once():
print('Hello')
return schedule.CancelJob
schedule.every().minute.at(':34').do(job_that_executes_once)
whileTrue:
schedule.run_pending()
time.sleep(1)

根据标签检索任务:

# 检索所有任务:schedule.get_jobs()
import schedule
def greet(name):
print('Hello {}'.format(name))
schedule.every().day.do(greet,'Andrea').tag('daily-tasks','friend')
schedule.every().hour.do(greet,'John').tag('hourly-tasks','friend')
schedule.every().hour.do(greet,'Monica').tag('hourly-tasks','customer')
schedule.every().day.do(greet,'Derek').tag('daily-tasks','guest')
friends = schedule.get_jobs('friend')
print(friends)

根据标签取消任务:

# 取消所有任务:schedule.clear()
import schedule
def greet(name):
print('Hello {}'.format(name))
if name =='Cancel':
schedule.clear('second-tasks')
print('cancel second-tasks')
schedule.every().second.do(greet,'Andrea').tag('second-tasks','friend')
schedule.every().second.do(greet,'John').tag('second-tasks','friend')
schedule.every().hour.do(greet,'Monica').tag('hourly-tasks','customer')
schedule.every(5).seconds.do(greet,'Cancel').tag('daily-tasks','guest')
whileTrue:
schedule.run_pending()

运行任务到某时间:

import schedule
from datetime import datetime, timedelta, time
def job():
print('working...')
schedule.every().second.until('23:59').do(job)# 今天23:59停止
schedule.every().second.until('2030-01-01 18:30').do(job)# 2030-01-01 18:30停止
schedule.every().second.until(timedelta(hours=8)).do(job)# 8小时后停止
schedule.every().second.until(time(23,59,59)).do(job)# 今天23:59:59停止
schedule.every().second.until(datetime(2030,1,1,18,30,0)).do(job)# 2030-01-01 18:30停止
whileTrue:
schedule.run_pending()

马上运行所有任务(主要用于测试):

import schedule
def job():
print('working...')
def job1():
print('Hello...')
schedule.every().monday.at('12:40').do(job)
schedule.every().tuesday.at('16:40').do(job1)
schedule.run_all()
schedule.run_all(delay_seconds=3)# 任务间延迟3秒

并行运行:使用 Python 内置队列实现

import threading
import time
import schedule
def job1():
print("I'm running on thread %s"% threading.current_thread())
def job2():
print("I'm running on thread %s"% threading.current_thread())
def job3():
print("I'm running on thread %s"% threading.current_thread())
def run_threaded(job_func):
job_thread = threading.Thread(target=job_func)
job_thread.start()
schedule.every(10).seconds.do(run_threaded, job1)
schedule.every(10).seconds.do(run_threaded, job2)
schedule.every(10).seconds.do(run_threaded, job3)
whileTrue:
schedule.run_pending()
time.sleep(1)


5. 利用任务框架APScheduler实现定时任务

APScheduler(advanceded python scheduler)是基于 Quartz 的一个 Python 定时任务框架,实现了 Quartz 的所有功能,使用起来十分方便。提供了基于日期、固定时间间隔以及 crontab 类型的任务,并且可以持久化任务。

基于这些功能,我们可以很方便地实现一个 Python 定时任务系统。


它有以下三个特点:

● 类似于LiunxCron的调度程序(可选的开始/结束时间)

● 基于时间间隔的执行调度(周期性调度,可选的开始/结束时间)

● 一次性执行任务(在设定的日期/时间运行一次任务)


APScheduler有四种组成部分:

① 触发器(trigger)包含调度逻辑,每一个作业有它自己的触发器,用于决定接下来哪一个作业会运行。除了他们自己初始配置意外,触发器完全是无状态的。

② 作业存储(job store)存储被调度的作业,默认的作业存储是简单地把作业保存在内存中,其他的作业存储是将作业保存在数据库中。一个作业的数据讲在保存在持久化作业存储时被序列化,并在加载时被反序列化。调度器不能分享同一个作业存储。

③ 执行器(executor)处理作业的运行,他们通常通过在作业中提交制定的可调用对象到一个线程或者进城池来进行。当作业完成时,执行器将会通知调度器。

④ 调度器(scheduler)是其他的组成部分。你通常在应用只有一个调度器,应用的开发者通常不会直接处理作业存储、调度器和触发器,相反,调度器提供了处理这些的合适的接口。配置作业存储和执行器可以在调度器中完成,例如添加、修改和移除作业。通过配置executor、jobstore、trigger,使用线程池(ThreadPoolExecutor默认值20)或进程池(ProcessPoolExecutor默认值5)并且默认最多3个(max_instances)任务实例同时运行,实现对job的增删改查等调度控制。


示例代码:

from apscheduler.schedulers.blocking importBlockingScheduler
from datetime import datetime
# 输出时间
def job():
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
# BlockingScheduler
sched =BlockingScheduler()
sched.add_job(my_job,'interval', seconds=5, id='my_job_id')
sched.start()


5.1 Job 作业

Job 作为 APScheduler 最小执行单位。创建 Job 时指定执行的函数,函数中所需参数,Job 执行时的一些设置信息。

构建说明:

● id:指定作业的唯一ID;

● name:指定作业的名字;

● trigger:apscheduler定义的触发器,用于确定Job的执行时间,根据设置的trigger规则,计算得到下次执行此job的时间,满足时将会执行;

● executor:apscheduler定义的执行器,job创建时设置执行器的名字,根据字符串你名字到scheduler获取到执行此job的执行器,执行job指定的函数;

● max_instances:执行此job的最大实例数,executor执行job时,根据job的id来计算执行次数,根据设置的最大实例数来确定是否可执行;

● 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执行函数需要的关键字参数;


5.2 Trigger 触发器

Trigger 绑定到 Job,在 scheduler 调度筛选 Job 时,根据触发器的规则计算出 Job 的触发时间,然后与当前时间比较确定此 Job 是否会被执行,总之就是根据 trigger 规则计算出下一个执行时间。

目前 APScheduler 支持触发器:

● 指定时间的DateTrigger

● 指定间隔时间的IntervalTrigger

● 像Linux的crontab一样的CronTrigger


触发器参数:date date定时,作业只执行一次。

run_date (datetime|str)– the date/time to run the job at
timezone (datetime.tzinfo|str)– time zone for run_date if it doesn’t have one already
sched.add_job(my_job,'date', run_date=date(2009,11,6), args=['text'])
sched.add_job(my_job,'date', run_date=datetime(2019,7,6,16,30,5), args=['text'])


触发器参数:interval interval间隔调度

● weeks (int)–间隔几周

● days (int)–间隔几天

● hours (int)–间隔几小时

● minutes (int)–间隔几分钟

● seconds (int)–间隔多少秒

● start_date (datetime|str)–开始日期

● end_date (datetime|str)–结束日期

● timezone (datetime.tzinfo|str)–时区

sched.add_job(job_function,'interval', hours=2)


触发器参数:cron cron调度

● (int|str)表示参数既可以是int类型,也可以是str类型

● (datetime | str)表示参数既可以是datetime类型,也可以是str类型

● year (int|str)–4-digit year -(表示四位数的年份,如2008年)

● month (int|str)– month (1-12)-(表示取值范围为1-12月)

● day (int|str)– day of the (1-31)-(表示取值范围为1-31日)

● week (int|str)– ISO week (1-53)-(格里历2006年12月31日可以写成2006年-W52-7(扩展形式)或2006W527(紧凑形式))

● hour (int|str)– hour (0-23)–(表示取值范围为0-23时)

● minute (int|str)– minute (0-59)–(表示取值范围为0-59分)

● second (int|str)– second (0-59)–(表示取值范围为0-59秒)

start_date (datetime|str) –earliest possible date/time to trigger on (inclusive)–(表示开始时间)

● end_date(datetime|str)–latest possible date/time to trigger on(inclusive)–(表示结束时间)

● timezone (datetime.tzinfo|str) – time zone to usefor the date/time calculations (defaults to scheduler timezone) - (表示时区取值)

CronTrigger可用的表达式:

# 6-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')
# 每周一到周五运行 直到2024-05-30 00:00:00
sched.add_job(job_function,'cron', day_of_week='mon-fri', hour=5, minute=30, end_date='2024-05-30'


5.3 Executor 执行器

Executor 在 scheduler 中初始化,另外也可通过 scheduler 的 add_executor 动态添加 Executor。每个 executor 都会绑定一个 alias ,这个作为唯一标识绑定到 Job ,在实际执行时会根据 Job 绑定的 executor 找到实际的执行器对象,然后根据执行器对象执行 Job。Executor 的种类会根据不同的调度来选择,如果选择 AsyncIO 作为调度的库,那么选择 AsyncIOExecutor。如果选择 tornado 作为调度的库,选择TornadoExecutor。如果选择启动进程作为调度,选择 ThreadPoolExecutor 或者 ProcessPoolExecutor 都可以。Executor 的选择需要根据实际的 scheduler 来选择不同的执行器。

目前APScheduler 支持的 Executor:

● executors.asyncio:同步io,阻塞

● executors.gevent:io多路复用,非阻塞

● executors.pool:线程ThreadPoolExecutor和进程ProcessPoolExecutor

● executors.twisted:基于事件驱动


5.4 Jobstore 作业存储

Jobstore 在 scheduler 中初始化,另外也可通过 scheduler 的 add_jobstore 动态添加 Jobstore。每个 jobstore 都会绑定一个 alias,scheduler 在 Add Job 时,根据指定的 jobstore 在 scheduler 中找到相应的 jobstore,并将 job 添加到jobstore 中。作业存储器决定任务的保存方式, 默认存储在内存中(MemoryJobStore),重启后就没有了。

APScheduler 支持的任务存储器有:

● jobstores.memory:内存

● jobstores.mongodb:存储在mongodb

● jobstores.redis:存储在redis

● jobstores.rethinkdb:存储在rethinkdb

● jobstores.sqlalchemy:支持sqlalchemy的数据库如mysql,sqlite等

● jobstores.zookeeper:zookeeper

不同的任务存储器可以在调度器的配置中进行配置(见调度器)


5.5 Event 事件

Event 是 APScheduler 在进行某些操作时触发相应的事件,用户可以自定义一些函数来监听这些事件,当触发某些 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(Listener表示用户自定义监听的一些Event,比如当Job触发了EVENT_JOB_MISSED事件时可以根据需求做一些其他处理。)

5.6 调度器

Scheduler 是 APScheduler 的核心,所有相关组件通过其定义。scheduler 启动之后,将开始按照配置的任务进行调度。除了依据所有定义 Job 的 trigger 生成的将要调度时间唤醒调度之外。当发生 Job 信息变更时也会触发调度。

APScheduler 支持的调度器方式如下,比较常用的为 BlockingScheduler 和 BackgroundScheduler。

● BlockingScheduler:适用于调度程序是进程中唯一运行的进程,调用start函数会阻塞当前线程,不能立即返回。

● BackgroundScheduler:适用于调度程序在应用程序的后台运行,调用start后主线程不会阻塞。

● AsyncIOScheduler:适用于使用了asyncio模块的应用程序。

● GeventScheduler:适用于使用gevent模块的应用程序。

● TwistedScheduler:适用于构建Twisted的应用程序。

● QtScheduler:适用于构建Qt的应用程序。


5.7 Scheduler的工作流程

Scheduler添加job流程:

Python实现定时任务的几种方法_python

Scheduler调度流程:

Python实现定时任务的几种方法_python_02


最后

网络中最常见的 python 宣传语:“人生苦短,我学 python”,定时执行重复工作跟这句话很搭。

以上就是五种运用 python 实现定时执行任务的方案,希望对大家有所帮助。


下期会给大家分享如何从pod内部访问k3s集群API,敬请期待~

欢迎各位关注、留言,大家的支持就是我的动力!