摘要
从改造工作量、可用性、负载均衡、资源利用等方面,简单介绍了几种集群环境下定时任务调度的方案。
问题:从单机扩展到集群
单机环境的定时任务很简单。无论是用比较原始的Timer,还是用自成体系的quartz、spring-scheduler,都可以轻松写意的实现功能。
但是,当应用水平扩展到集群环境下时, 定时任务会出现重复调度、重复执行,可能带来资源浪费、数据错误等问题。
方案一:不做改造,直接扩展
对有些定时任务来说,重复调度、重复执行并不构成大问题。例如删除过期数据任务、数据监控任务等,除了会造成一定的资源浪费之外,其实无伤大雅。
从功能上来说,只有严格幂等的任务才可以直接扩展。性能方面来说,这个方案适合对网络、内存等资源压力小的定时任务。另外,如果定时任务需要处理的资源本身不在集群服务之间共享,那么必然要使用这个方案:例如检查服务本地缓存数据过期情况的定时任务就属于这种情况。
工作量
由于不需要对单机环境的定时任务进行改造,因此这个方案的改造工作量是非常小的。
只不过,在开发单机环境的定时任务时就要重点考虑如何保证任务的幂等性,这可能是一项额外的工作。
可用性
高可用性是没的说的。集群中的每台服务都有一套独立、完整的任务调度、执行功能,只要有一台服务存活,就可以保证任务完整执行。
负载均衡
在负载均衡方面,这个方案是最差的:它实际上是“负载翻倍”。每台服务器需要都承担一个任务的全部压力;对网络、数据库等共享资源来说,压力更是N倍增长。所以前面才会提到:这个方案适合对网络、内存等资源性能压力小的任务。
资源利用
如果说这个方案在负载均衡方面得分为0的话,资源利用上就是负分。
方案二:多处调度、一处执行
方案一可以说是“多处调度、多处执行”,方案二则是“多处调度、一处执行”。
这种方案的基本思路是:虽然多台服务同时运行调度机制,但通过某种机制来保证只有一台服务能最终执行任务。这种机制可以是quartz的集群调度功能,也可以是zookeeper的多活选主机制,还可以是分布式锁机制。
由于这个方案保证“一处执行”,因此它并不要求定时任务具有幂等性,适用性更广。
工作量
整体而言,这个方案只改造定时任务的调度机制,不涉及执行机制。而对于定时任务来说,调度机制比较统一,执行功能则变化更多。因此,尽管工作量与实际使用的机制有关,但这个方案并不算太麻烦。
以我此前参与过的一个的定时任务多活改造为例,从使用spring-scheduler的单机调度功能改造为quartz的集群调度功能,工作量、工期、风险等都在可控范围之内。
可用性
只要实现了集群部署,可用性一定是上了一个台阶的。
不过,一般来说,这个方案总会引入一个第三方机制来决定由哪台服务来执行任务(quartz使用数据库、zookeeper使用ZK服务等)。这个第三方机制,无论多么可靠、可用,多多少少还是会引入一些不可用风险。例如使用zookeeper选主机制,如果因为网络、机房等缘故导致选主失败,进而使得“多处调度”之后“零处执行”,也会使得定时任务引发问题。
另外,“一处执行”也会增加可用性风险。如果某个定时任务由于某台服务自身的问题执行失败,我们需要额外的机制(如spring-batch的restart机制)来处理。
只不过这种风险概率非常低,大部分时候我们都直接忽略掉了。
负载均衡
负载均衡是这个方案的一个“黑点”。由于只能保证“一处执行”,同一个定时任务的所有压力都在这一台服务上;其它服务即使空闲、可用,也只能袖手旁观。
但是,这个方案可以把不同的定时任务“负载均衡”到不同的服务上执行,从而避免所有任务都在同一台服务上执行的极端情况。
资源利用
简单分析一下,我们可以知道,当不同任务的资源占用(内存/cpu/网络等资源的使用量,以及资源占用时间)比较平均时,这个方案对资源的利用率比较高。但是如果各任务间相差较大(如任务A执行时间1小时,任务B执行时间2分钟),就会造成一定的资源浪费。
方案三:一处调度、一处执行
方案二的调度机制会在多台服务上同时运行,因此也可以称之为“分布调度,一处运行”。方案三则将调度机制集中到一套调度服务上,由调服服务进行“集中调度”,然后再由应用服务“一处执行”。
关于这种方案已有不少实现,如ELASTIC-JOB等等。不过其中有一些方案,实际上是“多处调度、一处执行”。按下不表。
工作量
这种方案的改造量会比较大。
虽然定时任务可以分为调度和执行两部分,但大部分情况下,这两部分代码结合得都比较紧密。方案三实际上是将“调度”功能与“执行”工作剥离开(有时甚至会把二者部署为不同的服务)。拆分原有代码的工作量可见一斑。
可用性
参见方案二。
负载均衡
参见方案二。
资源利用
参见方案二。
方案四:一处调度、多处执行
方案四是方案三的一个扩展。在方案三把“调度”与“执行”拆开以后, 这个方案开始把触角伸向了“执行”功能,并将“执行”功能拆分成“读取-处理”两部分(是的,参考了spring-batch的reader-processer-writer模式)。
在方案四中,“调度”功能只是调度“读取”功能,即在指定的时间点读取出所有需要处理的数据。读出数据之后,利用消息队列等机制,将数据分给多个“处理”服务。
在我们系统中就有类似机制。某个任务使用的是quartz的集群部署方案来保证“一处调度”;任务运行时,会先读取出当天需要处理的数据,并将其数据发送到ActiveMQ的队列中,交由相关功能来处理。
工作量
这个方案不仅要把“调度”功能单独拆出来,还要把“执行”功能再次拆分为“读取”和“处理”,并分别进行部署。其中的难度可想而知。
可用性
相比方案二和方案三,这个方案引入了更多的外部依赖。所以至少理论上,它的可用性(可靠性?)是最低的。
负载均衡
这个方案的负载均衡能力来自于将“读取”到的数据分发给“处理”服务时所使用的机制。如果使用消息队列(如ActiveMQ),则它也能将任务负载均衡地分发给集群中的多台服务。
资源利用
对定时任务来说,这个方案可以相当充分地利用系统资源。
但是,对消息队列、网络等“额外”资源来说,这个方案在很大程度上会加重它们的负担。
方案五:多处调度、多处执行
虽然方案一也是“多处调度、多处执行”,但方案五跟它是有差别的。
方案五将“执行”机制进一步拆分为“读取”、“判断”、“执行”三部分。首先读取定时任务所需处理的数据全集;然后利用分布式锁等机制,逐个判断读取到的某条数据是否应当由当前服务执行;只有通过了判断的数据才会进入到执行阶段。
相比“执行”,方案五对“调度”机制基本没做处理。单机环境下怎么调度,集群环境下仍然怎么调度。
工作量
方案五不变更“调度”机制,改造工作量全在“执行”机制上。而对执行机制的改造也只是“插入”一个步骤,而并不修改、拆分基本流程。这种改造工作相对来说还是比较轻松的。
可用性
同样实现了集群部署,方案五的可用性是有保证的。由于调度、执行都在多台服务上同时运行,无论哪一台服务出现问题,其它服务仍能正常的调度和执行任务,受到影响的可能只是那台服务上正在处理的一条(一批)数据。
并且,它对外部环境的依赖也比较小(基本也就引入了一个分布式锁),相对其它方案来说,风险也更低。
负载均衡
懒得敲字了。
资源利用
方案五相当于把一个任务所需处理的数据平均分成N份,均匀的交给集群中的服务来处理。因此,它对资源的利用率可以说是最高的。
不过,在读取数据阶段,由于要读取一个“全集”,可能会带来一些资源压力。
©著作权归作者所有:来自51CTO博客作者winters1224的原创作品,请联系作者获取转载授权,否则将追究法律责任 集群环境下定时任务调度问题与方案探讨 https://blog.51cto.com/winters1224/1963788