调度系统稳定和一致性保证的思考

调度系统作为数据平台的大脑,稳定性保证是核心的功能,如何在出现网络问题,流量压力,服务器宕机,误操作等问题出现的时候保证调度系统的正常稳定的调度,其实是一个不小的考验。
而对于一致性而言,如何保证任务的有序执行,以及重跑/回刷场景下,保证数据的一致性,避免出现数据重复,丢失等问题,或者在上游数据出现问题的情况下,如何能通过稳定和一致性的保证,做到及时的发现,及时止损。

本文从以下几个常见的系统问题触发,来探索一下。

服务挂掉

以master-worker架构而言,server通常的作用为任务的调度,与第三方的通讯,以及元数据的管理。server端挂掉之后不影响worker端任务的执行。只是影响任务状态的上报。

server挂掉

server端在挂掉之后,worker端会轮询server的心跳,待心跳恢复正常,会把本地存储的任务状态上报给server。挂掉之后不影响任务的执行和查询,但是不能提交新的任务。
所以对于稳定性来讲,server端挂掉,在很短的时间内能够恢复其实就没有太大问题,又因为server端在内存中存储这不同时间任务实例的状态,如果做backup,数据的同步也是很大的问题。所以一般而言server挂掉在10mins内能够重启其实就已经满足需求了。

对于server重启,server端需要进行基础组件的恢复,任务的恢复,需要恢复包括任务的实例,执行计划,任务的DAG。根据任务状态的不同进行不同的恢复操作, 一般耗时最长的是任务DAG的重新生成,以下是任务恢复的一些场景。

  • 已经生成任务实例,需要重新推送到资源调度器中执行,对于已经执行中的,推送到对应的worker。
  • 对于时间/时间依赖调度的执行计划,需要根据最近一次生成任务实例的时间推算下一次的执行时间,并且推送到时间调度器中。
  • 对于TaskScheduler通过上游依赖触发生成实例一些特殊场景在server挂掉之后,无法生成触发实例生成的情况,需要轮询任务触发生成调度实例。
worker挂掉

因为worker只负责拿到任务实例,推送到容器执行或者fork一个子进程执行。并且worker的并发度的控制都是根据worker资源容量来动态管理,所以挂的可能性比较小。一般来说,大多数据挂掉的场景都是worker机器cpu,内存突然达到阈值,导致进程被随机killed。

父进程和子进程被随机kill,产生的影响就是worker机器和server之间心跳断开,新的任务实例无法发送到当前worker,在这个时间间隙,任务实例发送到当前worker,通过超时和找不到worker机器,被worker机器拒绝等导致重新选择worker机器执行。

子进程被kill了之后,任务实例的状态一直会处理running的状态,不会改变。待worker被重启了之后,任务实例从本地磁盘中拉取出来,恢复其状态(如果worker做到无状态化管理,任务实例元数据可以存储在第三方存储中,任务可以做到失败重新迁移到新的容器执行),如果任务实例包含中间状态(比如:任务实例中调用第三方的任务执行),这个就比较麻烦,需要在本地磁盘保存中间任务的一些状态(killd方式,查询状态方式等)。举个例子,以datax工具 同步kafka数据到hive,我们的做法是重写了inputformat, 在java代码中调用hive脚本。在java进程被干掉之后,hive任务在yarn上执行并不受影响。本地磁盘不仅要保留进程的相关信息,也要保留hive的的applicationid,applicationName信息。在recovery的时候可以从yarn上判断对应的任务是否还存活。

对于worker上执行shell脚本而言,一般来说进程挂了之后,就没有办法恢复,worker重启之后也只能根据任务的失败策略设置状态为失败,返回给server。对于spark等任务,可以通过循环遍历查询远程任务的状态,直到任务成功为止。

我们的一些经验是:对于spark, shell任务如果能通过中间proxy转发,提交到容器中执行是最优解,比如spark 依靠livy去提交,livy自身有recovery的机制。worker即使挂了也不影响任务的执行,而且每个任务都容器化,间接的降低了运维的成本。

任务实例恢复

因为所有的任务实例以及对应的DAG依赖关系都存储到内存和本地,在server挂掉之后,需要对内存中的任务实例和依赖关系进行恢复,

任务执行失败

一般来说,只要是上线的任务就有可能执行失败,任务的执行失败有多种形式。

  1. 任务实际进程执行失败,但是调度任务记录标记为running状态。
  2. 任务因为外部环境(网络,文件读写异常)执行失败,但是通过重试可以恢复。
  3. 任务因为本身代码问题或者系统异常导致的失败,不可恢复。

任务执行失败之后要进行恢复,修改问题,重新提交。需要考虑的问题包括任务执行失败是系统自身解决还是需要人工介入,是管理员介入还是需要任务owner介入,介入的途径是否需要根据任务的重要等级进行选择,介入之后系统是否能够自动化的诊断得到任务失败原因,帮助介入者解决问题。

上面是一整套的问题的解决逻辑,我们的做法就是根据任务的类型,任务的日志报错,任务的重要等级,任务失败的重试次数等沉淀下来一个模型,每当任务执行失败之后,任务的日志在存储到日志系统的同时需要根据模型分析出来任务报错的原因。并且分析出来是否需要重试去解决,或者给出最佳实践来帮助介入者解决。

<mapping>
            <patterns>
                <pattern>SocketException: Broken pipe</pattern>
            </patterns>
            <result>
                <notifyMsg>Hive断开连接</notifyMsg>
                <errorType>0</errorType>
                <isNeedRetry>true</isNeedRetry>
            </result>
        </mapping>
        <mapping>
            <patterns>
                <pattern>error: Table is occupied, requestId:</pattern>
            </patterns>
            <result>
                <notifyMsg>BDP网络出错</notifyMsg>
                <errorType>1</errorType>
                <isNeedRetry>true</isNeedRetry>
            </result>
        </mapping>
任务延迟产出

一般来说DAG调度和资源调度决定了任务产出的时间是不确定的,怎么能保证任务在合理的时间产出,以及任务延迟后,怎么通过特殊的手段,把资源让给重要的任务,以减少损失。

任务一般都是需要根据重要等级来选择执行队列, 资源,执行的优先级。如果任务的重要级别比较高,在执行的时候如果发生报错,任务延迟就需要值班同学现场处理,如果重要等级不高,一般只需要反馈给任务owner即可。

但是怎么确定任务产出的时间点呢?

  • 在创建周期任务的时候,需要在测试过程中预估任务需要的时间,以及任务需要的资源大小。并且需要根据历史周期任务的平均执行时间和事件点来进一步的调整任务的优先级和重要等级。
  • 对于kpi任务而言,某一个周期任务需要加入到kpi任务,需要多方面的进行判断,首先需要看一下kpi任务执行时间段资源池的占用,如果资源池占用是满的,就需要推迟任务的执行时间或者不能加入。其次对于周期任务而言,最近7天的平均执行时间点是重要的评估标准,如果最近的执行都超过了kpi的执行时间段,就需要对任务进行优化或者上游执行时间进行调整。

任务延迟了之后,对于需要报警的任务,一般需要根据重要等级通知业务方或管理员进行手工处理,如果是上游任务报错,任务抢不到资源,需要管理员手工触发,强行提升任务的执行等级和调整执行队列。如果任务出现系统问题(数据倾斜,处理并发不够)就需要业务方手工进行调整,这些在大促等流量暴增的时候是常规的处理手段。

数据重复

数据重复的问题一般都是由任务的并发写造成的,一般来说hive以及其他任务事务性做的都不是很好,双写很容易造成数据重复。虽然我们可以通过调度系统对这类任务保持串行执行。但是对于执行过程中任务中断,以及如何恢复,中断之后任务状态的判断,其实都是需要考虑的。

  • 表在读写的时候,如何通过锁来控制并发写?
  • 任务在执行的过程中系统中断,如何在系统恢复之后判断任务的状态?
  • 任务因其他因素重新执行,如何保证历史任务killed?
    基于以上几个问题,
    1:hive的表锁以及分区加锁机制,一般是通过zk来实现的,对于非分区表,一段时间范围数据容易遭遇长时间锁表,造成依赖表的任务调度失败。以及一些特殊操作会引起死锁的情况,经过一些线上调研,发现大部分都是设置set hive.support.concurrency=false来关闭锁,通过调度系统的能力来保证串行写入单表以及并发写入分区表。
    2:调度系统的任务执行过程一般不是原子的,包括任务执行,任务状态汇报两个过程,因为没有办法使用两阶段提交,所以需要针对各自挂了之后,都要做一定的容错处理,比如任务执行过程中,系统挂掉,此时任务是执行中,或者是执行完成状态,但是任务的状态还没有来得及汇报给server,在系统恢复之后,如何获取任务的状态,我们的做法是把任务的状态以及任务的参数全部本地化,任务在执行之前先存储任务的状态到本地(当然你也可以存储,这样整个worker就是无状态的),并且通过双向的心跳机制保持server-worker机器之间的联通。当worker服务挂了之后,worker上所有的子进程全部会被自动kill掉,此时server端通过心跳判断到worker处于失联的状态,在指定的时间内,如果还是无法联通,就会把worker下线掉。这样server端发送的任务请求就不会再打到这台故障机器上,故障机器上失败的任务实例,不会转移到其他机器继续执行,而是等待故障修复之后,通过服务的重启而恢复,任务恢复是根据任务的失败策略进行的,(spark 会保存当前任务的applicationId,会继续请求yarn判断任务的状态,根据任务的状态汇报给server,如果任务一直处于执行状态,会一直轮询直到成功,如果是本地的shell 会直接失败,然后返回给server)。
  • 任务如果发送给worker, 因为网络或者其他的因素,一直没有返回给server状态,在发送超时的过程中需要重新发送,发送的策略一般是继续发送给同一个worker机器,并且判断当前worker是否存在相同的任务实例,如果存在,就忽略掉当前的执行。

在我们的经验中,发现一般出现数据重复的问题,大都在hive任务,shell任务重,单次任务插入和正常调度任务重复执行导致任务重复,调度任务依赖问题导致相同任务并行跑,shell任务中包含调用hive,spark任务,shell任务被kill但是第三方的任务还在继续跑的情况,这些都是比较难控制的。
解决的方案就是全部通过系统的失败重跑来做,而不是手工执行单次脚本。
对于中间包含spark,hive任务的,特别是hive中包含多个applicationId的情况,要从日志中收集出这些applicationId并且记录到本地日志,并且applicationId能够根据既有的规则生成,报错可以反推出来。当任务有问题的时候,kill一遍,尽最大的可能解决所有并发写的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值