1.绪言
在上一篇文章《苏宁大数据离线任务开发调度平台实践—任务调度模块(上篇)》中,主要介绍了调度模块的架构设计、服务管理、重启恢复服务和 web 服务的设计细节,限于篇幅问题,今天我们接着详细阐述任务调度模块的核心服务—任务调度服务的设计以及开发实践过程中的关键功能点。
2.任务调度服务
主要负责上线任务流的配置检查、生成任务流执行计划、按照执行计划生成任务流与任务实例,生成任务实例状态机和节点之间的依赖触发关系。除了这些系统调用主要功能外,还提供人工干预任务执行的服务功能,比如:任务流上下线、任务补数据、任务重跑、任务杀死、失败重试等。
2.1 任务流上线
任务流上线是启动任务调度执行的第一步,上线不是单纯的修改任务流的数据库状态,涉及到以下几个关键步骤:
(1) 从数据库中获取上线任务流的配置信息:任务流、任务、事件以及节点之间的依赖关系等元数据信息
(2) 检查任务流的配置是否符合平台规范
(3) 检查跨流依赖(事件依赖)是否符合平台的频率依赖规则
(4) 建立任务节点的上下依赖关系的 DAG 图
(5) 生成任务流的执行计划
(6) 更新数据库上线状态,缓存任务流、任务等元数据信息
(7) 触发任务实例运行
(8) 对外发布任务实例的状态(创建状态)
大体处理流程如下图所示。
2.1.1 创建任务节点之间的依赖关系
没有采用复杂的数据结构,基于任务实体对象增加前置任务列表、后置任务列表、前置事件列表。
这种简单的数据结构开发方便,便于理解,也能满足调度依赖关系的查找。缺点是无法对循环依赖进行检查校验,当出现闭环依赖的时候,整个任务流的调度会出现死循环。目前我们是在前端页面配置任务流的时候,做闭环检查。当然也可以参考 AirFlow 的 DAG 图做有向无环图的数据结构,后期我们也会往这方面进行改造。
(前端页面图形化配置任务流依赖关系)
2.1.2 创建任务流的执行计划
基于 Quartz 做了单机版的任务执行计划,Quartz 集群在我们平台不适用,这里只是借助 Quartz 的多线程计划执行能力,同时也能很好的扩展支持 Crontab 时间表达式,做到时间调度的灵活性。
2.1.3 补偿丢失的任务流实例
任务流上线后,调度计划只会往前计算,对于一些调度周期较长的任务,比如几小时、天、周、月的任务,错过执行计划可能需要等很长时间才能进行下一次调度执行。对于这种长周期任务,需要在任务流上线后进行一次补偿操作。
2.1.4 创建任务(流)实例
到了调度计划时间的时候,任务流实例和任务实例都是同时生成。平台限制每个任务流、每个任务 相同数据时间 的实例个数只能有一个。当然这个仅限于未执行结束的任务(流)实例,执行结束后可以再重新生成。
任务(流)实例的概念可以理解为根据任务(流)的配置信息,在调度执行计划开始的时候按照配置信息和处理的数据时间生成的执行个体。这个个体除了具备任务的基本属性外,还有其他额外重要的几点属性。
任务流实例的主要属性有:任务流实体、数据时间、调度方式、运行状态、创建时间、更新时间。
任务实例的主要属性有:任务实体、任务流实体、数据时间、调度方式、运行状态、创建时间、待调度时间、待分配时间、已分配时间、已领取时间、执行开始时间、执行结束时间、更新时间。
这里需要注意的是,任务如果配置了任务事件(关于任务事件的说明,在上一篇文章中已经阐述过,在此省略),每次任务实例生成的时候,要删除同一数据时间的事件实例,否则会出现当前批次的任务还没执行结束(因为刚生成),而依赖该任务事件的其他任务流的任务已经可以执行,调度上是矛盾的,业务上也是不允许的。
任务实例具备多种状态,在每次状态切换都可能伴随着一定的业务处理逻辑。比如前置节点执行成功,需要通知下游节点去做流控检查、分配机器执行等;当任务由已分配到待分配状态变更,需要增加流控计数器等。如何管理这些状态的变更带来的多种业务处理,我们引入“状态机”的概念,类似 Yarn 里的状态机原理来解决这个问题。
伴随着任务实例的生成,任务实例的状态机也同时跟着生成,状态机与具体的任务实例绑定,不是一个任务实体一个状态机。为了有效的处理节点之间依赖关系的流转,状态机之间的依赖关系需要参考任务的依赖关系进行设定。
2.1.5 数据时间的说明
离线计算平台的数据处理具有延迟性,因为处理的数据量比较大,实时性要求不是太高。一般是今天处理昨天的数据,这个小时处理上个小时的数据。任务调度又具备周期性,每次的调度的执行时间都不一样,不能要求业务代码每次调度都要修改,因此需要提供一种时间变量,便于业务逻辑随着调度时间的变更而变更。我们提供了 ${statis_date}的时间变量,这个变量的数值对于每个调度频率都有一定的计算规则。
2.2 任务流下线
任务流下线主要是停止任务的调度执行计划。关键的第一步是要终止 Quartz 的 Job 执行,防止继续产生新的实例。然后是对 “未结束”(待调度、待分配、已分配、已领取、
执行中)的任务实例进行杀死操作,最后更新数据库的下线状态以及一些缓存操作和计数器。主要的操作流程如下图所示。
对于下线是否需要杀死还没结束的任务实例,可以根据具体业务场景考虑。有的下线操作只是停止调度计划的执行,不再生产新的任务实例,对于执行中和未执行的任务实例保持继续执行,尤其是执行中的任务实例,强制杀死可能导致任务终止而产生脏数据。有的场景就是要终止任务的执行。
2.3 任务状态机
任务实例具备多种状态,在每次状态切换都可能伴随着一定的业务处理逻辑。比如前置节点执行成功,需要通知下游节点去做流控检查、分配机器执行等;当任务由已分配到待分配状态变更,需要增加流控计数器等。如何管理这些状态的变更带来的多种业务处理,我们引入“状态机”的概念,类似 Yarn 里的状态机原理来解决这个问题。
状态机由一组状态组成,这些状态分为三类:初始状态、中间状态和最终状态。
关于 Yarn 的状态机的机制和原理,在这里不再详细阐述,有兴趣的同学可以自己查阅相关资料。
2.3.1 状态机管理服务
状态机不是一个服务,外部服务组件要触发任务实例状态机进行工作,需要对外提供统一的状态机管理服务—JobInstanceManager。其主要结构和内部工作原理如下图所示。
针对不同的频率我们设置了不同的处理线程,防止线程不足导致待处理的 Event(事件)堆积。分钟频率的任务实时性要求比较高,和其他频率类型的处理线程分开。外部服务组件要触发状态机的运转只要传递任务实例编码(JobInstanceCode),状态机管理服务根据任务实例编码找到对应的实例状态机,根据 Event 状态触发状态机执行相应的 Transition 执行具体的业务逻辑。
各个服务与 JobInstanceManager 的处理交互逻辑入下图所示。
2.3.2 状态机的各状态切换处理设计
系统调用(按照调度计划每次精准的生成任务实例)、任务补数据或者对任务重跑都会导致任务实例的生成和实例状态机的生成。实例生成以后,任务实例的生命周期由“新建”到“执行中”再到最后的“执行结束”,每次状态的切换都需要任务实例状态机管理(JobInstanceManager)进行驱动各个状态的切换变化。
检查前置节点状态 Transition
这里主要是检查当前任务的前置节点的状态,如果前置节点是任务节点,前置任务实例的状态必须是执行成功状态,否则当前任务节点会一直挂起,处于待调度状态。如果前置节点是事件节点,需要根据任务实例生成初期的事件实例生成计划,来判断所有计划内的事件实例是否都已经触发(或者理解为都已经存在),有一个事件实例没触发,当前节点都会挂起。
关于事件实例执行计划,我们举个例子说明一下。比如 天任务依赖一个前置任务事件,该前置事件是另外一个小时的任务生成的。按照计划,天依赖小时是需要小时的任务 00 点到 23 点的批次要全部存在,如果有一个不存在,则当前任务不能执行。这里的事件实例计划就是:00 点批次、01 点批次、02 点批次……23 点批次。
前置节点没有执行成功会导致下游节点一直挂起,如果挂起原因不给用户提示,用户很难判断是什么原因导致的,尤其是在前置节点特别多的情况下,用户排查原因很困难。因此在这里需要将后台的判断原因展示给用户。
任务执行结束处理 Transition
任务执行结束(杀死、失败、成功)后,当前任务实例的生命周期就结束了。
其他状态处理 Transition
待分配到已分配的状态变换,取决于任务分配服务的处理,关于任务实例如何被分配到具体 worker,已领取到执行中的状态变换比较简单,主要是将任务实例状态进行更新,转移已分配队列,已领取队里的实例信息,并进行任务状态发布。
3.后续
调度服务是整个调度模块非常核心的服务组件,除了前文所述的几点外,任务补数据、任务重跑也是非常核心的功能。限于篇幅和时间问题,本次的调度模块的分享先写这么多,后续会陆续对其他服务组件进行详细阐述,敬请期待。
整个调度模块的框架设计经过 2 年的上线运行,调度性能和执行能力的可扩展性已经得到充分的验证。上述的设计和实践经验完全基于苏宁大数据离线 ETL 和任务的开发实际情况,在某些设计上有些场景定制化的地方。如果要做成完全通用化的开发调度平台还有一段路要走。
作者简介
桑强,苏宁易购 IT 总部大数据平台研发中心离线计算工具研发部经理。10 年软件行业从业背景,13 年开始接触大数据,有着 5 年多的大数据应用和平台开发经验。现在负责苏宁大数据基础工具平台的研发工作,主要包括离线计算工具、实时计算工具、数据资产与质量平台的架构、研发和项目管理等工作。在对接大数据底层和大数据业务线之间,如何做好平台工具化,降低用户使用难度,支撑大数据应用的实践和研发上有着丰富的研发经验。
评论