对于规模以上的应用来说,调度系统已经是必不可少的组成部分,尤其在基于数据分析的后台应用大量增长的今天,健壮的调度任务管理已经是非常重要的一环。


1. 调度问题的由来

调度问题是怎么来的

当你的网站是个简单的blog,而且并不需要跟外部交互的时候,你大概不需要调度任务,因为此时网站需要处理的任务仅限于 即时交互 , 即用户想使用一个功能,你就立即给他就是了,如同你在简书上写一篇文章,一点保存,这篇文章立即就保存到网站的后台服务器中去了,这也是互联网刚出现时候的最早的应用模式。

之后因为网站发展的不错,用户多了起来,就发现需要大量处理一些非即时的任务,比如要定时将用户访问日志归档清理,这个时候一般情况下会在服务器启动一个定时任务,在每天固定时间发起清理归档,又如你想显示一下网站的访客流量、发表文章数、评论统计,由于并非每次用户或者后台管理员每次需要看的时候都去计算一遍,所以可能又需要启动另一个任务来去处理这些数据,这样的任务多了,就需要思考一个问题,哪些任务要先处理,哪些任务要后处理,每个任务要占用多少资源,从而任务调度问题开始出现。

调度什么时候变得复杂

在一个单机的系统,任务并不多的情况下,生活还依然是美好的,利用Linux自带的定时器或者系统框架提供的定时任务,实现好任务执行器,配置好任务触发的时间和频率,然后每天等待它自动触发,数据生成,任务搞定,似乎调度也没那么困难。

然而好景不长,伴随着网站的发展,你发现任务处理的越来越慢,甚至偶尔会有任务超时的情况,原因是每天用户产生的数据量越来越大,每次任务捞起的数量已经超载,依次执行完每个任务,可能已是最初执行时间的几倍。

这时稍有点经验你便会想到,任务没必要顺序执行啊,所以弄个线程池,起多个线程同时来捞数据然后执行,同时配置动态调整每次数据捞取的数量,增大执行频率,一系列组合拳打出去之后,调度任务又恢复正常,由于增加了并发数,甚至执行的比开始还更快了,就这样业务高峰便又顺利度过了。

调度什么时候变得更加复杂

之前所描述的情况基本上都在单机的情况,网站的QPS(每秒处理的任务数量)基本在500以下,通过一系列的参数调优,串行变并行,任务运行的很平稳。然而当任务变的规模更大,比如十倍于当前,一台机器已经不能处理所有的任务,这时候需要增加更多的机器,将任务分配到不同的机器上,于是便有了所谓的分布式调度的问题。

分布式是目前稍大型的网站不可避免的问题,这种处理方案有很多好处,比如可以利用廉价的机器,可以(理论上)无限水平拓展,同时也带来了一系列棘手的问题,机器之间的网络通信,如何把流量均匀的分布于不同流量,如果有机器宕机了如何处理,每个问题都已经是一个庞大的技术领域。

对于调度的分布式问题,首先要解决的便是如何把任务分发到不同的机器上,这要求调度系统通常要至少分为两层,第一层决定一共要处理哪些任务并把任务分发到哪些机器上处理,第二层接到任务后具体执行。虽然描述起来很简单,但是这个处理过程实际上需要大量支撑系统的支持,比如在任务分发时如何判断哪些机器还活着可以处理任务,这可能需要一个可以感知整个集群运行状态的配置中心,又比如任务如何分发,是采用消息还是实时服务接口,如果是用消息派发则需要消息系统,如果是实时服务,又需要类似dubbo这样的分布式服务框架。当系统达到这个复杂度,已经不是将任务捞起并处理这么单纯,而是多个关联系统复杂性的叠加。


2. 调度系统的分类

作业调度系统 vs 资源调度系统(或者集群调度系统)

作业调度系统代表:Crontab,Quartz等偏单机的定时调度程序/库,开源的分布式作业调度系统也有很多,如:oozie,azkaban,chronos,zeus等等,阿里的TBSchedule,SchedulerX,腾讯的Lhotse,当当的elastic-job,唯品会的Saturn等等。
资源调度系统代表:Yarn/Mesos/Omega/Borg,还有阿里的伏羲,腾讯的盖娅(Gaia),百度的Normandy等等。

资源调度系统,它的工作重点是底层物理资源的分配管理,目标是最大化的利用集群机器的CPU/磁盘/网络等硬件资源,所调配和处理的往往是与业务逻辑没有直接关联的通用的程序进程这样的对象。
而作业调度系统,关注的首要重点是在正确的时间点启动正确的作业,确保作业按照正确的依赖关系及时准确的执行。资源的利用率通常不是第一关注要点,业务流程的正确性才是最重要的。作业调度系统有时也会考虑负载均衡问题,但保证负载均衡更多的是为了系统自身的健壮性,而资源的合理利用,作为一个可以优化的点,往往依托底层的资源调度系统来实现。

那么,为什么市面上会存在这么多的作业调度系统项目,作业调度系统为什么没有像Hdfs/Hive/HBase之类的组件,形成一个相对标准化的解决方案呢?归根到底,还是由作业调度系统的业务复杂性决定的。
一个成熟易用,便于管理和维护的作业调度系统,需要和大量的周边组件对接,不仅包括各种存储计算框架,还可要处理或使用到包括:血缘管理,权限控制,负载流控,监控报警,质量分析等各种服务或事务。这些事务环节,在每家公司往往都有自己的解决方案,所以作业调度系统所处的整体外部环境,千差万别,再加上各公司各种业务流程的定制化需求进一步加大了环境的差异性,所以,调度系统很难做到既能灵活通用的适配广大用户的各种需求,又不落到太过晦涩难用的地步。
所以市面上的各种开源的作业调度系统,要不然就是在某些环节/功能上是缺失的,使用和运维的代价很高,需要大量二次开发;要不然就是只针对特定的业务场景,形态简单,缺乏灵活性;又或者在一些功能环节上是封闭自成体系的,很难和外部系统进行对接。

两大类作业调度系统

首市面上的作业调度系统,根据实际的功能定位,主要分为两大类方向:定时分片类作业调度系统和DAG工作流类作业调度系统。

定时分片类作业调度系统

定时分片类系统的方向,重点定位于任务的分片执行场景,这类系统的代表包括:TBSchedule,SchedulerX,Elastic-job, Saturn。
这种功能定位的作业调度系统,其最早的需要来源和出发点往往是做一个分布式的Crontab/Quartz。

DAG工作流类作业调度系统

这类系统的代表,包括:oozie,azkaban,chronos,zeus,Lhotse,还有各种大大小小的公有云服务提供的那些可视化工作流定义系统。

DAG工作流类调度系统所服务的往往是作业繁多,作业之间的流程依赖比较复杂的场景,比如大数据开发平台的离线数仓报表处理业务,从数据采集,清洗,到各个层级的报表的汇总运算,到最后数据导出到外部业务系统,一个完整的业务流程,可能涉及到成百上千个相互交叉依赖关联的作业。

所以DAG工作流类调度系统关注的重点,通常会包括:

  • 足够丰富和灵活的依赖触发机制:比如时间触发任务,依赖触发任务,混合触发任务。
  • 作业的计划,变更和执行流水的管理和同步
  • 任务的优先级管理,业务隔离,权限管理等。
  • 各种特殊流程的处理,比如暂停任务,重刷历史数据,人工标注失败/成功,临时任务和周期任务的协同等等。这类需求,本质上也是因为业务流程的复杂性带来的,比如业务逻辑变更啦,脚本写错啦,上游数据有问题啦,下游系统挂掉啦等等,而业务之间的网状关联性,导致处理问题时需要考虑的因素很多,也就要求处理的手段要足够灵活强大。
  • 完备的监控报警通知机制

DAG工作流调度系统的两种流派

  • 静态的执行列表
    所谓的静态执行列表流派,是说一个作业的具体执行实例,是跟据作业计划提前计算并生成执行列表的,然后调度系统按照这个提前生成的执行列表去执行任务。
    常见的做法是在头一天晚上接近凌晨的时候,分析所有作业的时间要求和作业间的依赖关系,然后生成下一天所有需要执行的作业的实例列表,将具体每个任务实例的执行时间点和相互依赖关系固化下来。调度系统执行任务时,遍历检查这个列表,触发满足条件的任务的执行。
  • 动态的执行列表
    所谓的动态执行列表流派,是说某个作业的具体执行实例,并没有提前固化计算出来,而是在它的上游任务(纯时间周期任务的话就是上个周期的任务)执行完毕时,根据当时时刻点最新的作业计划和依赖关系动态计算出来的。

3. 调度系统功能特性和需求分析

作业运行周期的管理

显式静态定义工作流的系统,对作业运行周期的管理,通常都是以整个工作流为单位来定义和管理的。当调度时间到达时,启动整个工作流,工作流内部的作业按照依赖关系依次执行。所以如果一个工作流内部存在需要按不同周期进行调度的作业,就会很难处理,需要想各种补救手段去间接规避,比如拆分工作流,创建子工作流,或者复制多份作业等。

非显式动态定义工作流的系统,对作业运行周期的管理,通常是以单个作业为单位的(因为压根就没有固定的Flow这个单位可以管理 ),所以用户只需要按需定义自己作业的运行周期就好了。相对的,对调度系统开发者来说,实现的难度会比较大,因为正确的自动判定依赖触发关系的逻辑会比较复杂。

作业依赖关系的管理

对于采用人工显式定义工作流的系统而言,作业依赖的管理,在很大程度上,其实是通过对工作流的拓扑逻辑的管理来实现的,用户改变工作流的拓扑逻辑的过程,实际上也就改变了作业间的依赖关系。 而作业的任务依赖关系,其边界,基本上就是在当前的工作流范围之类。

对于非显式定义工作流的系统而言,用户直接管理作业的依赖,所以这类系统一般都会提供给用户配置上游任务和触发条件的接口/界面。用户通过改变作业之间的依赖关系,间接的影响相关联作业的运行流程拓扑逻辑。

作业异常管理和系统监控

常在河边走,哪有不湿鞋,运行作业多了,总会出问题。所以对用户来说,作业异常流程管理能力的好坏,也是工作流调度系统是否好用的一个重要考虑因素。

比如,如果一个中间任务的脚本逻辑有错,需要重跑自身及后续下游任务,该如何处理?用户通过什么样的方式完成这件工作?需要手工重新创建一个新的工作流?还是可以通过勾选作业,自动寻找下游任务的方式实现?比如一个任务运行失败,但是结果数据通过其它手段进行了修复,那么如何跳过该任务继续运行后续任务?再比如,任务失败是否能够自动重试?重试有什么前提条件,需要做什么预处理,任务失败应该报警,向谁报警?以什么方式报警?什么情况下停止报警?任务运行得慢要不要报警? 怎么知道比以前慢?多慢该报警?不同的任务能否区别对待?等等

所有这些方面都决定了用户的实际体验和系统的好用/易用程度,同时,对系统的整体流程框架设计也可能会带来一些影响。
开源的工作流调度系统在这些方面做得通常都相对简单,这也是很多公司二次开发改造的重点方向。

资源和权限控制

有人的地方就有江湖。任务多了,势必就需要进行资源和权限管控。

最直接的问题就是,如果有很多任务都满足运行条件,资源有限的情况下,先跑哪个?任务优先级如何定义和管理?再退一步,你怎么知道哪些资源到了瓶颈?如果调度系统管理执行的任务类型很多,任务也可以在不同的机器或集群上运行,你如何判定哪些任务需要多少资源?哪些机器或集群资源不足?能否按照不同的条件区别管理,分类控制并发度或优先级?最后,谁能编辑,运行,管理一个任务?用户角色怎么定义?和周边系统,比如Hadoop/Hive系统的权限管理体系如何对接配合?

这些方面的内容,多数的开源的工作流调度系统也做得并不完善,或者说很难做到通用,因为很多功能需要和周边系统深度配合才可能完成。

系统运维能力

系统的运维能力:包括是否有系统自身状态指标的监控,是否有业务操作日志,执行流水等便于分析排查问题,系统维护,升级,上下线,能否快速完成,系统是否具备灰度更新能力等等。


总结

工作流调度系统做为大数据开发平台的核心组件,牵扯的周边系统众多,自身的业务逻辑也很复杂,根据目标定位的不同,场景复杂度和侧重点的不同,市面上存在众多的开源方案。但也正因为它的重要性和业务环境的高度复杂性,多数有开发能力的公司,还是会二次开发或者自研一套甚至多套系统来支撑自身的业务需求。


参考文献

  1. 彩色蚂蚁,数据平台作业调度系统详解-理论篇
  2. 杰克熊,我所理解的分布式调度