分布式任务调度

任务调度解决定时触发的问题,分布式解决单点触发的问题。

任务调度

任务调度触发那些无法自动触发的任务。

任务调度中最重要的三个基础组件:任务触发器执行器

任务:保存待处理的任务,包括任务的触发的关键信息(例如,目标地址、调用方式、触发参数)。

触发器:保存待处理任务的执行时间,包括定时触发、周期触发。

执行器:任务触发的媒介,任务达到触发状态后都交于执行器来触发任务的执行。

实现原理

任务调度的组件大体依赖关系如下图,

组件依赖关系

任务的创建和触发流程

  • 创建Job,并指定具体的任务类型(RpcJobHttpJob);
  • 创建Trigger作为任务的触发器,指定任务触发方式(TimeTriggerCrondTrigger);
  • 计算Trigger下一次触发时间;
  • 关联JobTrigger,用于组装JobContext
  • 扫描最近一段时间内待触发的Trigger,并组装对应的JobContext
  • Executor拉取待触发的JobContext,按Trigger指定的执行时间来执行Job

任务状态

任务调度系统的状态可划分为:

任务状态流转图

分布式

对于单机任务调度系统来说,存在问题包括:单点故障任务堆积

分布式系统通过把任务分配给到不同的调度节点来解决单点任务堆积的问题。

常见的分布式解决方案:

  • Quartz:基于数据库实现作业的高可用,存在代码入侵。
  • Elastic Job:采用zookeeper实现分布式协调,实现任务高可用以及分片。
并行调度

并行调度:将任务分配到多个实例节点,使得多个应用实例能并行执行任务,以提升调度系统的执行效率。

从单实例到多实例,任务的分配并行调度的关键问题,常见的分配方式包括:

哈希分配:计算任务的哈希值,分配到固定的实例节点,需要处理集群扩容和缩容的问题。

负载优先分配:需要动态调整各个实例节点的负载压力,难点在于机器负载的定义与计算。

平均分配:任务轮询分配到各个集群实例节点。

抢占式分配:实现相对简单,不需要集群的管理节点来实现,只需要控制好并发锁的问题。

高可用性

高可用性:当执行任务的应用实例崩溃后,其他应用实例可以继续执行该任务。

也就是说,如何保证分布式中节点异常情况下,所有任务的正常执行。

异常节点的任务迁移是关高可用的键问题。

迁移必然带来任务的动态分配问题(与扩容类似),具体方法与使用的任务分配方式相关。

  • 哈希分配:采用一致性哈希算法来解决节点异常导致的大量的任务迁移问题;
  • 负载优先分配:把节点异常的任务迁移到负载最低的节点来处理;
  • 平均分配:把节点异常的任务平均分配到集群其他节点上。
  • 抢占式分配:把节点异常的任务丢回任务池中等待其他节点来抢占。

其中,负载优先分配方式下需要实时监控每个实例节点的负载能力,并需要实现动态调整实例的负载,实现任务的高效执行。

集群实例节点的状态如何监控?

利用ZookeeperEtcd来实现应用实例状态的监控,当发生异常情况时及时迁移异常节点的待处理任务。

一般情况下,集群中会有一台实例节点充当管理者,利用ZookeeperEtcd的分布式协调能力来选择集群的管理者,并监控整个集群的状态。

ZookeeperEtcd分别实现了zabraft一致性算法,可以辅助其他系统实现集群的状态管理。


下图代表了一致性hash来实现集群任务的分配及异常节点的任务迁移。

一致性哈希下的任务分配和异常迁移


弹性扩容

动态扩容:当集群无法满足大量任务的并发执行时,需要动态增加集群实例数量,同时保证历史任务的正常触发。

上面有提到过,一致性哈希来实现任务的哈希分配扩容情况下造成的节点任务迁移问题。

此外,在扩容过程中也需要考虑扩容的任务迁移对任务的准时执行是否存在的影响,如果存在影响的话,是否可以考虑把可能会受到影响的任务留在当前实例节点。

失败处理

只要是程序就会出现异常,任务调度也不例外,异常处理也是重要的一部分。

失败一般包括以下几种情况:

  • 集群实例节点down机
  • 任务调用超时
  • 任务无法调用到目标应用(网络 or 目标应用down机)

任务调度系统需要设置合理的任务重试机制,包括重试次数超时时间等。

此外,对于异常任务,需要配置对应的报警策略来通知相关业务人员及时处理异常问题。

任务优先级

集群的并行处理能力是有限的,在满足需求的条件下,允许配置任务的优先级。

任务调度系统中,通过设置不同的优先级的任务处理器来实现任务的优先处理。

带有优先级的任务处理器可以由预分配多个线程池来处理,优先级的高低决定了任务处理器线程池中并发执行线程的数量,高优先级的线程池配置更多的线程,低优先级的线程池配置更少的线程,从而实现高低优先级任务的隔离处理。

任务分片

任务分片:任务按照参数可以拆分成多个子任务,子任务下发到集群的不同实例节点并发执行。

任务分片的关键问题:任务的拆分

可以分片的任务一定是可以逻辑拆分的,拆分之后的子任务没有任何关联关系。

例如,为每一个用户统计他最近一天的消费信息,可以按照用户id的取模方式来实现任务的拆分:

1
2
1. id % 5 = 任务拆分为5个子任务
2. 5个子任务分配分配到集群的5个节点来并发执行

总结

任务调度的目的是准时触发任务,分布式的目的是解决单点问题,通过zookeeper和etcd等组件来实现集群的状态管理。

为了试下准时触发和优先级触发,实例节点需要配置对应的线程池来实现高低优先级任务的隔离。

参考

https://zhuanlan.zhihu.com/p/26493355
https://juejin.im/post/5c55ac0bf265da2da771a216
https://www.cnblogs.com/davidwang456/p/9057839.html