Skip to content

develop

caojingwei edited this page Aug 17, 2017 · 49 revisions

1.系统架构设计

系统分为三个服务:

  • web-server: api 层,主要负责处理用户的请求,并返回相关信息
  • master-server: 调度的主服务,负责完成任务的调度功能
  • exec-server: 调度执行的服务,负责执行具体的工作流,支持分布式部署

具体架构如下所示: 架构图

2.调度设计

从上面的架构图可以看出,master-server 负责调度,当某个任务到了调度时刻点,则会发送给 exec-server 进行执行,这里会出现几种情况:

  • 没有找到 exec-server

这个时候,任务不会得到执行,master-server 会停留一定时间,然后继续发送,如果在超时时间范围没有找到 exec-server 来执行该任务,则会将该任务置为失败状态。

这种情况一般来说是因为 exec-server 挂了,只要重启 exec-server 就行了,只要任务没有超时,重启后还是会继续运行任务。

  • exec-server 执行中异常挂掉

这个时候,master-server 会监听到 exec-server 挂掉,然后会将没有该结点没有跑完的任务,调度到其它机器运行,所以,单点的 exec-server 挂掉是没有关系的。

另外,master-server 也有可能会异常挂掉,我们做如下处理:

exec-server 由于找不到 master-server,会自动挂掉,而 master-server 启动之后,会重新加载调度信息。 master-server 启动之后,会重新运行没有完成的任务,所以也没有问题。

当前 master-server 确实是单点的,后面需要考虑 master-server 的高可用问题,另外,master-server 和 exec-server 之间的服务器时间相差不能超过一个阈值(可配置)。

下面的调用逻辑展示了容错处理的过程: 调度流程图

这里有几个问题需要考虑:

  • exec-server 挂掉了,但是 master-server 还没来得及检测,而 exec-server 又立即启动了

这个时候,exec-server 会进行注册,注册的时候,master-server 会判断是重新注册的,会将未完成的任务重新分配给 exec-server 执行

  • master-server 挂掉了,但是 exec-server 还没来得及发现,而 master-server 又立即启动了

这个时候,master-server 启动的时候,不会进行任何的操作,然后在一定的时间之后,将未完成的所有任务重新分配(可以根据原来分配的 exec-server 来分配,如果 exec-server 挂了,则分配到新的 exec-server)

另外,需要注意的是,我们仅对调度的任务、补数据的任务做容错处理。

3.调度依赖设计

首先我们需要明确几个概念:

  1. 调度周期:调度周期指的是,这次调度到下次调度的时间间隔,调度周期可能是动态变化,比如配置某个任务为从 8:00-18:00 运行,每小时运行一次,我们认为周期是 1 小时,但是它是每天 8:00-18:00 间运行,只是在每天的周期是 1 小时,天之间的周期就没有意义了。

  2. 调度级别:我们可以从 "低" 到 "高" 将调度任务归类为若干级别(秒,分钟,小时,天,周,月),当然在实际中,秒周期任务是比较少见的。

  3. 任务自依赖:指的是任务依赖自身的上一个调度状态,比如任务 A 每小时跑一次,在每次任务 A 运行之前,都需要查看上一个调度时刻的状态。

  4. 任务间调度依赖:指的是一个工作流对其它工作流的依赖,为了表述方便,我们记 A 依赖 B 为 A -> B,这里有很多情况,即高级别可能依赖低级别的任务(如天依赖小时任务),也有可能低级别的依赖高级别的任务(如分钟依赖天任务),当然有可能同级别的依赖(小时依赖小时任务)。

这里我们要探讨的是,任务依赖怎么解析,这里分自依赖和任务之间的依赖。

方案一

  • 自依赖

这种情况比较好解决,每次任务运行的时候,就是检查上一个周期是否已经正常运行了。那么,怎么找到上一个周期呢?有几种手段:

  1. 简单的就看最近一次的运行状态,这种方法是可以,但是有缺陷,因为中间可能有间断,比如我看到了 8 点的状态,但是因为系统间断,9 点时刻错过了,我在 10 点去看 8 点的记录就有问题。

  2. 解决上面的问题,有个很简单的方案,每次任务调度的时候,我们记录下次应该调度的时刻。然后在检查的时候,我们是获取到这个时刻,再去看这个时刻的任务状态。这个方案的问题是,需要记录一下任务下次调度的时刻,但是,也是一个简单有效的方案。

  • 任务间依赖
  1. 同级别的任务依赖:比如有任务 A -> B,任务 A 是每小时运行一次(*/1),任务 B 是每三小时运行一次(*/3),对于任务 A,我们在 8 点运行的时候,会去看 B 任务 6 点是否运行了,那么,我怎么定位 6 点这个时刻而不是 7 点呢?这个也很简单,即我们会去看最近一次任务 B 的状态,如果最近一次是 6 点运行的,它的下一个点应该是 9 点,我们应该检测的就是 6 点,如果我们发现,最近一次是 3 点运行的,也就是 6 点就没有运行,那么我们任务依赖失败了。

  2. 低级别任务依赖高级别任务:这里同 1 的解决方案。

  3. 高级别任务依赖低级别任务:这种级别的依赖其实比较复杂,比如天任务依赖小时任务,比如我每天要统计一下昨天的用户 PV,其实只需要昨天的数据跑出来就行了,不需要今天的小时任务也是成功的。那么,怎么知道昨天的数据有没有跑成功呢?这个其实是非常复杂的,比较简单的做法是看当天最早的任务有没有成功(不算严谨),如果没有找到当天的任务,则找到最新的任务,如果这个任务的下次运行时间比当前小,则认为依赖失败,否则就看该任务是否是成功的。

  • 总结一下,上面的方案都依赖每个任务的 "下次调度时间",这个是必须要记录的,另外对于 "高级别任务依赖低级别任务" 这种情况,当前还没有特别合理的解决方案,但是近似方案还算是简单有效的。

方案二

方案一还是比较复杂的,我们有一个更为简便,但较为粗暴的方法。 假设任务 A 依赖任务 B,我们假定任务 A 的调度时间是 TA。然后进行如下算法:

  1. 找离 TA 最近的且小于等于 TA 的任务 B 的运行时间 TB。进入算法 2
  2. 如果 TA == TB,则会看 TB 时刻的运行状态,进行算法 5。否则进行算法 2
  3. 这个时候,肯定是 TA > TB, 然后计算从 TB 时刻开始,B 任务下一时刻的调度时间 TB2。进入算法 4
  4. 如果 TA >= TB2,则认为 B 在 TB2 时刻应该调度,而没有调度,则 A 处于依赖中,这个时候,其实是等待一定时间,然后进入算法 1
  5. 如果 TA < TB2,则认为 TB 是 B 任务最新的合理的调度时间,会查看 BT 时刻的运行状态。进入算法 5
  6. 等待 B 的调度状态,直到 A 自身超时或是 B 运行完,如果 B 成功,则依赖 B 成功,否则失败

当前采用的是 算法二 实现。

测试

内容 描述 测试结论
自依赖 任务 A 每十分钟运行一次,如果某次失败,下一次不能运行 测试通过
运行超时 任务 A 依赖任务 B,C,A 超时为 5 分钟,运行 2 分钟,B 运行 2 分钟,C 运行 3 分钟 05 秒,A 应该会运行超时 测试通过
依赖超时 任务 A 依赖任务 B,C,A 超时为 2.5 分钟,B 运行 2 分钟,C 运行 3 分钟 05 秒,A 应该会依赖过程中超时 测试通过
依赖失败 任务 A 依赖任务 B,C,B 运行 2 分钟,C 运行 3 分钟 05 秒,C 运行失败,A 超时为 10 分钟,A 应该依赖失败 测试通过
同时调度情况依赖 任务 A 依赖 B,A,B 都是运行 2 分钟,A 超时为 10 分钟,A 应该每次实际会运行 4 分钟,且从 B 运行之后开始运行 测试通过
补数据依赖 任务 A 依赖 B,B 因故没有运行,A 超时为 10 分钟,中途 B 通过补数据完成,则 A 应该可以接受 测试通过