分布式任务调度

前言算法

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

公司的任务调度系统经历了两个版本的开发,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月加入团队,目前主要负责资金团队后端的项目开发。

                                


                      更多精彩内容,请扫码关注 “铜板街科技” 微信公众号。 

相关文章
相关标签/搜索