关于任务调度:实现一个任务调度系统看这篇就够了

15次阅读

共计 7284 个字符,预计需要花费 19 分钟才能阅读完成。

浏览一篇「定时工作框架选型」的文章时,一位网友的留言 到了我:

我看过那么多所谓的教程,大部分都是教“如何应用工具”的,没有多少是教“如何制作工具”的,能教“如何仿造工具”的都曾经是百里挑一,中国 软件行业,缺的是真正能够“制作工具”的程序员,而相对不缺那些“应用工具”的程序员!……”这个业界最不须要的就是“会应用 XX 工具的工程师”,而是“有创造力的软件工程师”!业界所有的饭碗,实质就是“有创造力的软件工程师”提供进去的啊!

写这篇文章,想和大家从头到脚说说任务调度,心愿大家读完之后,可能了解实现一个任务调度零碎的外围逻辑。

1 Quartz

Quartz 是一款 Java 开源任务调度框架,也是很多 Java 工程师接触任务调度的终点。

下图显示了任务调度的整体流程:

Quartz 的外围是三个组件。

  • 工作:Job 用于示意被调度的工作;
  • 触发器:Trigger 定义调度工夫的元素,即依照什么工夫规定去执行工作。一个 Job 能够被多个 Trigger 关联,然而一个 Trigger 只能关联一个 Job;
  • 调度器:工厂类创立 Scheduler,依据触发器定义的工夫规定调度工作。

上图代码中 Quartz 的 JobStore 是 RAMJobStore,Trigger 和 Job 存储在内存中。

执行任务调度的外围类是 QuartzSchedulerThread

  1. 调度线程从 JobStore 中获取须要执行的的触发器列表,并批改触发器的状态;
  2. <font color=”red”>Fire</font> 触发器,批改触发器信息(下次执行触发器的工夫,以及触发器状态),并存储起来。
  3. 最初创立具体的执行工作对象,通过 worker 线程池执行工作。

接下来再聊聊 Quartz 的集群部署计划。

Quartz 的集群部署计划,须要针对不同的数据库类型(MySQL , ORACLE) 在数据库实例上创立 Quartz 表,JobStore 是: JobStoreSupport

这种计划是分布式的,没有负责集中管理的节点,而是利用数据库行级锁的形式来实现集群环境下的并发管制。

scheduler 实例在集群模式下首先获取{0}LOCKS 表中的行锁,Mysql 获取行锁的语句:

{0}会替换为配置文件默认配置的QRTZ_。sched_name 为利用集群的实例名,lock_name 就是行级锁名。Quartz 次要有两个行级锁触发器拜访锁 (TRIGGER_ACCESS) 和 状态拜访锁(STATE_ACCESS)。

这个架构解决了工作的散布式调度问题,同一个工作只能有一个节点运行,其余节点将不执行工作,当碰到大量短工作时,各个节点频繁的竞争数据库锁,节点越多性能就会越差。

2 分布式锁模式

Quartz 的集群模式能够程度扩大,也能够散布式调度,但须要业务方在数据库中增加对应的表,有肯定的强侵入性。

有不少研发同学为了防止这种侵入性,也摸索出 分布式锁模式

业务场景:电商我的项目,用户下单后一段时间没有付款,零碎就会在超时后敞开该订单。

通常咱们会做一个定时工作每两分钟来查看前半小时的订单,将没有付款的订单列表查问进去,而后对订单中的商品进行库存的复原,而后将该订单设置为有效。

咱们应用 Spring Schedule 的形式做一个定时工作。

@Scheduled(cron = "0 */2 * * * ?")
public void doTask() {log.info("定时工作启动");
   // 执行敞开订单的操作
   orderService.closeExpireUnpayOrders();
   log.info("定时工作完结");
 }

在单服务器运行失常,思考到高可用,业务量激增,架构会演进成集群模式,在 同一时刻 有多个服务执行一个定时工作,有可能会导致业务错乱。

解决方案是在工作执行的时候,应用 Redis 分布式锁来解决这类问题。

@Scheduled(cron = "0 */2 * * * ?")
public void doTask() {log.info("定时工作启动");
    String lockName = "closeExpireUnpayOrdersLock";
    RedisLock redisLock = redisClient.getLock(lockName);
    // 尝试加锁,最多期待 3 秒,上锁当前 5 分钟主动解锁
    boolean locked = redisLock.tryLock(3, 300, TimeUnit.SECONDS);
    if(!locked){log.info("没有取得分布式锁:{}" , lockName);
        return;
    }
    try{
       // 执行敞开订单的操作
       orderService.closeExpireUnpayOrders();} finally {redisLock.unlock();
    }
    log.info("定时工作完结");
}

Redis 的读写性能极好,分布式锁也比 Quartz 数据库行级锁更轻量级。当然 Redis 锁也能够替换成 Zookeeper 锁,也是同样的机制。

在小型我的项目中,应用:定时工作框架(Quartz/Spring Schedule)和 分布式锁(redis/zookeeper)有不错的成果。

然而呢?咱们能够发现这种组合有两个问题:

  1. 定时工作在分布式场景下有空跑的状况,而且工作也无奈做到分片;
  2. 要想手工触发工作,必须增加额定的代码能力实现。

3 ElasticJob-Lite 框架

ElasticJob-Lite 定位为轻量级无中心化解决方案,应用 jar 的模式提供分布式工作的协调服务。

利用外部定义工作类,实现 SimpleJob 接口,编写本人工作的理论业务流程即可。

public class MyElasticJob implements SimpleJob {
    @Override
    public void execute(ShardingContext context) {switch (context.getShardingItem()) {
            case 0:
                // do something by sharding item 0
                break;
            case 1:
                // do something by sharding item 1
                break;
            case 2:
                // do something by sharding item 2
                break;
            // case n: ...
        }
    }
}

举例:利用 A 有五个工作须要执行,别离是 A,B,C,D,E。工作 E 须要分成四个子工作,利用部署在两台机器上。

利用 A 在启动后,5 个工作通过 Zookeeper 协调后被调配到两台机器上,通过 Quartz Scheduler 离开执行不同的工作。

ElasticJob 从实质上来讲,底层任务调度还是通过 Quartz,相比 Redis 分布式锁 或者 Quartz 分布式部署,它的劣势在于能够依赖 Zookeeper 这个大杀器,将工作通过负载平衡算法调配给利用内的 Quartz Scheduler 容器。

从使用者的角度来讲,是非常简单易用的。但从架构来看,调度器和执行器仍然在同一个利用方 JVM 内,而且容器在启动后,仍然须要做负载平衡。利用如果频繁的重启,一直的去选主,对分片做负载平衡,这些都是绝对比拟 的操作。

另外,ElasticJob 的控制台是比拟毛糙的,通过读取注册核心数据展示作业状态,更新注册核心数据批改全局工作配置。

4 中心化流派

中心化的原理是:把调度和工作执行,隔离成两个局部:调度核心和执行器。调度核心模块只须要负责任务调度属性,触发调度命令。执行器接管调度命令,去执行具体的业务逻辑,而且两者都能够进行分布式扩容。

4.1 MQ 模式

先谈谈我在艺龙促销团队接触的第一种中心化架构。

调度核心依赖 Quartz 集群模式,当任务调度时候,发送音讯到 RabbitMQ。业务利用收到工作音讯后,生产工作信息。

这种模型充分利用了 MQ 解耦的个性,调度核心发送工作,利用方作为执行器的角色,接管工作并执行。

但这种设计强依赖音讯队列,可扩展性和性能,零碎负载都和音讯队列有极大的关联。这种架构设计须要架构师对音讯队列十分相熟。

4.2 XXL-JOB

XXL-JOB 是一个分布式任务调度平台,其外围设计指标是开发迅速、学习简略、轻量级、易扩大。现已凋谢源代码并接入多家公司线上产品线,开箱即用。

咱们重点分析下架构图:

▍ 网络通讯 server-worker 模型

调度核心和执行器 两个模块之间通信是 server-worker 模式。调度核心自身就是一个 SpringBoot 工程,启动会监听 8080 端口。

执行器启动后,会启动内置服务(EmbedServer)监听 9994 端口。这样单方都能够给对方发送命令。

那调度核心如何晓得执行器的地址信息呢?上图中,执行器会定时发送注册命令,这样调度核心就能够获取在线的执行器列表。

通过执行器列表,就能够依据工作配置的路由策略抉择节点执行工作。常见的路由策略有如下三种:

  • 随机节点执行:抉择集群中一个可用的执行节点执行调度工作。实用场景:离线订单结算。

  • 播送执行:在集群中所有的执行节点散发调度工作并执行。实用场景:批量更新利用本地缓存。

  • 分片执行:依照用户自定义分片逻辑进行拆分,散发到集群中不同节点并行执行,晋升资源利用效率。实用场景:海量日志统计。

▍ 调度器

调度器是任务调度零碎外面十分外围的组件。XXL-JOB 的晚期版本是依赖 Quartz。

但在 v2.1.0 版本中齐全去掉了 Quartz 的依赖,原来须要创立的 Quartz 表也替换成了自研的表。

外围的调度类是:JobTriggerPoolHelper。调用 start 办法后,会启动两个线程:scheduleThread 和 ringThread。

首先 scheduleThread 会定时从数据库加载须要调度的工作,这里从实质上还是基于数据库行锁保障同时只有一个调度核心节点触发任务调度。

Connection conn = XxlJobAdminConfig.getAdminConfig()
                  .getDataSource().getConnection();
connAutoCommit = conn.getAutoCommit();
conn.setAutoCommit(false);
preparedStatement = conn.prepareStatement("select * from xxl_job_lock where lock_name ='schedule_lock'for update");
preparedStatement.execute();
# 触发任务调度 (伪代码)
for (XxlJobInfo jobInfo: scheduleList) {// 省略代码}
# 事务提交
conn.commit();

调度线程会依据工作的「下次触发工夫」,采取不同的动作:

已过期的工作须要立即执行的,间接放入线程池中触发执行,五秒内须要执行的工作放到 ringData 对象里。

ringThread 启动后,定时从 ringData 对象里获取须要执行的工作列表,放入到线程池中触发执行。


5 自研在伟人的肩膀上

2018 年,我有一段自研任务调度零碎的经验。

背景是:兼容技术团队自研的 RPC 框架,技术团队不须要批改代码,RPC 注解办法能够托管在任务调度零碎中,间接当做一个工作来执行。

自研过程中,研读了 XXL-JOB 源码,同时从阿里云分布式任务调度 SchedulerX 汲取了很多养分。

  • Schedulerx-console 是任务调度的控制台,用于创立、治理定时工作。负责数据的创立、批改和查问。在产品外部与 schedulerx-server 交互。
  • Schedulerx-server 是任务调度的服务端,是 Scheduler 的外围组件。负责客户端工作的调度触发以及工作执行状态的监测。
  • Schedulerx-client 是任务调度的客户端。每个接入客户端的利用过程就是一个的 Worker。Worker 负责与 schedulerx-server 建设通信,让 schedulerx-server 发现客户端的机器。并向 schedulerx-server 注册以后利用所在的分组,这样 schedulerx-server 能力向客户端定时触发工作。

咱们模拟了 SchedulerX 的模块,架构设计如下图:

我抉择了 RocketMQ 源码的通信模块 remoting 作为自研调度零碎的通信框架。基于如下两点:

  1. 我对业界赫赫有名的 Dubbo 不相熟,而 remoting 我曾经做了多个轮子,我置信本人能够搞定;
  2. 在浏览 SchedulerX 1.0 client 源码中,发现 SchedulerX 的通信框架和 RocketMQ Remoting 很多中央都很相似。它的源码里有现成的工程实现,齐全就是一个宝藏。

我将 RocketMQ remoting 模块去掉名字服务代码,做了肯定水平的定制。

在 RocketMQ 的 remoting 里,服务端采纳 Processor 模式。

调度核心须要注册两个处理器:回调后果处理器 CallBackProcessor 和心跳处理器 HeartBeatProcessor。执行器须要注册触发工作处理器 TriggerTaskProcessor。

public void registerProcessor(
             int requestCode,
             NettyRequestProcessor processor,
             ExecutorService executor);

处理器的接口:

public interface NettyRequestProcessor {
 RemotingCommand processRequest(
                 ChannelHandlerContext ctx,
                 RemotingCommand request) throws Exception;
 boolean rejectRequest();}

对于通信框架来讲,我并不需要关注通信细节,只须要实现处理器接口即可。

以触发工作处理器 TriggerTaskProcessor 举例:

搞定网络通讯后,调度器如何设计?最终我还是抉择了 Quartz 集群模式。次要是基于以下几点起因:

  1. 调度量不大的状况下,Quartz 集群模式足够稳固,而且能够兼容原来的 XXL-JOB 工作;
  2. 应用工夫轮的话,自身没有足够的实践经验,放心出问题。另外,如何让工作通过不同的调度服务(schedule-server)触发,须要有一个协调器。于是想到 Zookeeper。但这样的话,又引入了新的组件。
  3. 研发周期不能太长,想快点出成绩。

自研版的调度服务破费一个半月上线了。零碎运行十分稳固,研发团队接入也很顺畅。调度量也不大,四个月总共靠近 4000 万到 5000 万之间的调度量。

坦白的讲,自研版的瓶颈,我的脑海里常常能看到。数据量大,我能够搞定分库分表,但 Quartz 集群基于行级锁的模式,注定下限不会太高。

为了解除心中的困惑,我写一个轮子 DEMO 看看可否 work:

  1. 去掉外置的注册核心,调度服务(schedule-server)治理会话;
  2. 引入 zookeeper,通过 zk 协调调度服务。然而 HA 机制很毛糙,相当于一个任务调度服务运行,另一个服务 standby;
  3. Quartz 替换成工夫轮(参考 Dubbo 里的工夫轮源码)。

这个 Demo 版本在开发环境能够运行,但有很多细节须要优化,仅仅是个玩具,并没有机会运行到生产环境。

最近读阿里云的一篇文章《如何通过任务调度实现百万规定报警》,SchedulerX2.0 高可用架构见下图:

文章提到:

每个利用都会做三备份,通过 zk 抢锁,一主两备,如果某台 Server 挂了,会进行 failover,由其余 Server 接管调度工作。

这次自研任务调度零碎从架构来讲,并不简单,实现了 XXL-JOB 的外围性能,也兼容了技术团队的 RPC 框架,但并没有实现工作流以及 mapreduce 分片。

SchedulerX 在降级到 2.0 之后基于全新的 Akka 架构,这种架构 号称 实现高性能工作流引擎,实现过程间通信,缩小网络通讯代码。

在我调研的开源任务调度零碎中,PowerJob也是基于 Akka 架构,同时也实现了工作流和 MapReduce 执行模式。

我对 PowerJob 十分感兴趣,也会在学习实际后输入相干文章,敬请期待。

6 技术选型

首先咱们将任务调度开源产品和商业产品 SchedulerX 放在一起,生成一张对照表:

Quartz 和 ElasticJob 从实质上还是属于框架的层面。

中心化产品从架构上来讲更加清晰,调度层面更灵便,能够反对更简单的调度(mapreduce 动静分片,工作流)。

XXL-JOB 从产品层面曾经做到极简,开箱即用,调度模式能够满足大部分研发团队的需要。简略易用 + 能打,所以十分受大家欢送。

其实每个技术团队的技术储备不尽相同,面对的场景也不一样,所以技术选型并不能一概而论。

不论是应用哪种技术,在编写工作业务代码时,还是须要留神两点:

  • 幂等。当工作被反复执行的时候,或者分布式锁生效的时候,程序仍然能够输入正确的后果;
  • 工作不跑了,千万别惊恐。查看调度日志,JVM 层面应用 Jstack 命令查看堆栈,网络通讯要增加超时工夫,个别能解决大部分问题。

7 写到最初

2015 年其实是十分乏味的一年。ElasticJob 和 XXL-JOB 这两种不同流派的任务调度我的项目都开源了。

在 XXL-JOB 源码里,至今还保留着许雪里老师在开源中国的一条动静截图:

刚写的任务调度框架,Web 动静治理工作,实时失效,热乎的。没有意外的话,今天中午推送到 git.osc 下来。哈哈,下楼炒个面加个荷包蛋庆贺下。

看到这个截图,内心深处居然会有一种共情,嘴角不自禁的上扬。

我又想起:2016 年,ElasticJob 的作者张亮老师开源了 sharding-jdbc。我在 github 上创立了一个公有我的项目,参考 sharding-jdbc 的源码,本人实现分库分表的性能。第一个类名叫:ShardingDataSource,工夫定格在 2016/3/29。

我不晓得如何定义“有创造力的软件工程师”,但我置信:一个有好奇心,努力学习,乐于分享,违心去帮忙他人的工程师,运气必定不会太差。


感觉对您有帮忙的话,请给作者一个「点赞」和「珍藏」,咱们下期见。

正文完
 0