分布式任务调度

栏目: 编程工具 · 发布时间: 6年前

内容简介:公司的任务调度系统经历了两个版本的开发,1.0 版本始于 2013 年,主要解决当时各个系统任务配置不统一,任务管理混乱的问题,1.0 版本提供了一个统一的任务管理平台。2.0 版本主要解决 1.0 版本存在的单点问题。

前言

任务调度 可以说是所有系统都必须要依赖的一个中间系统,主要负责触发一些需要定时执行的任务。传统的非分布式系统中,只需要在应用内部内置一些定时任务框架,比如 spring 整合 quartz ,就可以完成一些定时任务工作。在分布式系统中,这样做的话,就会面临任务重复执行的问题(多台服务器都会触发)。另外,随着公司项目的增加,也需要一个统一的任务管理中心来解决任务配置混乱的问题。

公司的任务调度系统经历了两个版本的开发,1.0 版本始于 2013 年,主要解决当时各个系统任务配置不统一,任务管理混乱的问题,1.0 版本提供了一个统一的任务管理平台。2.0 版本主要解决 1.0 版本存在的单点问题。

任务调度系统 1.0

1.0 版本的任务调度系统架构如下图1:由一台服务器负责管理所有需要执行的任务,任务的管理与触发动作都由该机器来完成,通过内置的 quartz 框架,来完成定时任务的触发,在配置任务的时候,指定客户端 ip 与端口,任务触发的时候,根据配置的路由信息,通过 http 消息传递的方式,完成任务指令的下达。

这里存在一个比较严重的问题,任务调度服务只能部署一台,所以该服务成为了一个单点,一旦宕机或出现其他什么问题,就会导致所有任务无法执行。

分布式任务调度

任务调度系统 2.0

2.0 版本主要为了解决 1.0 版本存在的单点问题,即将任务调度服务端调整为分布式系统,改造后的项目结构如下图2:需要改造调度服务端,使其能够支持多台服务器同时存在。这带来一个问题,多台调度服务器中,只能有一台服务器允许发送任务(如果所有服务器都发任务的话,会导致一个任务在一个触发时间被触发多次),所以需要一个 Leader,只有 Leader 才有下达任务执行命令的权限。其他非 Leader 服务器这里称为 Flower,Flower 机器没有执行任务的权限,但是一旦 Leader 挂掉,需要从所有 Flower 机器中,重新选举出一个新的 Leader,继续执行后续任务。

分布式任务调度

另外一个问题是,如果某一个应用,比如说资产中心系统,我们有 A,B,C 三台机器,在凌晨12点要执行一个任务, 调度系统要如何发现 A,B,C 三台机器 ?如果 B 机器在12点的时候,恰好宕机,调度系统又要如何识别出来? 其实就是一个服务发现的问题。

群首选举

当多台任务调度服务器同时存在时,如何选举一个 Leader,是面临的第一个问题。比较成熟的算法如:基于 paxos 一致性算法的 zookeeper、Raft 一致性算法等等都可以实现。在该项目中,采用的是一个简单的办法,基于 zookeeper 的临时(ephemeral)节点功能。

zookeeper 的节点分为2类,持久节点和临时节点,持久节点需要手动 delete 才会被删除,临时节点则不需要这样,当创建该临时节点的客户端崩溃或者与 zookeeper 的连接断开,则该客户端创建的所有临时节点都会被删除。

zookeeper 另外一个功能:监视点。某一个连接到 zookeeper 的客户端,可以监视一个 zookeeper 节点的变化,比如 exists 监视操作,会监视节点是否存在,如果节点被删除,那么客户端将会收到一条通知。

基于临时节点和监视点这两个特性,可以采用 zookeeper 实现一个简单的群首选举功能:每一台任务调度服务器启动的时候,都尝试创建群首选举节点,并在节点中写入当前机器 IP,如果节点创建成功,则当前机器为 Leader。如果节点已经存在,检查节点内容,如果数据不等于当前机器 IP,则监视该节点,一旦节点被删除,则重新尝试创建群首选举节点。

分布式任务调度

使用 zookeeper 临时节点做群首选举的缺陷:有的时候,即使某一台任务调度服务器能够正常连接到 zookeeper,也并不表示该机器是可用的,比如一个极端场景,服务器无法连接到数据库,但是可以正常连接到 zookeeper,这个时候,基于 zookeeper 的临时节点功能,是无法剥离这一台异常机器的(但是可以通过一些手段处理这个问题,比如本地开发一套自检程序,检测所有可能导致服务不可用的异常,如数据库异常等等,一旦自检程序失败,则不再发送 zookeeper 心跳包,从而剥离异常机器)。

脑裂问题

群首选举中,我们选举出了一个 Leader,我们也希望系统中只有一个 Leader,但是在一些特殊情况下,会出现多个 Leader 同时发号施令的现象,即脑裂问题。

有以下几种情况会导致出现脑裂问题:

  • zookeeper 本身集群配置有问题,导致 zookeeper 本身脑裂了。

  • 同一个集群里面的多个服务器的 zookeeper 配置不一致。

  • 同一个 IP,部署了多台任务调度服务器。

  • 任务调度服务主备切换时候的瞬时脑裂问题。

其中前三个属于配置问题,应用程序没有办法解决。

第四个主备切换时候的瞬时脑裂,具体场景如下图4:

分布式任务调度

现象:

  • A 先连接上了 zookeeper,并成功创建 /leader 节点。

  • t1: A 与 zookeeper 失去连接, 此时 A 依然认为自己是 Leader。

  • t2: zookeeper 发现 A 超时,所以删除 A 的所有临时节点,包括 /leader 节点。由于此时B 正在监视 /leader 节点,故 zookeeper 在删除该节点的同时,也会通知 B 服务器,B 收到通知之后立即尝试创建 /leader 节点。

  • t3: B 创建 /leader 节点成功,当选为 Leader。

  • t4: A 网络恢复,重新访问 ZK 时,发现失去 Leader 权限,更新本地 Leader-Flag = false。

可以看出

如果 A 机器,在 T1 发现无法连接到 zookeeper 之后,如果不失效本地 Leader 权限,那么,在 T3-T4 时间段内,就有可能会出现脑裂现象,即 A、B 两台机器同时成为了Leader。(这里 A 发现超时之后,之所以不立即失效 Leader 权限,是出于系统可用性的一个权衡:尽可能减少没有 Leader 的时间。因为一旦 A 发现超时,马上就失效Leader 权限的话,会导致 T1-T3 这一段时间,没有任何一个 Leader 存在,相比于出现2个 Leader 来说,没有 Leader 的影响更严重)。

脑裂出现的原因很多,一些配置性问题导致的脑裂,无法通过程序去解决,脑裂现象无法完全避免,必须通过其他方式保障系统在脑裂情况下的数据一致性。

系统采用的是基于数据库的唯一主键约束:任务每一次触发,都会有一个触发时间(Schedule Time),该时间精确到秒,如果对于同一个任务,每一次触发执行的时候,在数据库插入一条任务执行流水,该流水表使用任务触发时间 + 任务 Id 来作为唯一主键,即可避免脑裂时带来的影响。两台服务器如果同时触发任务,且都具有 Leader 权限,此时,其中一台服务器会因为数据库唯一主键约束,导致任务执行失败,从而取消执行。(由于在分布式环境下,多台 Legends 服务器时钟可能会有一些误差, 如果任务触发时间过短,还是有可能出现并发执行的问题:A 机器执行01秒的任务,B 机器执行02秒的任务。所以不建议任务的触发时间过短)。

发现存活的客户端

服务端发送任务之前,需要知道有哪些服务器是存活的,具体实现方式如下:

应用服务器客户端启动成功之后,会向 zookeeper 注册本机 IP(即创建临时节点)

任务调度服务器通过监视 /clients 节点的子节点数据,来发现有哪些机器是可用(这里通过监视点来永久监视客户端节点的变化情况)。

当该系统有任务需要发送的时候,调度服务器只需要查询本地缓存数据,就可以知道有哪些机器是存活状态,之后根据任务配置的策略,发送任务到 zookeeper 中指定客户端的待执行任务列表中即可。

分布式任务调度

任务执行流程

任务触发的具体流程如下图6:

分布式任务调度

流程说明:

  1. Quartz 框架触发任务执行 (如果发现当前机器非 Leader,则直接结束)。

  2. 服务器查询本地缓存数据,找到对应的应用的存活服务器列表,并根据配置的任务触发策略,选取可以执行的客户端。

  3. 向 ZK 中,需要执行任务的客户端所对应的任务分配节点(/assign)写入任务信息 。

  4. 应用服务器的发现分配的任务,获取任务并执行任务。

这里存在一个问题:在任务数据发送到 zk 之后,如果存活的客户端立即死亡要如何处理?因为任务调度服务器一直在监视客户端注册节点的变化,一旦一台应用服务器死亡,任务调度服务器会收到一条客户端死亡的通知,此时,可以检测该客户端对应的任务分配节点下,是否有已经分配,但是还未来得及执行的任务,如果有,则删除任务节点,回收未处理的任务,再重新将该任务分配到其他存活服务器执行即可(这里客户端执行任务的操作是,先删除 zookeeper 中的任务节点,之后再执行任务,如果一个任务节点已经被删除,则表示该任务已经成功下达,由于删除操作只有一个 zk 客户端能够执行成功,故任务要么被服务端回收,要么被客户端执行)。

这个问题引申的还有一些其他问题,比如任务调度服务发现应用服务器死亡,回收该应用服务器未执行的任务之后,突然断电或者失去了 Leader 权限,导致内存数据丢失,此时会造成任务漏发现象。

任务变更的信息流

当一个用户在任务调度服务器后台修改或新增一个任务时,任务数据需要同步到所有的任务调度服务器,由于任务数据保存在 DB,ZK 以及每个调度服务器的内存中,任务数据的一致性,是任务更新时要处理的主要问题。

任务数据的更新顺序如图7所示:

分布式任务调度

  1. 用户连接到集群中的某一台 Server, 对任务数据做修改,提交。

  2. Server 接收到请求之后,先更新 DB 数据 ( version + 1 )。

  3. 异步提交 ZK 数据变动( zookeeper 数据更新也是强制乐观锁更新的模式) 。

  4. 所有 Server 中的 JOB Watcher 监控到 ZK 中的任务 数据发生了变化,重新查询 ZK 并更新本地 Quartz 中的内存数据。

由于 2,3,4 三步更新,都采用了乐观锁更新的模式,且所有任务数据的变动,都是按照一致的更新顺序操作,所以解决了并发更新的问题。另外这里之所以要采用异步更新zookeeper 的原因,是由于 zookeeper 客户端程序是单线程模式,任何同步的代码,都会阻塞所有的异步调用,从而降低整个系统的性能,另外也有 SessionExpired 的风险( zookeeper 一个重量级的异常)。

三步操作,任何一步都有可能失败,但是又无法做到强一致性,所以只能采用最终一致性来解决数据不一致的问题。采用的方案是用一个内置线程,查询5分钟内有过更新的任务数据,之后对三处数据做一个比对验证,以使数据达到一致。

另外这里也可以调整为:zookeeper 不存储任务数据,只在任务数据有更新的时候,发送给所有服务器任务有更新的通知即可,调度服务器接受到通知之后,直接查询 DB 数据即可,数据只保存在 DB 与各个调度服务器。

实践总结

任务调度系统 1.0 版本解决了公司的任务管理混乱的问题,提供了一个统一的任务管理平台。2.0 版本解决了 1.0 版本存在的单点问题,任务的配置也相对更简单,但是有一点过度依赖 zookeeper,编码的时候应用层与会话层也没有做好解耦,总的来说还是有很多可以优化的地方。

作者简介

卢云,铜板街资金端后台开发工程师,2015年12月加入团队,目前主要负责资金团队后端的项目开发。

分布式任务调度


以上所述就是小编给大家介绍的《分布式任务调度》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Pro JavaScript Techniques

Pro JavaScript Techniques

John Resig / Apress / 2006-12-13 / USD 44.99

Pro JavaScript Techniques is the ultimate JavaScript book for the modern web developer. It provides everything you need to know about modern JavaScript, and shows what JavaScript can do for your web s......一起来看看 《Pro JavaScript Techniques》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

MD5 加密
MD5 加密

MD5 加密工具