【技术修养】漫谈定时任务
前言
在很多的业务场景中,我们都会使用到定时任务。这篇文章简单对定时任务的一些原理和业界的方案进行整理和归纳。其中参考和引用的网络资料出处在Reference标注。
Linux 定时任务
定时任务调度分类
Linux 下的定时任务调度分为两类:系统任务调度和用户任务调度。
系统任务是由 cron (crond) 系统服务来控制的,这个系统服务是默认启动的。用户自己设置的计划任务则使用 crontab 命令。在 velinux 系统中,查看配置文件如下:
1 |
|
第一行 SHELL 变量指定了系统要使用哪个 shell,这里是 sh;第二行 PATH 变量指定了系统执行命令的路径; 用户定期的任务,比如系统信息收集。用户可以使用 crontab 。用户定义的 crontab 文件都被保存在 /var/spool/cron/crontabs 目录中。文件名为用户名。velinux 如下:
1 |
|
Crontab 的工作原理
Crontab 由一个名为”Crond”的守护进程负责调度任务,当 Crond 启动的时候,就会从配置文件(路径在 /var/spool/cron 下)加载所有的定时任务。当执行 crontab 命令的时候,会动态的添加新的定时任务,并加入到配置文件中。Crontab 每次执行任务,都会产生执行记录,目录在 /var/log/cron 下。
Crontab 的痛点问题
使用 crontab 主要有如下痛点:
- 无高可用:为了保证业务幂等执行,需要在不同的机器配置不同的 crontab 任务。crontab 只能调度本机器上的定时任务,如果某一个机器挂了,那上面的定时任务也都不会执行了,有稳定性风险。
- 无自动负载均衡:不同的脚本放在不同的机器上,需要手动负载均衡,如果脚本比较多,运维代价很高。
- 无权限隔离:一般企业生产的机器只有运维才能登陆,但是开发要新增/修改脚本和定时任务,也需要登录到生产的机器上,没法做到权限隔离。
Quartz
概述
Quartz是Java领域最著名的开源任务调度工具。Quartz提供了极为广泛的特性如持久化任务,集群和分布式任务等,其特点如下:
- 完全由Java写成,方便集成(Spring)
- 伸缩性
- 负载均衡
- 高可用性
quartz基本原理
核心元素
Quartz核心要素有Scheduler、Trigger、Job、JobDetail,其中trigger和job、jobDetail为元数据,而Scheduler为实际进行调度的控制器。
- Trigger
Trigger用于定义调度任务的时间规则,在Quartz中主要有四种类型的Trigger:SimpleTrigger、CronTrigger、DataIntervalTrigger和NthIncludedTrigger。
- Job&Jodetail
Quartz将任务分为Job、JobDetail两部分,其中Job用来定义任务的执行逻辑,而JobDetail用来描述Job的定义(例如Job接口的实现类以及其他相关的静态信息)。对Quartz而言,主要有两种类型的Job,StateLessJob、StateFulJob
- Scheduler
实际执行调度逻辑的控制器,Quartz提供了DirectSchedulerFactory和StdSchedulerFactory等工厂类,用于支持Scheduler相关对象的产生。
核心元素间关系
主要线程
在Quartz中,有两类线程,也即执行线程和调度线程,其中执行任务的线程通常用一个线程池维护。线程间关系如图1-2所示。
在quartz中,Scheduler调度线程主要有两个:regular Scheduler Thread(执行常规调度)和Misfire Scheduler Thread(执行错失的任务)。其中Regular Thread 轮询Trigger,如果有将要触发的Trigger,则从任务线程池中获取一个空闲线程,然后执行与改Trigger关联的job;Misfire Thraed则是扫描所有的trigger,查看是否有错失的,如果有的话,根据一定的策略进行处理。
数据存储
Quartz中的trigger和job需要存储下来才能被使用。Quartz中有两种存储方式:RAMJobStore,JobStoreSupport,其中RAMJobStore是将trigger和job存储在内存中,而JobStoreSupport是基于jdbc将trigger和job存储到数据库中。RAMJobStore的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在集群应用中,必须使用JobStoreSupport。其中表结构如表1-1所示。
Table name | Description |
---|---|
QRTZ_CALENDARS | 存储Quartz的Calendar信息 |
QRTZ_CRON_TRIGGERS | 存储CronTrigger,包括Cron表达式和时区信息 |
QRTZ_FIRED_TRIGGERS | 存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息 |
QRTZ_PAUSED_TRIGGER_GRPS | 存储已暂停的Trigger组的信息 |
QRTZ_SCHEDULER_STATE | 存储少量的有关Scheduler的状态信息,和别的Scheduler实例 |
QRTZ_LOCKS | 存储程序的悲观锁的信息 |
QRTZ_JOB_DETAILS | 存储每一个已配置的Job的详细信息 |
QRTZ_SIMPLE_TRIGGERS | 存储简单的Trigger,包括重复次数、间隔、以及已触的次数 |
QRTZ_BLOG_TRIGGERS | Trigger作为Blob类型存储 |
QRTZ_TRIGGERS | 存储已配置的Trigger的信息 |
QRTZ_SIMPROP_TRIGGERS |
quartz集群原理
一个Quartz集群中的每个节点是一个独立的Quartz应用,它又管理着其他的节点。这就意味着你必须对每个节点分别启动或停止。Quartz集群中,独立的Quartz节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一Quartz应用的,如图1-3所示。
XXL Job
概述
XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
架构设计
架构图
设计思想
将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。
将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。
因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性;
系统组成
- 调度模块(调度中心)
- 负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;
- 支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。
- 执行模块(执行器):
- 负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;
- 接收“调度中心”的执行请求、终止请求和日志请求等。
xxl-job与quartz
整体来说,xxl-job就是quartz的一个增强版,其弥补了quartz不支持并行调度,不支持失败处理策略和动态分片的策略等诸多不足,同时其有管理界面,上手比较容易,支持分布式,适用于分布式场景下的使用。两者相同的是都是通过数据库锁来控制任务不能重复执行。
K8s Cronjob
什么是 K8s CronJob
Job 是 K8s 中的一种资源,用来处理短周期的 Pod,相当于一次性任务,跑完就会把 Pod 销毁,不会一直占用资源,可以节省成本,提高资源利用率。CronJob 也是 K8s 中的资源,用来周期性的重复调度 Job。
下面是一个 CronJob 的示例,每隔 5 分钟调度脚本 edas/schedulerx-job.sh:
1 |
|
K8s CronJob 的优势
与单纯使用 Crontab 相比,使用 K8s CronJob 带来了如下优势:
- 高可用:K8s 会保证集群的高可用,如集群中有节点挂了,都不会影响定时任务的调度。
- 自动负载均衡:Pod 默认选择负载最低的 node 执行,支持 NodeSelector 和亲和性等多种负载均衡策略。
- 权限隔离:只有运维可以登录 master 和 worker 节点,开发通过管控或者 ApiServer 来创建和更新 CronJob,并且支持命名空间隔离,RBAC 权限管理。
K8s CronJob 的进阶能力
Linux Crontab 只能周期性调度本机的脚本,功能比较简单,K8s 定时任务支持更多的进阶能力:
在 Job 资源上
并行执行:通常一个 Job 只启动一个 Pod,可以通过配置 spec.completions 参数,来决定一个 Job 要执行多少个 Pod。
索引任务:并行执行通常需要和索引任务结合使用,当配置 .spec.completionMode=”Indexed” 时,这个 Job 就是一个索引任务,每个 Pod 会获得一个不同的索引值,介于 0 和 .spec.completions-1 之间,这样就可以让不同的 Pod 根据索引值处理不同的数据。
并行限流:并行执行的时候,通常还需要做限流,可以配置 .spec.parallelism 参数,来控制一个 Job 最多同时跑多少个 Pod。
失败自动重试:可以配置 .spec.backoffLimit,来设置 Job 失败重试次数。
超时:可以配置 .spec.activeDeadlineSeconds,来设置 Job 超时的时间。
在 CronJob 资源上
时区:可以通过设置 .spec.timeZone 参数,决定 CronJob 按照哪个时区的时间来调度任务。
并发性规则:当一个 Job 还在执行,下次调度时间到了,是否执行新的 Job,可以通过 .spec.concurrencyPolicy 来配置,取值为 Allow/Forbid/Replace。
任务历史限制:可以通过配置 .spec.successfulJobsHistoryLimit 和 .spec.failedJobsHistoryLimit 来决定保留多少成功和失败的 Job。
Reference
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!