数据平台作业调度系统详解-实践篇
彩色蚂蚁 简书

纸上得来终觉浅,绝知此事要躬行。实践才是硬道理。我司刚巧在开发工作流作业调度系统这块有一些实践经验,所以这篇文章来和大家探讨一下过去两年多来,我司Jarvis调度系统的产品功能定位,架构实现以及经验教训。

需求定位

如前文(数据平台作业调度系统详解-理论篇)所述,我司的Jarvis调度系统,经历了两代系统的迭代发展。

第一代Jarvis调度系统的实现,是以静态执行列表的方案为基础实现的,系统每天晚上11点半,根据周期性作业的计划和依赖关系,提前生成第二天要执行的所有任务列表和任务依赖关系。而Jarvis 2则是以动态执行列表方案为基础的。

Jarvis一代系统的问题

Jarvis一代系统是多年前,部门内一个同学在短时间内构建起来的系统,由于受当时的时间和人力资源所限,一代系统的功能形态和架构实现方面都缺乏整体规划设计,在系统的模块化方面也没有太多考虑。

所以,2015年下半年开始,我们开始构思Jarvis二代系统的实现。最开始,重构的原因其实很简单,主要是为了提升系统的可维护性。

在当时,正常情况下,Jarvis一代系统应对每日的作业调度流程,问题不大,但是在运维管理,报警监控,流量控制,权限隔离等方面都没有较好的支持,模块化程度不够,所以功能拓展也比较困难。后续添加的各种功能都是以补丁的方式到处Hack流程来实现的,开发维护代价很高,添加新功能时一不小心就可能破坏了其它同样Hach的流程逻辑,导致有一段时间内,系统故障频发。

其次,从功能上来说,Jarvis一代调度系统,对于我司当时的应用场景,虽然能够勉强应对,但是遇到系统异常情况的处理,或者稍微特殊一点的调度需求,往往都需要大量的人肉运维干预。

于此同时,系统所管理的作业任务快速增长,各种长周期任务(比如月任务),短周期任务(小时,甚至分钟级别任务)需求日益增多,任务之间的依赖关系日趋复杂,而使用调度系统的用户也从单一的数仓团队向更多的业务开发团队甚至运营团队拓展。

这种情况下,拓展应用场景,降低使用成本,提升系统易用性也成为了重构开发Jarvis二代系统的重要的目标。

Jarvis 2 设计目标

Jarvis 2的核心设计目标,从后台调度系统自身逻辑的角度来看,大致包括以下几点:

  • 准实时调度,支持短周期任务,作业计划的变更,即时生效

  • 灵活的调度策略,触发方式需要支持:时间触发,依赖触发或者混合触发,支持多种依赖关系等

  • 系统高可用,组件模块化,核心组件无状态化

  • 丰富的作业类型,能够灵活拓展

  • 支持用户权限管理,能和各种周边系统和底层存储计算框架即有的权限体系灵活对接

  • 做好多租户隔离,内建流控,负载均衡和作业优先级等机制

  • 开放系统接口,对外提供REST API,便于对接周边系统

从作业管控后台以及用户交互的产品形态方面来看,则包括以下目标:

  • 用户可以在管控后台中,自主的对拥有权限的作业/任务进行管理,包括添加,删除,修改,重跑等。对没有权限的作业,只能检索信息。

  • 支持当日任务计划和执行流水的检索,支持周期作业信息的检索,包括作业概况,历史运行流水,运行日志,变更记录,依赖关系树查询等。

  • 支持作业失败自动重试,可以设置自动重试次数,重试间隔等

  • 支持历史任务独立重刷或按照依赖关系重刷后续整条作业链路

  • 允许设置作业生命周期,可以临时禁止或启用一个周期作业

  • 支持任务失败报警,超时报警,到达指定时间未执行报警等异常情况的报警监控

  • 支持动态按应用/业务/优先级等维度调整作业执行的并发度

  • 调度时间和数据时间的分离

最后,从进一步提升易用性和可维护性的角度出发,和周边一些系统相结合,还有如下目标需求:

  • 支持灰度功能,允许按特定条件筛选作业按照特定的策略灰度执行

  • 根据血缘信息,自动建立作业依赖关系

  • 任务日志分析,自动识别错误原因和类型

具体目标的详细分析和实践

接下来,逐一具体讨论一下上述各条设计目标的需求来源,部分设计实现细节和相关实践经验

准实时调度,支持短周期任务,作业计划的变更,即时生效

所谓准时实调度,首先指的是Jarvis 2的设计目标不是以强实时触发为最高原则的(亦即只要触发时间到了,一定要精确准时执行),实际上Jarvis 2的设计目标基本上是以资源使用情况为最高原则的,受并发度控制,任务资源抢占,上游任务执行时间等因素的限制,Jarvis 2只保证在资源许可的情况下,尽量按时执行作业。

所以,如果要保证特定作业的精确定时执行,就必需保障该作业的资源可用性和计划可控性,比如提高作业优先级,划定专门的执行队列和资源,没有外部依赖,或者相关依赖作业执行时间严格可控等。但总体上来说,Jarvis 2并不保证秒级别的精确定时触发逻辑。

而另外一方面,Jarvis2的设计目标中,作业计划的变更,只要具体的任务还没有被触发进入就绪状态,所有的变更(增删改),就应该能够立刻生效,无需等到下一个周期或人工更新当日任务执行计划。

比如9点钟用户修改了一个原定于9点5分开始执行的作业的参数配置,改到10点执行,那么从今天开始,这个修改就应该生效,也不需要人工干预,从计划中先挪除之前的计划,再添加新计划等等。

但如果这个作业当前周期的任务实例已经执行过了,比如9点5分的任务,已经执行完毕,在11点修改调度计划到14点执行,那么,这个修改第二天才会生效。

我们认为上述逻辑应该符合绝大多数用户的意图,如果有特殊情况,可以通过人工重跑任务等方式来实现

具体的策略方面其实还有很多细节要考虑,比如面对一个月一次的长周期任务,或者一个小时执行一次的短周期任务,该如何处理这种计划的变更?在很多情况下,并没有绝对正确的处理方式,重要的是让默认的处理逻辑尽可能符合多数用户的意图,同时给予必要的结果反馈。

灵活的调度策略,多种触发方式和依赖关系支持

支持时间触发,和作业依赖触发,这两个需求很好理解。 为什么需要支持时间和依赖混合触发?这有几方面的考量因素:

首先,一些作业的触发时间周期和父作业的触发时间周期可能不一致,比如月任务依赖日任务,小时任务依赖日任务等。

其次,有些作业属于低优先任务,在依赖满足的条件下,定制触发时间,可以人工调配资源,错开集群峰值负载时间

最后,在当前作业依赖多个父亲作业的情况下,填写时间触发周期,有利于对特定周期依赖触发条件的判断,以及到点未执行等异常情况的报警判断。

(本质上,各种按一个Flow流定义整体触发周期的系统如oozie等,其实是把所有作业都定义成混合触发模式了,只不过这些作业的时间触发周期范围都一样而已。)

在实际Jarvis 2系统的实现中,我们只允许把仅有单个作业依赖的任务设置成纯依赖任务,存在多个父亲作业依赖关系的作业都要求配置成混合触发,也就是说需要设定触发时间周期。

作业依赖关系方面,当依赖的父作业在当前作业的触发周期区间内有多个任务时,Jarvis 2支持如下几种依赖策略:

  • All:调度区间内,所有依赖满足。

  • Any:调度区间内,任何1个依赖满足。

  • First(n):调度区间内,前面n个依赖满足。

  • Last(n):调度区间内,后面n个依赖满足。

  • Continuous(n):连续n个依赖满足。

默认策略为All,实际情况下,对多数一天执行一次的离线作业来说,这几种策略都是等价的,但对于不等周期的作业之间的依赖关系,就有区别了。通常All或者Last(1)的策略会常用一些,比如日任务依赖小时任务,可能只需要最新的一次小时任务执行成功,日任务就可以执行了。

系统高可用,组件模块化,核心组件无状态化

系统高可用当然是任何工程系统的通用追求,但是怎么实现高可用,实现的程度如何,那就因系统而异了。

从系统架构的角度出发,模块化的设计有利于功能隔离,降低组件耦合度和单个组件的复杂度,提升系统的可拓展性,一定程度上有利于提升系统稳定性,但带来的问题是开发调试会更加困难,从这个角度来说又不利于稳定性的改进。所以各个功能模块拆不拆,怎么拆往往是需要权衡考虑的。

Jarvis 2 采用常见的主从式架构,有中心的作业管理体系方案:Master节点负责作业计划的管理和任务的调度分配,worker节点负责具体任务的执行。


此外,Jarvis 2使用专门的Log服务器管理任务日志的读写,用户通过Web控制后台管理作业,而Web控制后台与Master服务器之间的交互透过Rest服务来执行,Rest服务也可以给Web控制后台以外的其它系统提供服务(用于支持外部系统和调度系统的对接)。

实际上,图上没有画出来,Jarvis 2还支持通过Tesla接口(我司自研的RPC服务接口)对外提供作业调度服务。

调度系统的核心逻辑,抽象的来说,就是一个状态机,所以,显然,严格的按学术定义来说,核心组件是不可能做到无状态的。

所以,这里所说的无状态化,更多的强调的是各个调度组件运行时状态的持久化,在组件崩溃重启后,所有的运行时状态都应该能够通过外部持久化的数据中快速的恢复重建。

为了保证状态的一致统一,Jarvis 2所有作业和任务的信息变更,无论是用户发起的作业配置修改,还是执行器反馈的作业状态变更,都会提交给Master节点同步写入到外部数据库。

在HA方面,按照准实时的设计目标,Jarvis 2并没有打算做到秒级别的崩溃恢复速度,系统崩溃时,只要能在分钟级别范围内,重建系统状态,就基本能满足系统的设计目标需求。

所以其实高可用性设计的重点,关键在于重建的过程中,系统的状态能否准确的恢复。比如,主节点崩溃或维护期间,发生状态变更的任务在主节点恢复以后,能否正确更新状态等等。

而双机热备份无缝切换,目前来看实现难度较大(太多流程需要考虑原子操作,数据同步和避免竞争冲突),实际需求也不强烈,通过监控,自动重启和双机冷备的方式来加快系统重建速度,基本也就足够了。

另外,为了提高系统局部维护/升级期间的系统可用性,Jarvis 2 支持Worker节点的动态上下线,可以对worker节点进行滚动维护,当Master节点下线时,Worker节点也会缓存任务的状态变更信息,等到Master节点重新上线后再次汇报结果。所以一定程度上也能减少和规避系统不可用的时间。

丰富的作业类型,能够灵活拓展

做为通用调度系统,当然需要支持各种类型的作业的调度。理论上,只要支持Shell作业,那任何类型的作业都可以交给用户,通过调用Shell脚本封装的形式调用起来。

但实际上,出于更好的控制作业的生命周期,定制执行流程,定制执行环境,适配作业参数,简化用户部署难度等需要考虑,往往还是需要根据实际的作业类型,定制专门的执行器(Executor)来执行特定类型的作业

Jarvis 2通过标准化任务执行接口(Executor)的方式来实作业类型的灵活拓展,具体的调度Worker负责将任务相关信息和环境变量传递给特定类型的Executor进程去执行,每个Executor除了实现run,kill,status等任务运行管理接口,还可以实现包括pre/post等流程Hook接口,用于执行一些特殊作业运行前的准备或运行后的清理工作。

Jarvis 2目前内建支持包括Hive/Presto/MR/Spark/Java/Shell以及一些我司内部专属的作业类型,其它少数没有强烈定制需求的作业类型,还是交给具体业务实现方,通过Shell作业来封装实现。扩展作业类型并不困难,困难的其实是各种类型的作业之间,流程,功能,环境,部署等方面在通用和定制两个维度间的平衡取舍,亦即,到底要定制到什么程度才是合理的。

支持用户权限管理,能和各种周边系统和底层存储计算框架即有的权限体系灵活对接

用户权限管理,其实一直是大数据生态环境最棘手的问题之一。

大数据系统组件众多,且不说,从底层组件的角度来看,并没有一个统一完整的权限管控解决方案:比如Kerberos,Sentry,Ranger都有自己的局限性和适用范围,比如很多组件可能压根没有权限体系,还有些组件不同的版本可能也有不同的权限体系方案。

从业务流程的角度来看,各家公司根据自己的应用场景也会有各种权限和用户管控需求。比如公司用户账号体系有统一的管理系统,不同业务流程要求不同的认证方式,各种大数据体系以外的系统(比如DB)有自己的权限和用户体系方案等等

而调度系统是对接这些组件的核心枢纽之一,所以势必需要一个通用的解决方案能够灵活适配各个周边系统和底层组件。

但要在一个中间系统中实现完全管控,这其实很难。所以,Jarvis 2的设计中,调度系统的核心逻辑实际上尽可能的只是起到用户和权限管理的二传手的作用,重点在于Hook好上下游系统,确保整体链路的权限管理流程体系的通畅无障碍。

对上,Jarvis自身维护作业信息,但是不维护用户数据信息,通过模块化组件,借助外部系统获取和管理用户。比如,通过我司的统一登陆服务,实现用户认证。通过我司的RBAC统一权限管理服务,对用户进行功能性验权。通过元数据管理系统等,进行业务组信息管理和任务/脚本/对象的授权管理。

对下,Jarvis将权限控制所需的相关信息尽可能完整的向后传递,比如任务的作业类型,owner归属信息,业务组信息,当前执行者信息等等。

这样,具体的计算和存储框架的用户和权限匹配工作,可以在上层,通过外部系统进行Gateway式的管理,也可以在下层,交由对应具体类型的Executor去实现。具体各种组件采用哪种方式来实现用户管理和验权,各有什么优缺点和适用场景,这又是一个很复杂的话题,后续文章专门另行讨论吧。

做好多租户隔离,内建流控,负载均衡和作业优先级等机制

大多数的公有云工作流调度系统,多租户的隔离是简单粗暴彻底的,那就是业务上租户之间完全独立,租户之间的业务很难相互关联。同一租户的工作流方面也往往如此。所以,它们更多的考虑是物理资源层面的隔离,这个,多半通过独立集群或者虚拟化的方案来解决,同一租户内部,做得好的可能再考虑一下业务队列管理。

我司的业务环境则不适合采取类似的方案,首先从业务的角度来说,不同的业务组(亦即租户),虽然管理的作业会有不同,但是往往不同租户之间的作业,相互依赖关系复杂,犬牙交错,变化也频繁,基本不可能在物理集群或机器的层面进行隔离,业务组之间的人员流动,业务变更也比较频繁。

其次在同一租户业务内部,不同优先级的任务,不同类型的任务,不同应用来源的任务,包括周期任务,一次性任务,失败重试任务,历史重刷任务等各种情况,也有不同的资源和流控管控需求。

Jarvis 2在上述需求方面,主要实现并支持如下功能:

  • 作业优先级定义,主要管理满足触发条件,在就绪任务队列中的任务的执行顺序。

  • 多维度的并发度控制:包括作业类型(Hive/MR/Spark等),应用类型(即作业的提交来源,比如从开发平台提交的还是从其它外部系统提交的),调度类型(周期任务,一次性任务,重跑任务等)和优先级类型(关键任务/非关键任务)等多种维度的并发度控制

  • Worker节点的被动负载反馈(负载高的情况拒绝接收任务)和主节点的主动负载均衡(轮询和Worker节点并发数控制等策略)

其中,并发度控制相关,后面专门的小节再详细讨论

开放系统接口,对外提供REST API,便于对接周边系统

一方面支撑调度系统自身逻辑运行,需要对接的周边系统众多,所以各组件和系统之间,需要通过低耦合的接口进行对接。

另一方面,往往还有很多业务流程无法完全接入开发平台的调度系统体系统一进行管理。比如,外部业务方业务流程复杂,多数业务相关程序必须在自己的系统中运行,只有部分数据处理作业可以提交到数据平台上来,或者出于安全角度的考虑,只有部分任务需要(可以)提交到开发平台上执行和管理。再比如,一些外部业务,可能需要根据调度系统中作业运行的相关状态信息来执行对应的流程方案。

Jarvis 2自身调度组件之间采用RPC通讯提高交互效率和可靠性,与后台管控组件和周边系统之间采用REST服务或我司通用RPC服务封装进行通讯和作业管理。同时对外部系统提供诸如作业状态变更消息通知等机制来辅助业务决策或串流外部业务流程。

用户可以在管控后台中,自主的对拥有权限的作业/任务进行管理,包括添加,删除,修改,重跑等。对没有权限的作业,只能检索信息。


这个很直白了,根本的目的是让用户能够尽可能的自助服务,同时降低操作代价,减少犯错的可能性。

支持当日任务计划和执行流水的检索,支持周期作业信息的检索,包括作业概况,历史运行流水,运行日志,变更记录,依赖关系树查询等。

这部分功能的目的,是为了让系统更加透明,让业务更加可控,让排查和分析问题更加容易。当然,也是为了降低平台开发和维护者背黑锅的可能性。尽可能的让一切作业任务信息和变更记录都有源可查,做到冤有头债有主 ;)


支持作业失败自动重试,可以设置自动重试次数,重试间隔等

这部分功能也很直白,作业失败的原因很多,有些情况下可能是临时性的,比如网络原因,比如集群或者外部DB负载原因,可能重试了就好了,你当然不希望半夜起来处理问题,然后发现问题已经不存在了,你只是点一下重跑button。。。所以,为了降低运维代价,我们需要可以支持相关重试策略的配置。当然,更加理想的情况是系统可以智能的根据失败的原因自动采取不同的策略。

支持历史任务独立重刷或按照依赖关系重刷后续整条作业链路

用户永远是对的,所以,用户三天两头没事批量重刷历史任务也是理直气壮的! 既然如此,我们就需要提供一个方便的手段让用户高效的去做正确的事情。。。

与当天失败任务的处理不同,失败的任务,下游后续的任务默认都是阻塞的,所以,修改脚本也好,修复其它错误原因也好,只要重新开始执行失败任务,恢复计划中的作业流程就好了,基本上,就是一个暂停,修复,重启动的过程。

而当执行历史任务重刷时,通常情况下,对应的历史任务是已经执行成功过的,所以用户的意图是单独重跑这个任务,还是要重跑所有下游依赖任务,甚至只是重跑部分下游任务,都是有可能的,系统自身无从判断,此外,重跑的日期范围是哪些,所有这些信息,都需要用户明确的定义。如何能给用户提供一个便捷的手段完成相关操作,上述所有的内容都需要考虑。

我司Jarvis 2的做法是提供图形化的界面,让用户搜索和选择相关作业,可以树形展开单独勾选这些作业的部分下游作业,也可以选择重跑所有下游作业,并提供日期范围选择


用户交互容易处理,更难处理的问题是重刷逻辑的构建以及它和正常调度逻辑的共存。

首先,需要根据所选的作业构建正确的依赖关系(只依赖于链路选择范围内部的作业,需要排除其它预定义依赖),生成执行列表。

其次,要考虑作业的串行/并行执行机制,以及重刷任务和正常周期任务之间的依赖关系,是否会干扰周期任务,同时运行是否会冲突,如果冲突如何处理等,如果相同数据日期的作业重刷成功,对应失败的周期任务如何解决等等。这里面,可能包含很多的业务流程逻辑,你可以完全不处理,也可以帮用户按照你认为合理的方式包办一切。Again,选择怎样的业务流程逻辑,没有唯一的答案,取决于你所提供的功能的形态定义和场景的需求与多数用户的认知是否能达成一致。

允许设置作业生命周期,可以临时禁止或启用一个周期作业

维护一个服务平台,头大的问题之一,就是用户用完即忘,留下一堆垃圾作业浪费系统资源。。。而你要让用户定期清理吧,用户也有苦衷,这个作业现在是不用了,但是说不定哪天我又要再用一下呢。。。

为了降低维护代价和机会成本,设置作业生命周期是一种可以采取的手段。虽然作用有限,多数同学实际上不会去设置,不过,总是聊胜于无吧。 而且,万一,哪天,你要是强势起来,可以强制设置有限的生命周期要求用户定期刷新呢 ;)

至于提供临时禁止和启用的功能,也是降低用户心理负担的一种方式,留下一条后路,心里总是不慌一些。除此之外,这些功能,平时也可以用作一些特殊操作的变通手段。

不过,凡事总有代价。要支持这些功能,系统的调度逻辑复杂度就会增加,在生成调度计划,触发作业执行,包括系统暂停,恢复,判定调度周期等场景下,需要权衡考虑的问题就多了很多。。。

另外,还需要考虑依赖关系链问题,禁止了一个作业,下游别的作业怎么办?挪除依赖?自动禁止? 当前我们的实现是不允许禁止还有下游依赖的作业。

总之,在易用性,可维护性和实现复杂性之间要取得一个合理的平衡,还是蛮伤脑筋的。

支持任务失败报警,超时报警,到达指定时间未执行报警等异常情况的报警监控

这个需求同样很明确,很直白。但困难的地方在于,如何做到智能化?

条件太宽,过多的无用报警,只会让大家精神紧张,降低报警的收益。条件太严,该报的没有及时报,那肯定更不行了。

举个例子,比如任务失败报警,配置了失败重试逻辑,那么第一次任务失败了报不报警,还是失败三次后再报警?你说,可以让用户来选择嘛,但用户往往是不会主动做这个选择的。理想中,不应该按固定的次数来判断,而应该按照重试代价来评估,如果重跑一次任务只需要5秒钟,那重试完几次都失败了再报警又如何?如果跑一次就需要两小时,那最好还是出了问题立即报警不是。。。

其次,报警的逻辑,其实和调度系统自身逻辑,不应该是强耦合的,因为这里面可能掺杂了大量的可能会随时调整的业务逻辑。比如:

  • 可能需要支持按业务组(用户组)排班的方式报警:比如很多业务可能是一个团队共同负责的,每天安排人值班

  • 可能在不同的时间段需要设置不同的报警策略:比如非关键业务,不着急处理,如果是半夜出错了,也不着急报警,白天上班时间再报警。

  • 可能需要设置不同的报警方式:比如短信,电话,邮件,IM工具等等

  • 可能需要临时调整报警的行为方式:比如大促高峰流量期间,系统维护期间等等。

所以,智能化的根据业务和历史信息进行报警;和核心调度逻辑松耦合,能够灵活调整策略,满足不同场景的需求,这两点才是实现报警功能的难点所在。

在我们的Jarvis 2 系统中,主要的思路是,尽可能把与调度逻辑无关的策略部分剥离出去。报警行为的触发判定功能模块化,而具体报警策略的制定,通过独立的报警服务平台来承接,解耦核心调度逻辑和报警策略之间的关联关系。

支持动态按应用/业务/优先级等维度调整作业执行的并发度

这里所谓的并发度,指的不是一个作业分成几片执行,而是说系统中允许多少个特定类型的作业的并行执行

如果集群资源无限丰富,那么当然不用考虑流控问题。但实际情况往往是僧多粥少,狼多肉少,你的集群优化工作做得越好,集群利用率越高,这个问题就越突出,因为缓冲的余地就越小。

虚拟化动态弹性集群资源固然是一种解决方式,但实际情况是弹性的响应周期,弹性的范围,和业务的需求往往并不能完全匹配。所以,或多或少,自主控制资源的使用量总是一个逃不过去的问题。

但是决定是否能够给一个具体的任务分配资源并运行起来,往往需要考虑这个任务多方面的属性。比如

  • 任务类型:因为不同的任务类型,需要的资源和对应的执行环境往往不同,对资源的消耗也不同。

  • 调度来源:比如正常的调度作业,重刷历史作业,临时性一次跑的业务,可能共享相同的资源,但是互相之间又不能过于互相影响,比如多数情况下要留给正常的调度作业足够的资源

  • 业务重要程度:比如核心业务的作业,需要优先保证执行,很可能需要更多的资源和并发度支持。

这几个维度的参数通常还是混杂在一起的,比如一个重刷历史的,属于核心业务范围的Spark任务,能不能跑起来?

我司Jarvis 2系统的实现中,多种维度的并发度是独立设置,然后共同约束的,任何一个维度的条件不满足,对应的作业任务就跑不起来。

这种实现方案的优点是实现简单,但是缺点是,正确的划分维度,并不容易,有些时候,你很难做到既能精确的控制某一特定属性集合的作业,又不对其它作业的并发度控制逻辑造成影响。因为这些维度压根就是纠缠在一起的。

举个例子,比如某一天,系统出了些问题,作业跑得慢了,我如果想要确保核心业务的产出时间,那么,我可以临时降低非核心业务的并发度,腾出资源给核心业务使用。但如果于此同时,我又希望非核心业务中,一些实时计算相关任务不要延迟太严重,要有足够的资源可用,那就很尴尬了,因为上述条件“与”的逻辑并不支持这种设定方式。(一定要实现的话,需要一个“或”的逻辑。但这样,各种并发度条件设置的综合效果,对用户来说就更难理顺和理解了)

总体而言,按不同维度进行并发度控制,有着广泛的应用场景和价值,但是具体的实现和维度分类方式值得深入思考和研究。

调度时间和数据时间的分离

先解释一下这两个时间的概念。

调度时间,指的是一个作业的任务实例,按计划是什么时候跑起来的。如果一定要细分,由于任务调度受各种因素干扰,不一定能够按时执行,所以还可以分为计划调度时间和实际执行时间

而数据时间,这个概念对很多强实时性要求的分片作业系统来说,通常并没有意义,也不是一个必需的环境参数。但对于用在大数据平台业务领域的工作流调度系统来说,却往往是一个重要的参数,重要到可以成为必备参数。

举例来说,比如,在离线数据业务处理流程中,常见的逻辑是从一天的凌晨开始,批量计算和处理头一天的数据。 这时候,调度日期是T,而数据日期则是T-1。你要说,那不能固定的认为数据日期就是T-1么?脚本根据调度时间,自己位移并计算数据时间就好了呗。

可惜的是,即使抛开用户使用成本不说,这种假设在很多情况下也是不成立的。比如,你在今天重刷3天前的作业,需要处理的数据是T-4呢?比如,如果你的任务是短周期小时任务,时间位移是以小时为单位呢?

所以,我们的做法是调度时间和数据时间是分离的,用户主动调度任务时后者可以独立设置。而对于系统定时生成的周期性作业的数据时间,则是通过作业调度计划和作业周期类型自动判断并生成的,然后以环境变量参数的形式传递给具体任务。

爱思考的同学可能还会问,那只有数据时间可不可以?调度时间好像没啥用。应该说对于作业自身运行逻辑来说,这可能比只有调度时间好很多,但是还是有些场合需要调度时间参数,此外,更重要的是,调度时间是调度系统用来管理作业调度生命周期的重要依据,反正是系统运转不可或缺的部分,那多传给作业一些信息也没啥坏处。

支持灰度功能,允许按特定条件筛选作业按照特定的策略灰度执行

没有不下线的系统,没有不犯错的人。降低系统变更风险的最好办法就是不变更。。。其次,是局部变更,验证,再整体变更,也就是所谓的灰度发布

我司Jarvis 2系统的灰度服务功能,其设计目标包含了两个层面的内容,一个是和自身系统升级等相关的灰度,另一个是业务层面的灰度。

举个例子,比如调度系统对接的Hive执行后端想要升级,那么能不能先灰度一部分Hive作业跑在新版本的Hive执行引擎上,先验证一下脚本/语法的兼容性和性能呢?这个动作能不能不需要修改任何脚本,对用户也是透明的呢?

再比如,一部分业务逻辑我想换一种执行方式,比如出于快速验证性能或流程的目的,一批作业想要dummy执行。

我们当前的实现方式,是让用户根据各种作业属性信息,创建规则,筛选出一批作业,然后按照一定的灰度比例定向发布到特定的机器(worker)上去执行。


这种方式,可以处理大部分通过执行器(部署在worker上)的变更就能够完成的灰度任务(比如前面提到的灰度升级Hive版本),理论上多数的任务也一定可以通过这种灰度手段来完成,只是代价大小的问题。。

对于无法低成本的通过执行器变更完成的任务,比如,修改作业自身的业务参数或者运行变量之类,理论上我们只需要增加更多的灰度手段就好,灰度的筛选规则和整体流程,用户交互形式都可以保持不变。不过,难点是可能有些灰度手段会涉及到调度组件自身功能逻辑的一些变更,目前看来这方面的需求不太明显,但是一个可以改进的方向。

根据血缘信息,自动建立作业依赖关系

从作业脚本中自动分析血缘关系,然后自动建立作业的依赖关系,当然是为了进一步降低用户构建工作流拓扑逻辑的代价。

试想,很多情况下,用户可能只知道自己的任务读取的是哪张表的数据,但根本不知道或者不关心这个表是由哪个作业任务生成的,是什么时候生成的。那他如何能够定义出作业的依赖关系呢?去找上游业务负责同学打听么?更糟糕的情况是,如果将来生成这个表的作业发生了变更,换成另外一个作业了怎么办?

所以,如果能够降低这部分工作的难度,对于维护准确的作业拓扑逻辑关系,保障业务的正确稳定运行还是有很大价值的。而对用户来说,如果他只需要开发脚本,然后提交,作业依赖关系依托系统自动搞定,对于提高工作效率来说,也应该是极好的。

但是自动分析作业血缘关系这件事并不容易,目前,我们只实现了针对Hive脚本和部分我们自身系统生成的Shell脚本的血缘关系分析。

由于涉及语法分析,所以如果不是文本/SQL形式的作业,而是比如MR作业或者Java作业,那么,自动分析依赖关系基本上是不太现实的,所以我们也提供给用户人工编辑依赖关系的选择。毕竟,本质上,作业的依赖关系最终还是要由用户说了算。

任务日志分析,自动识别错误原因和类型

维护开发平台的同学应该多少都有些体会,但凡作业跑出问题了,不管Log日志记录有多么详细,哪怕错误原因日志里都明确写出来了,总还会有一些业务开发同学会第一时间来找你。你要问他为什么就不能先翻一下日志,或者先谷歌一下呢?他可能会告诉你,哦,日志太长了,没看见呀,或者是,看不懂呀~~~

说实话,这也不能完全怪他(她)们,有人靠着多容易啊,再说,有时候人家压根也就没想好好研究脚本该怎么写,正确的业务流程应该是怎样的,还有GMV指标要背呢,没空自己排错,你帮我排错多好。所以要想改变用户,基本是不现实的,想要不被琐事累死,还是要靠自己啊。

比如,自动分析运行日志,识别常见的错误模式,明确的告诉用户错误类型,如果可能,告知解决方案。


不过,你可能会说,这很好,但是这和调度系统好像并没有太直接的关系啊。应该是做为开发平台整体功能的一部分。

是的,虽然从服务用户的角度来说,任务日志分析好像更多的是脚本业务逻辑开发方面的辅助功能。但其实,从作业运行管理的角度来说,还是有一定关系的。

比如,你可以通过任务错误的类型,来决定该任务是否需要重试。如果是语法错误,显然没必要重试了。如果是集群错误或者DB超时之类,没准重跑一下是可行的。

再比如,你还可以通过错误的类型,来决定要向谁发送错误报警。如果是业务逻辑,语法之类的问题,那么报警给业务Owner,如果是集群,执行引擎,网络之类的问题,那么发给业务方可能也没用,还是发给平台维护同学来处理更好一些

以上功能,我司的Jarvis 2系统当前只实现了一部分,毕竟错误的类型多种多样,情况复杂,要做到智能识别,并不容易。

Jarvis 2 现状和将来

最后再总结一下我司Jarvis 2 系统的现状,以及将来想要改进的方向

现状和问题

整体来说,本文前面所描述的一众设计目标,Jarvis 2系统基本上都已经实现了。经过快两年的开发和持续改进过程,当前,Jarvis 2调度系统日常大概日均承载约2万个周期调度作业,以及大致数量级的一次性任务作业和重刷任务作业。由于系统自身原因造成的大规模系统故障已经非常罕见。

但是,当前整体系统的流程逻辑,有些部分的实现过于定制化,为了降低开发难度,还有一些偏外围的业务逻辑被耦合到了核心调度流程中,不利于系统功能的后续拓展和调整。

此外,如果负载相对平均的话,稳定承载到10万个/日以上的作业,估计问题不大,但是在突发峰值或者极端高负载情况下的系统稳定性和节点失效恢复能力,尤其是在高负载大流量情况下,遇到系统硬件,内存,DB或网络异常问题时,能否较好的进行容错处理,还是需要经历更多的复杂场景来加以磨练的。

产品改进目标

系统自身的稳定性,部分既有逻辑的梳理重构,这些太过细节,你也未必关心,所以我就不说了,下面主要谈谈产品形态方面的改进目标

系统整体业务健康度检测和评估手段改进

为了保障系统的健康运行,监控当然是必不可少的,从系统监控的维度来说,大致会有几种做法:

首先是硬件指标层面的监控,比如监控一下CPU/内存/IO / 网络的使用情况,只要想去做,实施起来都不难。

其次是系统和进程层面的监控,比如服务是否存活,进程有没有假死,GC情况等等,稍微麻烦一点,但也不是什么困难的事

再进一步的,是组件和链路层面的监控,比如各功能调用的链路跟踪记录,比如各组件和功能模块自身Metrics的统计,这些相对来说,实现起来就复杂很多了。

以上这些监控手段,对于分析具体的故障和问题来说,是有帮助的,但是对于工作流调度系统这样一个关联系统众多,承载了大量用户自行定义的业务逻辑的复杂系统来说,往往还是不够的,这点我们深有体会。

举个例子,比如,某天凌晨,你突然被大批的作业延时报警吵醒,怎么办?什么原因? 最幸运的情况是你发现前面有作业失败(有报警,没吵醒你),阻塞了后面的作业运行,那么,赶紧针对性的分析一下好了。但是如果没有作业失败呢?系统进程看起来也都活着,后端Hadoop集群负载? 和平时也没有太大区别,甚至有可能,多半比平时还要低很多(不正常,但是,为什么?)

你想看一下具体某个超时未运行的作业是什么情况,可是依赖链路复杂,上游有十几层依赖上千个作业,有些作业完成了,有些还没有。检查了一下部分上游执行完毕的作业,也没有发现明显的异常,就是触发执行的时间比以前晚了,实际运行时间,有些长了,有些还更短了,具体变长变短是否属于正常,不好说。。。

上面的例子毕竟还有迹可查,更难搞一点的例子,比如有一天你发现最近主要的离线批处理作业流程,整体产出时间,平均同比上个月推迟了两个小时,但是这个变化是渐进的,找不到某一天开始有明显变化的点。事实上,受各种因素影响,这些作业流程的产出时间,日常的波动范围也在一个小时到两个小时之间,所以每天的环比根本看不出问题来,只是放大时间范围来看,总体趋势在变坏中。。。

所以是整体数据量变化导致的么?是某些组件负载或者容量趋近瓶颈,性能缓慢变差的原因么?是最近业务方太闲,写了很多脚本,模型越来越复杂导致的么?还是局部某个作业,脚本写法不科学,数据不断累积,数据倾斜日益严重拖累了下游业务?千头万绪,你能够如何入手展开分析?

上述问题之所以难解决,本质的原因还是在于可能导致问题的相关因素太多,信息过于繁杂,难以快速甄别有用信息。可能的解决方法,比如:广泛的收集各种维度的业务历史信息和系统环境信息,将日常实践中总结出来的各种分析的手段和问题排查模式固化下来,实时自动化的进行统计和汇总。即使不能自动发现问题,也可以抽象出更精简的数据,或者通过图形化的方式将各种数据汇总比对,提高分析问题的效率。

具体的手段我们还需要在将来更多实践和思考,可能的做法,随便举个例子,比如:

将作业执行的各种相关信息和流水记录,实时格式化的导入数据库,接入自定义报表可视化分析系统,便于随时进行各种多维度的统计分析和问题挖掘

  • 比如分析所有作业的执行时间的历史变化趋势,及时监测异常变化的作业

  • 比如按特定链路,特定类型,特定业务组,特定时间段,汇总统计作业的变化趋势

  • 按照各种组合条件灵活进行挖掘分析。

统计分析是一方面,更重要的是,在此基础上,可以更好的结合历史数据,判断当前业务的健康情况,或者预测将来系统的行为,比如一批任务还需要多长时间,多少资源才能完成。

个人业务视图的改进

多租户场景下,用户需要看到自己维护的作业,而如果是一个团队共同负责一部分业务,那么还会需要看到自己的用户组相关的作业,再然后,做为系统或具体业务组的管理员,则往往需要更大范围内的全局视图。

所以在业务众多的情况下,如何规划好个人业务视图,让各种条件的检索更佳快捷方便,也会对平台的效率和易用性产生影响,目前Jarvis更多的是提供按需过滤的方式,在全局视图中筛选出个人所关注的业务。这么做功能可以实现,但是操作想对繁杂,尤其是用户在多种角色中切换的时候,这方面的应用形态还需要进一步改进

此外,业务维度的资源汇总情况,比如每天跑的任务统计,资源消耗,健康程度,变化情况等等,也应该更好的按不同维度(个人/业务组/全局等)汇总展示给用户,方面用户随时掌控和调整自身业务

归根到底,只有用户清晰的掌握了自身业务的状态,才有可能减少犯错,提高工作效率,从而减少需要平台开发者介入帮助处理的情况

业务诊断专家系统的改进

目前我们提供了部分作业的错误原因分析,但是对于没有出错的业务的健康度的诊断,或者一批业务失败原因的快速综合诊断能力还不够。

比如某个任务跑得慢,是因为GC,还是数据量变化,是因为集群资源不够,还是自身业务在某个环节被限流?各种情况下,用户该如何应对解决,能否自动给出建议?

此外,是否能够定期自动诊断,及时提醒用户,敦促用户自主优化?避免问题积压到影响业务正常运行的时候才被关注。

自动测试体系的完善

固定的单元测试对当前Jarvis系统在大流量负载,复杂并发场景下潜在的Bug的发现,还是不够的,需要构建更加自动的,随机生成测试用例以及模拟和再现组件失效模式的测试体系。

这方面,理想很好,要真正实践起来,还是有很大难度的。

开源

开源严格的说不是一个目标,是一个手段,是用来提高产品质量的一个有效手段。

但开源也绝对不能像国内多数公司那样,只是开放代码,而没有开放思想。

开放代码很简单,只要你不偷不抢,使用别人的代码还不遵守版权协议。但是,光秃秃的代码放在那里,再加一个几百年不更新的README,你以为就叫开源了么?那不是开源,那是晒代码。

开放思想就难得多了,开源的目的是让大家一起参与,共同提高项目质量,共同受益,既不是单向输出,也不要奢望别人无偿奉献。要做到这一点,首先,维护者得花费大量的精力去维护社区的氛围,包括:

  • 解耦项目的公司内部逻辑功能模块,提供替换机制,做到外部真正可用。

  • 提供目标需求,场景说明文档,项目计划,Road Map等

  • 提供当前项目架构解析,关键技术说明,常见问题指南, 安装使用维护说明等。

  • 提供问题反馈途径,及时解答社区问题

  • 代码单元/集成测试手段和代码评审机制

只有做到这些,才能构建起健康的开源项目环境,才能真正对外输出价值,同时从社区汲取有益的帮助,而要做到这些,花费的精力绝对是巨大的,所以不要指望通过开源来节省自己的精力 ;)

小结

工作流调度系统是开发平台的核心组件,要做得完善并不容易,而且根据你自己的业务环境需求不同,选择怎样的路径并没有固定的套路,我们所走的路,所选择的产品形态,未必适合你的场景。使用开源产品,二次开发,还是完全自研,没有绝对对错,理解用户需求,了解现有产品局限性,评估自研代价和收益价值,才是最重要的。当然,现实是,没有实践可能也无从评估,所以,如果没有把握,找一个目标场景最接近的系统先用着,在实际使用中去发现问题,总结需求。





CIO之家 www.ciozj.com 公众号:imciow
关联的文档
也许您喜欢