面试官问,怎么实现一个定时任务调度器?如果是分布式下的呢?
首先要思考一下通用的定时器大概包含哪些要素?首先,应该听过Quartz,SpringSchedule等框架;往分布式研究,又有SchedulerX,ElasticJob等分布式任务调度。那么往底层实现看,又有多种定时器实现方案的原理、工作效率、数据结构等等可以进行思考。
那么抽象来说,定时器大概包含如下属性,判断一个任务是否到期,基本会采用轮询的方式,每隔一个时间片去检查最近的任务是否到期,并且,在NewTask和Cancel的行为发生之后,任务调度策略也会出现调整。存储一系列的任务集合,并且Deadline越接近的任务,拥有越高的执行优先级NewTask:将新任务加入任务集合Cancel:取消某个任务Run:执行一个到期的定时任务
框架列举【单机】ScheduledExecutorService:相对延迟或者周期作为定时任务调度,缺点没有绝对的日期或者时间【单机】spring定时框架:配置简单功能较多,如果系统使用单机的话可以优先考虑spring定时器【分布式】Quartz:Java事实上的定时任务标准。但是Quartz可以基于数据库实现作业的高可用,有侵入性,而且缺少分布式并行调度的功能【分布式】Springbatch:轻量级的,完全面向Spring的批处理框架,可以应用于企业级大量的数据处理系统。SpringBatch可以提供大量的,可重复的数据处理功能,包括日志记录跟踪,事务管理,作业处理统计工作重新启动、跳过,和资源管理等重要功能。【分布式】elasticjob:当当开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,实现任务高可用以及分片【分布式】xxljob:是大众点评员工徐雪里于2015年发布的分布式任务调度平台,是一个轻量级分布式任务调度框架,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。支持集群部署xjob:保证每个集群节点配置(db和登陆账号等)保持一致。调度中心通过db配置区分不同集群。执行器支持集群部署,提升调度系统可用性,同时提升任务处理能力。集群部署唯一要求为:保证集群中每个执行器的配置项xxl。job。admin。addresses调度中心地址保持一致,执行器根据该配置进行执行器自动注册等操作。ejob:重写Quartz基于数据库的分布式功能,用Zookeeper实现注册中心。作业注册中心:基于Zookeeper和其客户端Curator实现的全局作业注册控制中心。用于注册,控制和协调分布式作业执行。多节点部署时任务不能重复执行xjob:使用Quartz基于数据库的分布式功能ejob:将任务拆分为n个任务项后,各个服务器分别执行各自分配到的任务项。一旦有新的服务器加入集群,或现有服务器下线,elasticjob将在保留本次任务执行不变的情况下,下次任务开始前触发任务重分片。日志xjob:支持日志可追溯,有日志查询界面ejob:可通过事件订阅的方式处理调度过程的重要事件,用于查询、统计和监控。ElasticJob目前提供了基于关系型数据库两种事件订阅方式记录事件监控报警xjob:调度失败时,将会触发失败报警,如发送报警邮件。任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔ejob:通过事件订阅方式可自行实现弹性扩容缩容xjob:使用Quartz基于数据库的分布式功能,服务器超出一定数量会给数据库造成一定的压力ejob:通过zk实现各服务的注册、控制及协调支持并行调度xjob:调度系统多线程(默认10个线程)触发调度运行,确保调度精确执行,不被堵塞ejob:采用任务分片方式实现。将一个任务拆分为n个独立的任务项,由分布式的服务器并行执行各自分配到的分片项高可用策略xjob:调度中心通过DB锁保证集群分布式调度的一致性,一次任务调度只会触发一次执行ejob:调度器的高可用是通过运行几个指向同一个ZooKeeper集群的ElasticJobCloudScheduler实例来实现的。ZooKeeper用于在当前主ElasticJobCloudScheduler实例失败的情况下执行领导者选举。通过至少两个调度器实例来构成集群,集群中只有一个调度器实例提供服务,其他实例处于待命状态。当该实例失败时,集群会选举剩余实例中的一个来继续提供服务失败处理策略xjob:调度失败时的处理策略,策略包括:失败告警(默认)、失败重试ejob:弹性扩容缩容在下次作业运行前重分片,但本次作业执行的过程中,下线的服务器所分配的作业将不会重新被分配。失效转移功能可以在本次作业运行中用空闲服务器抓取孤儿作业分片执行。同样失效转移功能也会牺牲部分性能对比总结xJob侧重的业务实现的简单和管理的方便,学习成本简单,失败策略和路由策略丰富。推荐使用在用户基数相对少,服务器数量在一定范围内的情景下使用eJob关注的是数据,增加了弹性扩容和数据分片的思路,以便于更大限度的利用分布式服务器的资源。但是学习成本相对高些,推荐在数据量庞大,且部署服务器数量较多时使用
数据结构
我们主要衡量NewTask(新增任务),Cancel(取消任务),Run(执行到期的定时任务)这三个指标,分析时间复杂度和空间复杂度。双向有序链表(LinkedList)
NewTask:O(N)
Cancel:O(1)
Run:O(1)
N:任务数堆(PriorityQueue)
在Java中,PriorityQueue是一个天然的堆,可以利用传入的Comparator来决定其中元素的优先级。
NewTask:O(logN)
Cancel:O(logN)
Run:O(1)
N:任务数
expireTime是Comparator的对比参数。NewTaskO(logN)和CancelO(logN)分别对应堆插入和删除元素的时间复杂度;RunO(1),由expireTime形成的小根堆,我们总能在堆顶找到最快的即将过期的任务。时间轮
Netty针对IO超时调度的场景进行了优化,实现了HashedWheelTimer时间轮算法。
HashedWheelTimer是一个环形结构,可以用时钟来类比,钟面上有很多bucket,每一个bucket上可以存放多个任务,使用一个List保存该时刻到期的所有任务,同时一个指针随着时间流逝一格一格转动,并执行对应bucket上所有到期的任务。任务通过取模决定应该放入哪个bucket。和HashMap的原理类似,newTask对应put,使用List来解决Hash冲突。
NewTask:O(1)
Cancel:O(1)
Run:O(M)
Tick:O(1)
M:bucket,MNC,其中C为单轮bucket数,Netty中默认为512
构造Netty的HashedWheelTimer时有两个重要的参数:tickDuration和ticksPerWheel。tickDuration:即一个bucket代表的时间,默认为100ms,Netty认为大多数场景下不需要修改这个参数;ticksPerWheel:一轮含有多少个bucket,默认为512个,如果任务较多可以增大这个参数,降低任务分配到同一个bucket的概率。层级时间轮
Kafka针对时间轮算法进行了优化,实现了层级时间轮TimingWheel
调度和任务分开执行
把调度和任务执行,隔离成两个部分:调度中心和执行器。调度中心模块只需要负责任务调度属性,触发调度命令。执行器接收调度命令,去执行具体的业务逻辑,而且两者都可以进行分布式扩容。
MQ模式
调度中心依赖Quartz集群模式,当任务调度时候,发送消息到RabbitMQ。业务应用收到任务消息后,消费任务信息。
这种模型充分利用了MQ解耦的特性,调度中心发送任务,应用方作为执行器的角色,接收任务并执行。
但这种设计强依赖消息队列,可扩展性和功能,系统负载都和消息队列有极大的关联。这种架构设计需要架构师对消息队列非常熟悉。
XXLJOB
XXLJOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。