我看过那么多所谓的教程,大部分都是教“如何使用工具”,教“如何制作工具”的不多,能教“如何制作工具”的不多模仿工具”已经非常罕见了。中国软件业缺的是真正会“造工具”的程序员,但绝对不缺会“用工具”的程序员!……”这个行业最不需要的是“会使用XX工具的工程师”,而是“创意软件工程师”!这个行业所有的岗位,本质上都是由“创意软件工程师”提供出来的!写这篇文章,想和大家从头到脚的聊一聊任务调度,希望大家看完之后能够理解实现一个任务调度系统的核心逻辑1QuartzQuartz是一个Java开源的任务调度框架,有很多Java工程师接触任务调度的起点,下图展示了任务调度的整体流程:Quartz的核心是三个组件。调度时间,即按照什么Time规则执行任务。一个Job可以关联多个Trigger,但是一个Trigger只能关联一个Job;Scheduler:工厂类创建一个Scheduler并进行调度根据触发器定义的时间规则执行任务。上面代码中Quartz的JobStore是RAMJobStore,Trigger和Job是存放在内存中的。执行任务调度的核心类是QuartzSchedulerThread。调度线程从JobStore中获取需要执行的触发器列表,修改触发器的状态;触发触发器修改触发器信息(下面是第一次执行触发器的时间,以及触发器的状态),并存储起来。最后创建具体的执行任务对象,通过工作线程池执行任务。接下来说一下Quartz的集群部署方案。Quartz的集群部署方案需要在不同数据库类型(MySQL、ORACLE)的数据库实例上创建Quartz表。JobStore是:JobStoreSupport。本方案是分布式的,没有节点负责集中管理。相反,集群是在环境中使用数据库行级锁并发控制来实现的。集群模式下,scheduler实例首先获取{0}LOCKS表中的行锁,Mysql获取行锁语句:{0}将替换为配置文件中默认配置的QRTZ_。sched_name是应用集群的实例名,lock_name是行级锁名。Quartz主要有两种行级锁触发访问锁(TRIGGER_ACCESS)和状态访问锁(STATE_ACCESS)。这种架构解决了任务的分布式调度问题。只有一个节点可以运行同一个任务,其他节点不会执行该任务。当遇到大量的短任务时,各个节点会频繁的争夺数据库锁。节点越多,性能会下降。更差。2分布式锁模式Quartz的集群模式可以水平扩展,也可以分布式调度,但是需要业务方在数据库中添加相应的表,侵入性强。为了避免这种侵入性,很多研发同学也对分布式锁模式进行了探索。业务场景:对于一个电商项目,如果用户下单后一段时间内未付款,系统会超时关闭订单。通常我们会每隔两分钟做一个定时任务,查看前半小时的订单,查询未付款的订单列表,然后恢复订单中商品的库存,然后将订单设置为无效.我们使用SpringSchedule来做一个定时任务。@Scheduled(cron="0*/2***?")publicvoiddoTask(){log.info("定时任务启动");//执行关闭订单操作orderService.closeExpireUnpayOrders();log.info("定时任务结束任务");}单机运行正常。考虑到高可用和业务量的激增,架构将演进为集群模式。多个服务同时执行一个定时任务,可能导致业务混乱。解决方案是在任务执行过程中使用Redis分布式锁来解决此类问题。@Scheduled(cron="0*/2***?")publicvoiddoTask(){log.info("定时任务启动");StringlockName="closeExpireUnpayOrdersLock";RedisLockredisLock=redisClient.getLock(lockName);//尝试添加Lock,最多等待3秒,加锁5分钟后自动解锁booleanlocked=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/SpringSchedule)和分布式锁(redis/zookeeper)都有不错的效果。但?我们可以发现这种组合存在两个问题:定时任务在分布式场景下可能跑空,任务不能分片;如果你想手动触发任务,你必须添加额外的代码来完成它。3ElasticJob-Lite框架ElasticJob-Lite定位为轻量级的去中心化解决方案,以jar的形式为分布式任务提供协调服务。在应用程序内部定义任务类,实现SimpleJob接口,编写自己任务的实际业务流程。publicclassMyElasticJobimplementsSimpleJob{@Overridepublicvoidexecute(ShardingContextcontext){switch(context.getShardingItem()){case0://dosomethingbyshardingitem0break;case1://dosomethingbyshardingitem1break;case2://dosomethingbyshardingitem2break;//casen:Applicationhas}}需要执行五个任务,即A、B、C、D、E。任务E需要分成四个子任务,应用部署在两台机器上。应用A启动后,5个任务由Zookeeper协调分发到两台机器上,不同的任务由QuartzScheduler分别执行。ElasticJob的底层任务调度本质上还是通过Quartz。与Redis分布式锁或者Quartz分布式部署相比,它的优势在于可以依赖Zookeeper这个大杀器,通过负载均衡算法将任务分配给应用中的QuartzScheduler。容器。从用户的角度来看,它非常简单易用。但是从架构上看,scheduler和executor还是在同一个应用端JVM中,容器启动后,还是需要做负载均衡的。如果应用频繁重启,不断的选主,对分片做负载均衡,这些都是比较重的操作。ElasticJob的控制台通过读取注册表数据显示作业状态,更新注册表数据修改全局任务配置。从任务调度平台的角度来看,控制台的功能还很薄弱。4集中化集中化的原则是将调度和任务执行分离为调度中心和执行者两部分。调度中心模块只需要负责任务调度属性和触发调度命令即可。执行器接收调度命令执行特定的业务逻辑,两者都可以进行分布式扩展。4.1MQ模式先说一下我在艺龙推广团队接触到的第一个中心化架构。调度中心依赖于Quartz集群模式,在任务调度时向RabbitMQ发送消息。业务应用收到任务消息后,消费任务信息。该模型充分利用了MQ的解耦特性。调度中心发送任务,应用端作为执行者接收和执行任务。但这种设计强烈依赖消息队列、可扩展性和功能性,系统负载与消息队列有很大关系。这种架构设计需要架构师对消息队列非常熟悉。4.2XXL-JOBXXL-JOB是一个分布式任务调度平台。其核心设计目标是快速开发、易学、轻量、易扩展。现已开放源码,接入多家公司线上产品线,开箱即用。xxl-job2.3.0架构图重点看下面的架构图:▍网络通信server-worker模型调度中心和执行器两个模块之间的通信是server-worker模式。调度中心本身是一个SpringBoot项目,启动时会监听8080端口。执行器启动后,会启动内置服务(EmbedServer)监听9994端口,这样双方就可以互相发送命令了。调度中心如何知道执行器的地址信息?上图中,执行器会定时发送注册命令,以便调度中心获取在线执行器列表。通过执行者列表,可以根据任务配置的路由策略选择节点执行任务。常见的路由策略有以下三种:随机节点执行:在集群中选择一个可用的执行节点执行调度任务。适用场景:线下订单结算。广播执行:分发调度任务,在集群所有执行节点上执行。适用场景:批量更新应用本地缓存。分片执行:按照用户自定义的分片逻辑进行拆分,分布到集群中的不同节点并行执行,提高资源利用效率。适用场景:海量日志统计。▍Scheduler调度器是任务调度系统中非常核心的一个组件。XXL-JOB的早期版本依赖于Quartz。不过在v2.1.0版本中,完全去掉了Quartz依赖,将需要创建的Quartz表替换为自研表。核心调度类是:JobTriggerPoolHelper。调用start方法后,会启动两个线程:scheduleThread和ringThread。首先,scheduleThread会定时从数据库中加载需要调度的任务,本质上是基于数据库行锁来保证只有一个调度中心节点同时触发任务调度。Connectionconn=XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection();connAutoCommit=conn.getAutoCommit();conn.setAutoCommit(false);preparedStatement=conn.prepareStatement("select*fromxxl_job_lockwherelock_name='schedule_lock'forupdate");preparedStatement.execute();#触发任务调度(伪代码)for(XxlJobInfojobInfo:scheduleList){//省略代码}#事务提交conn.commit();调度线程会根据任务的“下次触发时间”采取不同的动作Action:如果已经过期的任务需要立即执行,则直接放入线程池中触发执行,将需要的任务放入在五秒内执行到ringData对象中。ringThread启动后,定时从ringData对象中获取需要执行的任务列表,放入线程池触发执行。5站在巨人的肩膀上自研2018年有过自研任务调度系统的经历。背景是:兼容技术团队自研的RPC框架,技术团队无需修改代码,RPC注解方法可以托管在任务调度系统中,直接作为任务执行。在自主开发的过程中,研究了XXL-JOB的源码,同时从阿里云的分布式任务调度SchedulerX中吸取了很多养分。SchedulerX1.0架构图Schedulerx-console是一个任务调度控制台,用于创建和管理计划任务。负责数据的创建、修改和查询。与产品内部的schedulerx服务器交互。Schedulerx-server是任务调度的服务器,是Scheduler的核心组件。负责客户端任务的调度和触发,以及任务执行状态的监控。Schedulerx-client是一个任务调度客户端。每个连接到客户端的应用进程都是一个Worker。Worker负责与Schedulerx-server建立通信,让schedulerx-server发现客户端机器。并向schedulerx-server注册当前应用所在的组,以便schedulerx-server定时向客户端触发任务。我们仿照了SchedulerX的模块,架构设计如下:选用RocketMQ源码的通信模块remoting作为自研调度系统的通信框架。基于以下两点:我对业界知名的Dubbo不熟悉,我做了多个remoting的轮子,我相信我能搞定;在阅读SchedulerX1.0客户端的源码时,我发现SchedulerX和RocketMQRemoting的通信框架在很多地方都非常相似。相似的。它的源码里有现成的工程实现,完全是个宝。我去掉了RocketMQremoting模块的名称服务代码,做了一定程度的定制。在RocketMQ的remoting中,服务端采用的是Processor模式。调度中心需要注册两个处理器:回调结果处理器CallBackProcessor和心跳处理器HeartBeatProcessor。执行者需要注册触发任务处理器TriggerTaskProcessor。publicvoidregisterProcessor(intrequestCode,NettyRequestProcessorprocessor,ExecutorServiceexecutor);处理器接口:publicinterfaceNettyRequestProcessor{RemotingCommandprocessRequest(ChannelHandlerContextctx,RemotingCommandrequest)throwsException;booleanrejectRequest();}对于通信框架,我不需要关注通信处理器接口的细节即可.以TriggerTaskProcessor为例:网络通信解决后调度器如何设计?最后我选择了Quartz集群模式。主要基于以下几个原因:当调度量不大时,Quartz集群模式足够稳定,兼容原有的XXL-JOB任务;如果使用时间轮,实践经验不足,担心出问题。另外,如何通过不同的调度服务(schedule-server)来触发任务,需要一个协调器。于是想到了Zookeeper。但是在这种情况下,引入了一个新组件。研发周期不要太长,要尽快出成果。自主研发的排班服务版本,历时一个半月上线。系统运行非常稳定,研发团队接入顺畅。排量不大,四个月总排量接近4000万到5000万。坦白说,我经常能在脑海中看到自研版本的瓶颈。数据量大,分库分表我可以搞定,但是Quartz集群是基于行级锁模式的,所以上限注定不会太高。为了排解心中的困惑,写了个轮子DEMO看看能不能行:去掉外部注册中心,由调度服务(schedule-server)管理session;引入zookeeper,通过zk协调调度服务。但是HA机制很粗糙,相当于一个任务调度服务运行,另一个服务待命;Quartz被时间轮替代(参考Dubbo中时间轮源码)。这个Demo版本可以在开发环境中运行,但是有很多细节需要优化。它只是一个玩具,没有机会在生产环境中运行。最近在阿里云上看到一篇文章《如何通过任务调度实现百万规则报警》,SchedulerX2.0高可用架构如下图所示:文章中提到每个应用都会有三备,通过zk抢锁,一主二备,如果一个服务器挂了,会进行故障转移,其他的Server会接管调度任务。在架构上,自研的任务调度系统并不复杂。实现了XXL-JOB的核心功能,也兼容技术团队的RPC框架,但没有实现workflow和mapreducesharding。SchedulerX基于升级到2.0后的全新Akka架构。该架构声称实现了一个高性能的工作流引擎,实现了进程间通信,减少了网络通信代码。在笔者调研的开源任务调度系统中,PowerJob也是基于Akka架构,同样实现了工作流和MapReduce执行模式。本人对PowerJob很感兴趣,学习实践后也会输出相关文章,敬请期待。6技术选型首先我们把任务调度的开源产品和商业产品SchedulerX放在一起,生成一张对比表:Quartz和ElasticJob本质上还是框架层面的。中心化产品在架构上更清晰,调度层面更灵活,可以支持更复杂的调度(mapreducedynamicsharding,workflow)。XXL-JOB从产品层面进行了简化,开箱即用,调度模式可以满足大部分研发团队的需求。好用+会玩,所以很受大家的欢迎。事实上,每个技术团队的技术储备不同,面对的场景也不同,所以技术选型不能一概而论。无论使用哪种技术,在编写任务业务代码时,仍然需要注意两点:幂等性。当任务重复执行时,或者分布式锁失效时,程序仍然可以输出正确的结果;如果任务停止运行,请不要惊慌。查看调度日志,使用Jstack命令查看JVM级别的堆栈,加上网络通信的超时时间,一般可以解决大部分问题。7总而言之,2015年实际上是非常有趣的一年。两种不同类型的任务调度项目ElasticJob和XXL-JOB是开源的。XXL-JOB源码中,还有开源中国许学礼老师的动态截图:刚刚写的任务调度框架,web动态管理任务,实时生效,温馨。如无意外,明天中午就会推送到git.osc。哈哈,我们下楼炒个面,加个荷包蛋庆祝一下。看到这张截图,内心深处其实有一种感同身受,嘴角不由得上扬。又想起了:2016年,ElasticJob的作者张亮老师开源了sharding-jdbc。自己在github上创建了一个私有项目,参考了sharding-jdbc的源码,自己实现了分库分表的功能。第一堂课的名字是:ShardingDataSource,时间固定在2016/3/29。我不知道如何定义一个“有创造力的软件工程师”,但我相信一个有好奇心、勤奋、乐于分享、乐于助人的工程师不会太幸运。
