谈到大数据,避免不了hadoop, hive, spark 这些基础套件,但是在整个大数据开发的时候,我们面对的基本上都是数据开发平台和任务调度系统。数据开发平台一般直接面对业务同学,很大程度上影响业务同学的开发效率和节奏。而任务调度系统像一个黑盒一样,使用方一般都是数据开发的同学。但是任务调度系统在整个大数据体系中却占据着核心的地位。本人刚好负责一家互联网上市公司的调度平台,前段时间经历了一次大数据整体业务上云的过程,对调度系统有了更进一步的认识,本文将通过几个方面介绍以下调度平台整体结构和在工作中的使用经验。
在大数据任务调度中,有一些任务是每天必须在规定时间内完成的,这些任务一般拥有最高优先级,最大的资源使用占比,预警高优先级等。比如数据平台规定kpi任务必须要在早上9点之前产出,否则就是故障。
有向无环图指的是一个无回路的有向图。DAG调度即为工作流调度,是指任务从根节点依次往下执行,上游节点没有执行成功,下游不会执行,是一个自上而下的过程。
是一种平滑过渡的发布方式,AB test是一种灰度发布的方式,让一部分用户继续用A,一部分用户开始用B。如果用户对B没有反对意见。那么逐步扩大范围,将所有用户都迁移到B上来。灰度发布更多强调的是单一的新功能或者改进的逐步放量直到全量应用的过程。
资源按照需求递增的顺序进行分配 ,不存在用户得到的资源超过自己的需求,未得到满足的用户等价的分享资源
即任务,Job是调度系统中最小的单位。
即任务实例,每次真正执行的任务。指的是某次具体的执行。
上游任务开始调度,意味着任何一个依赖任务开始触发。
调度时间= 依赖任务触发时间的最小值
上游任务结束调度,意味着所有上游依赖都触发了,满足调度的依赖条件。
业务数据的真实时间。数据时间=(调度时间-偏移时间)。 在创建任务的时候可以填数据偏移,比如一个任务每天01:00运行,实际要处理的是上一天的数据,就需要根据调度时间-便宜时间。
首先举个例子,一般我们提交一个hive-sql, 首先需要登录服务器,连接上beeline,把sql脚本导入执行,获取日志和结果集,这没有什么问题。但是当大量的hive-sql需要执行的时候,并且sql之间有一定的依赖关系,或者sql需要定时在上午9点执行。这个时候手工执行任务就会明显效率低下。
调度系统的出现,让我们不需要关注任务具体是怎么提交,怎么调度,怎么执行,资源分配是否合理,依赖是否满足等。业务方把更多的精力放到业务上去,而不用去关心数据什么时候产出,数据质量的问题等。
那么什么是调度平台? 调度平台是包括调度系统,执行系统,通讯系统,日志系统,管理平台,专家监测系统,服务接口等一系列系统组合。
很多时候我们会把具体的执行器纳入到调度平台的范畴,hive,spark,这些具体的执行器都是调度平台涵盖的东西。但是仔细想下,无论是大数据组件,其他只要符合DAG调度,时间调度的其实都可以使用调度平台来调度。一个真正强大的调度平台应该是开放的。有公共的执行器模板,业务方可以根据模板接入。调度平台只负责具体的调度和资源分配即可。调度和执行是两个阶段。
调度系统封装了整个任务的执行,参数建议优化,进度预测/查看,结果产出。业务方只需要提交脚本即可。开发效率大大提升。
封装了大部分的任务类型python, shell, java, hadoop, hive, spark)的提交,业务方不用单独开发调度,一套系统满足大多数业务场景。
任务参数自动化推荐,资源的动态分配和均衡利用,错误日志智能分析原因。
通过对整个任务生命周期的监控,集群资源的监控,通过动态分配调度队列,调整任务执行并发度,动态优化调度时间。在正常完成任务调度的同时做到资源的合理利用。
首先要看一下整个数据平台的架构:
从下图可以看出来调度平台是在底层与上游的中间层,所有的底层数据需要通过调度系统调用平台服务,离线平台和实时平台的组件进行加工生产,最终产出到下游应用或者回流到底层数据中。所以调度平台就类似一个大脑的地位,支配这上游和下游的应用的运行。
整个系统分为使用master/slave的方式进行部署,sever负责整体系统的元数据和调度,worker服务任务的具体执行。rest负责整个系统的通讯,logagent负责worker日志的上报和存储。spiderx负责整个系统的监控,报警,优化等智能诊断。
底层的通讯方式以akka为主,包括sever,worker,rest, logagent之间的心跳和传输。
支持最简单的工作流DAG调度,支持hive,spark的离线任务调度。任务的失败重试,历史数据的回刷任务,基本的页面查看功能。
遇到的问题:
产生的效果:
调度器作为调度系统的心脏,主要作用是进行任务的调度。在该系统设计中,能实现对纯时间任务,依赖任务,时间+依赖任务的调度。
调度器由三个子模块组成:TimeScheuler, DAGScheduler和TaskScheduler, FairScheduler. 前两个主要时间调度和DAG调度,TaskScheduler针对具体的task判断是否符合依赖并且发送给下游的资源调度器FairScheduler。
四个调度器协同工作,共同完成对各种任务的调度,当完成对一个job的调度之后,提交一个task给Dispatcher。
调度器(Scheduler)和分发器(Dispatcher)之间是生产者和消费者的关系,Scheduler提交task给Dispatcher。只要Dispatcher空闲,就会拿走进行分发。
状态 | 描述 |
---|---|
WAITING | 任务依赖不满足或者任务调度时间未到。 |
READY | Task实例满足了作业依赖 |
PAUSE | 在Task处于WAITING或者READY状态下被手工暂停,只有这两种状态才能被暂停 |
ACCEPTED | Task已经提交给Worker,并且通过了接收策略(资源够用) |
RUNNING | Task正在执行 |
REJECTED | Task已经提交给Worker,但是没有通过接收策略 |
KILLED | Task在运行的时候被Kill |
FAILED | 运行过程中出现错误,比如业务,系统错误 |
SUCCESS | Task正常结束 |
ready包含4个子阶段:ready.queue(队列中) ready.dispatching(分发中) ready.accepted(接受) ready.Rejected(拒绝)。
当任务生成执行计划的时候,任务初始状态为WAITING。当任务进入执行队列时,状态更新为READY。当任务被work接收时,状态更新为ACCEPTED,反之更新为REJECTED。当worker开始运行任务的时候,状态更新为RUNNING。当任务运行成功后,状态更新为SUCCESS,反之更新为FAILED。
队列主要负责调度系统资源的管理和任务优先级的管理。这个借鉴了yarn队列资源管理模式,并且根据队列的权值设置队列执行的优先级。队列根据任务类型,优先级和容量规划了5中不同类型的队列,任务在创建的开始就已经确定了自己所属的队列。kpi队列是优先级最高的队列,kpi任务享受最高级的优先级和最充足的资源。
队列名称 | 优先级 | 说明 | 权重 |
---|---|---|---|
kpi队列 | 最高 | 一般用于kpi任务,在yarn上执行 | 最高 |
低延迟队列 | 高 | 一般用于临时任务,一次性任务 | 高 |
高优先级队列 | 中等 | 中等 | |
普通优先级队列 | 低 | 低 | |
重刷任务队列 | 最低 | 最低 |
在资源分配中队列的分配和队列的优先级是两个需要重点考虑的:
队列资源分配包括初始化分配和队列的动态分配。
初始化分配包括队列的初始最小份额,最大份额,队列的权重划分。一般的做法是根据最大的资源分配数,单个队列最小份额,最大份额,和权重,计算每个队列可分配的资源数。
对于hive,spark任务,如果是kpi任务执行,一般在yarn上划分出单独的队列去执行这些任务,目的是给这些任务留有充足的资源。
系统总资源,队列的权重是一个长期优化的过程,需要考虑当前使用的资源,需求资源和期望资源三个的占比然后进行优化。
在进行资源分配过程中,虽然最大-最小公平算法可以达到最大的公平,但是在实际的场景中,如果优先级较低的任务有资源的需求,虽然对应的队列已经达到期望的资源,但是在实际中我们还是会把其他的资源分配给这个任务,以达到资源的最大化利用,而不会去做抢占,因为抢占的本质在于快速四百,快速恢复。我们在处理这种抢占的时候,更多的是人工去kill任务。达到释放资源的目的。
依赖管理是整个DAG调度的核心。
调度依赖包括依赖策略和依赖区间
依赖分为任务依赖和作业依赖,任务依赖是DAG任务本身的依赖关系,作业依赖是根据任务依赖每天的作业产生的。两者在数据存储模型上有所不同。作业依赖树存储的多了两个时间:调度时间,数据时间。
job依赖:
任务依赖:
依赖是由依赖策略和调度依赖区间组成
定义调度依赖的区间,通过基准时间与偏移时间来计算。
表达式 = (基准时间t, 起始偏移时间x(m), 结束偏移时间x(n))
比如 (“yyyy-MM-dd HH:00:00”, x(n), x(m))
基准时间t : 基准时间的格式化,默认"yyyy-MM-dd HH:mm:ss"
起始偏移时间x(n) : 基准时间"向前偏移x(n)时间,作为偏移时间的起始点。(正数向前,负数向后)
结束偏移时间x(m) : 偏移开始时间 向前偏移x(m)时间,作为偏移时间的结束点。(正数向前,负数向后)
时间单位 : 年 y 月M 周w 天d 时h 分m
目前灰度主要应用于需求变化比较强烈的worker机器上。主要是做一些任务执行模型功能性的修改或者新增。灰度机器和普通的机器不一样,是单独运行的。灰度机器只能执行灰度任务不能执行普通任务。一般来说灰度都是靠灰度策略配置的。灰度策略的条件决定了灰度操作的多样性。
一般我们会用在修改bug的场景,比如某个脚本有问题,需要进行debug操作,我们会开一个单独的机器作为灰度机器,避免对线上数据做成影响。对于修改的脚本我们会在灰度机器上面跑稳定之后再同步分发到其他机器上面去。
一般有三种表达式类型:
通过上面的表达式我们可以得到任务的类型:
任务类型 | cron | Fixed Rate | Fixed Delay | 纯依赖任务 |
---|---|---|---|---|
cron | 1 | 1 | ||
Fixed Rate | 1 | 1 | ||
Fixed Delay | 1 | 1 | ||
纯依赖任务 | 1 |
如果强调任务间隔,建议用后两者,如果强调任务在固定时间执行可以用cron,cron有时可以替代Fixed Rate。
大部分的场景下我们用纯依赖和cron依赖比较多,一般都是分钟级任务或者是天级任务,它们有个共同点是最上游必须是时间或者时间依赖任务。但是在算法侧有一些纯依赖的应用,就是root节点也是纯依赖没有时间的,这个时候就需要通过api或者手动调用触发root节点执行。
先说一下什么是作业计划,比如调度系统有1w+的job。这些job有每天执行一次或者多次,有纯依赖,有复杂依赖。一个job从第一次创建,如何知道下一次的执行时间,依赖等。就需要通过创建作业计划来完成。
作业计划有两种创建模式:
如果调度系统简单,直接可以使用第二种,但是对于1w+job的系统,或者某个纯依赖的上游任务很多,或者上游也是纯依赖并且图深度>10. 显然第二种会影响执行的效率。我们采用的是第一种和第二种的结合。
执行计划的生成有多种方式:
在作业计划中,最重要的是计算下一次的执行时间。
任务恢复是整个调度系统中除了调度流程以外最复杂的功能。在系统某一组件出现问题,导致宕机。需要对机器进行重启或者启动备份机器,恢复成正常调度。
在系统宕机过程中,任何一个环节都会出现问题,server端,worker端,rest端等等。
比如server端出现问题,我们可以做HA, 用zk来选举master节点,如果一个节点宕机,其他节点自动重新初始化数据或者做数据同步。
在系统恢复的过程中,任务的自动恢复是比较复杂的,因为任务的状态在不同的流程下有不同状态。虽然任务的状态能同步持久化到数据库中,但是不同任务在不同的环节如何加载,恢复。其实是比较繁琐的。
以下图为例,一个任务从提交到最后执行成功的所经历的状态转变。在机器出现故障的时候,任何一个环节都有可能有问题。所以需要从任务状态的角度去看整个任务的恢复。
server端无状态保证:
有两种方案:
worker端无状态保证:
云原生改造的目的有三个
在整个系统迭代过程中,我们发现算法类应用一般在白天暂用集群资源比较多,和数据平台刚好形成一个互补。以云原生的方式进行部署应用,可以大大提升机器的使用效率,降低成本。
在最初的改造过程中,worker云原生时,设置弹性扩容最小内存为16G, 在凌晨dump任务并发数高的时候,会出现容器POT OOM. 解决方案有,worker增大内存,server投递减少并发数,任务jvm内存固定最大最小值,worker fork任务进程时候判断最大容量是否充足。在后面进一步改造的时候,为了把重量型任务和小任务隔开,通过任务云原生化,进一步资源隔离。
任务的生命周期管理,是一个挺麻烦的事情,对于一些短期的任务。需要设置合适的生命周期保证到截止日期之后下线该业务。对于kpi任务默认自动延长其生命周期。任务的owner离职之后,需要找到任务新的负责人,继续维护脚本。在这些场景下,如何保证任务高质量的运行,就需要强制限定一些规则,比如过期提醒策略,责任人离职变更策略,空闲任务监测,血缘关系中任务产出数据无访问记录检测等。
监控报警,失败重试 任务的失败重试,一般都是分析失败原因,如果是系统问题,一般重试一次,如果再次失败就报警,如果是超时,网络,一般会根据重试策略进行重试,默认是3次,失败通知值班人,如果是业务问题,再联系任务owner, 尤其是关键链路上因为偶然因素导致的问题。失败重试,超时报警,一般都是通知值班人,而不是任务owner。这个在调度平台产品化时很重要。
sever的HA 目前还是很难完成,目前做到的只是通过zk来保证一个机器挂了之后,另外一台机器重新初始化。在我们的场景下,sever需要初始化大量的数据在内存中(整个dag都是存储到内存中的),整个初始化的过程在10~30mins左右。如何能保证状态机和内存数据与备份机器无缝衔接,这个就需要分布式缓存和图数据库来存储依赖关系的保证。而且目前sever端和其他服务端通过akka进行通信,在zk选择新的sever端的时候,akka的通信也要重新切换到新的server端,并且确保旧sever端的数据不影响新的sever的服务。这个是未来的一些考虑。
提供更多插件化的任务接口, 支持更多不同形式的任务接入比如(go,spring等),并且与调度系统进行剥离,调度系统只负责任务的调用,而不在支持具体任务的执行逻辑。
任务执行优化智能运维 主要是任务重试的优化,在我们的系统里面任务重试主要是指失败的任务,根据日志分析失败原因,并且匹配任务失败策略。如果匹配成功就根据策略进行重试,如果没有找到失败原因就通知业务方处理,虽然我们一直在完善整个链路的失败重试策略,但是对于一些莫名其妙的问题,尤其是关键链路上,凌晨值班的同学在接到电话报警的时候总是要处理,其实应该让这些没有匹配上的任务自动延时重试一次。将会大大降低值班的次数。并且这种做法针对于worker挂掉自动拉起的情况,也有一定的恢复功能。
数据源同步进度优化 主要涉及一些kafka消息报,增量数据库同步进度收集,在一般的处理中进度收集和具体的执行是单独分开的任务,而且并没有关联的关系,这个在整个运维管理下都是比较麻烦的,针对这种任务,需要特殊处理进行任务的绑定已经延迟通知。
动态调整资源的波峰/波谷 在以往的场景下,我们会用时间依赖调度比较多,好处就是可以根据时间调度来人为的调整任务的开始调度时间。根据yarn集群和k8s集群的资源使用趋势图,来手工的调整整个DAG图中任务的调度时间。一般的做法是把某个时间点资源占用比较多任务拉取出来,手动调整调度时间到资源比较低的点。以达到资源的合理利用。如果是能够根据资源占用动态调整DAG图任务的开始调度时间,这个就比较完美了。
一些工作流调度的特殊场景 在实际的开发过程中,尤其是算法侧的对于调度平台有着不一样的需求(自定义工作流任务的编排,可以根据业务系统触发工作流任务的执行,而不用考虑周期调度),算法的业务注重的是任务的工作流编排和随时随地触发,这些任务可能执行一次或者多次。处于周期任务和临时任务的边界。所以是否需要页面化树节点的编排和动态触发工作流执行?这个需要想一下。
资源的使用量优化
资源的使用统计 资源的使用量主要是cpu和内存,在最初的版本上,任务主要在yarn上和worker主机上,yarn上面可以计算cpu的时间,申请cpu的个数,内存大小。worker主机上只能计算任务使用量,而且计算量比yarn偏小,而且口径基本上统一不了。所以在资源使用上主要以yarn为主。在现在的版本上,资源都在k8s和yarn上面,资源的统计口径就能保持一致。
资源使用优化,从应用级别进行优化,对于spark任务可以根据申请的内存和cpu大小计算空闲cpu和jvm使用量。提醒业务进行资源优化。从整体链路上,根据最终输出数据的读取频次进行数据的优化,比如最终输出的数据1个月以上已经没有读取了,可以从链路的末端依次提醒对应的业务方下掉对应的业务,以达到资源的合理利用。
任务资源弹性扩容,主要是针对大促场景下,不仅仅是平台集群进行扩容,对任务也需要扩容,尤其是一些核心的任务,内存,cpu需要成倍的扩容,目前我们都是通过人工来做,大概率会大促第二天任务产出延迟,如果提前进行自动化的任务扩容,任务的产出延迟就可能大大改善。
感谢数据平台的前辈们,尤其是天火老师的思想,站在巨人的肩膀上看问题果然简单了很多。
数据平台作业调度系统详解-理论篇
数据平台作业调度系统详解-实践篇
最大最小公平算法