阅读一篇「定时任务框架选型」的文章时,一位 友的留言电到了我:
我看过那么多所谓的教程,大部分都是教“如何使用工具”的,没有多少是教“如何制作工具”的,能教“如何仿制工具”的都已经是凤毛麟角,中国 软件行业,缺的是真正可以“制作工具”的程序员,而绝对不缺那些“使用工具”的程序员! … ”这个业界最不需要的就是“会使用XX工具的工程师”,而是“有创造力的软件工程师”!业界所有的饭碗,本质就是“有创造力的软件工程师”提供出来的啊!
写这篇文章,想和大家从头到脚说说任务调度,希望大家读完之后,能够理解实现一个任务调度系统的核心逻辑。
Quartz的核心是三个组件。
- 任务:Job 用于表示被调度的任务;
- 触发器:Trigger 定义调度时间的元素,即按照什么时间规则去执行任务。一个Job可以被多个Trigger关联,但是一个Trigger 只能关联一个Job;
- 调度器 :工厂类创建Scheduler,根据触发器定义的时间规则调度任务。
- 调度线程从JobStore中获取需要执行的的触发器列表,并修改触发器的状态;
- Fire触发器,修改触发器信息(下次执行触发器的时间,以及触发器状态),并存储起来。
- 最后创建具体的执行任务对象,通过worker线程池执行任务。
接下来再聊聊 Quartz 的集群部署方案。
Quartz的集群部署方案,需要针对不同的数据库类型(MySQL , ORACLE) 在数据库实例上创建Quartz表,JobStore是: JobStoreSupport 。
这种方案是分布式的,没有负责集中管理的节点,而是利用数据库行级锁的方式来实现集群环境下的并发控制。
scheduler实例在集群模式下首先获取{0}LOCKS表中的行锁,Mysql 获取行锁的语句:
这个架构解决了任务的分布式调度问题,同一个任务只能有一个节点运行,其他节点将不执行任务,当碰到大量短任务时,各个节点频繁的竞争数据库锁,节点越多性能就会越差。
2 分布式锁模式
Quartz的集群模式可以水平扩展,也可以分布式调度,但需要业务方在数据库中添加对应的表,有一定的强侵入性。
有不少研发同学为了避免这种侵入性,也探索出分布式锁模式。
业务场景:电商项目,用户下单后一段时间没有付款,系统就会在超时后关闭该订单。
通常我们会做一个定时任务每两分钟来检查前半小时的订单,将没有付款的订单列表查询出来,然后对订单中的商品进行库存的恢复,然后将该订单设置为无效。
我们使用Spring Schedule的方式做一个定时任务。
在单服务器运行正常,考虑到高可用,业务量激增,架构会演进成集群模式,在同一时刻有多个服务执行一个定时任务,有可能会导致业务紊乱。
解决方案是在任务执行的时候,使用Redis 分布式锁来解决这类问题。
应用内部定义任务类,实现SimpleJob接口,编写自己任务的实际业务流程即可。
举例:应用A有五个任务需要执行,分别是A,B,C,D,E。任务E需要分成四个子任务,应用部署在两台机器上。
调度中心依赖Quartz集群模式,当任务调度时候,发送消息到RabbitMQ 。业务应用收到任务消息后,消费任务信息。
这种模型充分利用了MQ解耦的特性,调度中心发送任务,应用方作为执行器的角色,接收任务并执行。
但这种设计强依赖消息队列,可扩展性和功能,系统负载都和消息队列有极大的关联。这种架构设计需要架构师对消息队列非常熟悉。
4.2 XXL-JOB
XXL-JOB 是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
调度中心和执行器 两个模块之间通讯是 server-worker 模式。调度中心本身就是一个SpringBoot 工程,启动会监听8080端口。
执行器启动后,会启动内置服务( EmbedServer )监听9994端口。这样双方都可以给对方发送命令。
那调度中心如何知道执行器的地址信息呢 图中,执行器会定时发送注册命令 ,这样调度中心就可以获取在线的执行器列表。
通过执行器列表,就可以根据任务配置的路由策略选择节点执行任务。常见的路由策略有如下三种:
- 随机节点执行:选择集群中一个可用的执行节点执行调度任务。适用场景:离线订单结算。
分片执行:按照用户自定义分片逻辑进行拆分,分发到集群中不同节点并行执行,提升资源利用效率。适用场景:海量日志统计。
已过期的任务需要立刻执行的,直接放入线程池中触发执行 ,五秒内需要执行的任务放到 ringData 对象里。
ringThread 启动后,定时从 ringData 对象里获取需要执行的任务列表 ,放入到线程池中触发执行。
5 自研在巨人的肩膀上
2018年,我有一段自研任务调度系统的经历。
背景是:兼容技术团队自研的RPC框架,技术团队不需要修改代码,RPC注解方法可以托管在任务调度系统中,直接当做一个任务来执行。
自研过程中,研读了XXL-JOB 源码,同时从阿里云分布式任务调度 SchedulerX 吸取了很多营养。
我选择了 RocketMQ 源码的通讯模块 remoting 作为自研调度系统的通讯框架。基于如下两点:
-
我对业界大名鼎鼎的 Dubbo不熟悉,而remoting我已经做了多个轮子,我相信自己可以搞定;
-
在阅读 SchedulerX 1.0 client 源码中,发现 SchedulerX 的通讯框架和RocketMQ Remoting很多地方都很类似。它的源码里有现成的工程实现,完全就是一个宝藏。
我将 RocketMQ remoting 模块去掉名字服务代码,做了一定程度的定制。
在RocketMQ的remoting里,服务端采用 Processor 模式。
搞定 络通讯后,调度器如何设计 终我还是选择了Quartz 集群模式。主要是基于以下几点原因:
- 调度量不大的情况下 ,Quartz 集群模式足够稳定,而且可以兼容原来的XXL-JOB任务;
- 使用时间轮的话,本身没有足够的实践经验,担心出问题。 另外,如何让任务通过不同的调度服务(schedule-server)触发, 需要有一个协调器。于是想到Zookeeper。但这样的话,又引入了新的组件。
- 研发周期不能太长,想快点出成果。
自研版的调度服务花费一个半月上线了。系统运行非常稳定,研发团队接入也很顺畅。 调度量也不大 ,四个月总共接近4000万到5000万之间的调度量。
坦率的讲,自研版的瓶颈,我的脑海里经常能看到。 数据量大,我可以搞定分库分表,但 Quartz 集群基于行级锁的模式 ,注定上限不会太高。
为了解除心中的困惑,我写一个轮子DEMO看看可否work:
- 去掉外置的注册中心,调度服务(schedule-server)管理会话;
- 引入zookeeper,通过zk协调调度服务。但是HA机制很粗糙,相当于一个任务调度服务运行,另一个服务standby;
- Quartz 替换成时间轮 (参考Dubbo里的时间轮源码)。
文章提到:
每个应用都会做三备份,通过 zk 抢锁,一主两备,如果某台 Server 挂了,会进行 failover,由其他 Server 接管调度任务。
这次自研任务调度系统从架构来讲,并不复杂,实现了XXL-JOB的核心功能,也兼容了技术团队的RPC框架,但并没有实现工作流以及mapreduce分片。
SchedulerX 在升级到2.0之后基于全新的Akka 架构,这种架构 称实现高性能工作流引擎,实现进程间通信,减少 络通讯代码。
在我调研的开源任务调度系统中,PowerJob也是基于Akka 架构,同时也实现了工作流和MapReduce执行模式。
我对PowerJob非常感兴趣,也会在学习实践后输出相关文章,敬请期待。
6 技术选型
首先我们将任务调度开源产品和商业产品 SchedulerX 放在一起,生成一张对照表:
文章知识点与官方知识档案匹配,可进一步学习相关知识Java技能树首页概览91438 人正在系统学习中
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!