春雨里洗过的太阳

世间所有的相遇,都是久别重逢

oozie的死锁和oozie集群的阻塞

一 oozie的死锁产生原因

​ 严格来说并不是Ooize导致的死锁,而是YARN的调度机制导致的死锁。我们先来解释一下何时会产生死锁,以及原因.

​ YARN目前主要有三种调度策略,而最常使用的是Capacity Scheduler和Fair Scheduler,这两种策略都是基于队列的,而且默认只有一个default队列,也就是所有的任务都是放在这个队列中的;同一队列内使用FIFO、Fair、DRF三者之一。而产生死锁的原因和我们进程死锁道理其实是一样的,YARN支持一个任务(在YARN里面一般叫Application,Oozie里面叫Job,本文统一用任务来指代这两个概念)里面可以产生新的子任务,这样父任务会一直等待所有子任务完成后自己才会完成退出。这样如果某一时刻提交了很多任务,这些任务也会产生若干子任务,而资源是有限的,如果这些父任务占光了所有资源,那产生的子任务就只能一直等待,无法运行。而父任务却一直在等待子任务的返回,这样便产生了死锁。

​ 可见,产生死锁的必要条件就是任务会产生子任务,而Ooize的机制恰好是这样的:Oozie拉起一个YARN应用的机制是先拉起一个MapReduce任务(称为oozie launcher任务),然后该MR任务拉起真正的任务(文章刚开始提到的那些任务)。举个死锁的例子:某一时刻我们通过Oozie提交了n个Spark任务(通过Oozie的Spark Action或Shell Action),这样Oozie会向YARN提交n个MapReduce任务(oozie launcher),假设m(m≤n)个MR任务获得了资源并且创建了spark任务,但此时队列内的资源都被这m个MR任务占用了,所以spark任务一直在等待资源,而那m个MR任务却在等待spark任务完成返回,这样便产生了死锁。

二 死锁的避免

​ 目前我还没有发现有比较完美的方案可以完全杜绝这种死锁的情况,但通过一些手段可以极大的避免死锁:

1 多任务队列

​ 可以看到死锁主要是因为任务抢间占同一资源导致,所以我们可以通过划分多个队列,将父子任务分到不同的队列里面去,各自使用各自队列的资源。比如我们可以专门划分一个队列(假设队列名为root.oozie_launcher)用于放oozie launcher任务,Oozie提供了一个oozie.launcher.mapred.job.queue.name这个配置来设置oozie launcher任务要放置的队列名,目前没有全局配置,只能在每一个workflow.xml里面去配置该选项。当然,单单这样做还是不能比较好的解决这个问题,因为这样只是解决了父子任务竞争同一资源的问题,子任务之间的竞争还没有解决。比如父任务特别多,拉起了非常多的子任务,这些子任务之间因为相互抢占资源,导致都不能返回,那父任务也就会一直等待下去。但我们不可能为每个子任务分配一个单独的队列,而且也无法预估每个子任务到底需要多少资源。这个时候我们就需要另外一种辅助手段了。

2 并发任务数限制

​ 通过多任务队列的方式我们避免了父子任务的竞争,通过限制队列内并发任务数来限制同一队列内任务的竞争。

限制的方式有很多种,本次以Fair Scheduler为例(Capacity Scheduler有对应的配置)列几个比较常用且有效的:

  • maxRunningApps:这个配置是最直观的,限制队列内可以同时运行的任务个数。
  • maxAMShare:这个配置比较隐晦一点,用于限制队列内有多少比例的资源可以用来创建AM(Application Master),默认值为0.5,-1表示不检查AM占用的资源,实质就是不限制。为什么这个也可以限制并发数呢?因为每个YARN应用启动时第一件事情就是申请资源创建AM,我们限制了这个值,就相当于限制了AM的个数,从而也就限制了任务的个数。
  • maxResources:这个配置也比较隐晦,需要我们比较了解YARN的调度机制。YARN的elastic queue特性使得队列之间可以相互抢占资源,所以我们的多任务队列方式并不能完全隔离父子任务的竞争(或者说队列之间的竞争),而该配置限制了某个队列资源的上限(自身分配的资源+从别的空闲队列抢占的资源)。举个例子,比如A队列(运行父任务)的资源配额为a,B队列(运行子任务)的资源配额为b,如果某一时刻父任务特别多,而子任务还没有创建或者运行,即子任务队列B资源是空闲的,那父任务就会从B队列中抢占资源,等到子任务后面再去申请的时候,已经被抢走了,而默认子任务只有在自己被抢占的资源释放后,才会获得,所以这样也就产生了死锁。所以我们可以通过该选项设置每个队列资源上限,保证任何情况下都不要把别的队列的资源全部抢占(但为了提高资源使用率,要适当的允许抢占)。同时,为了避免最坏的情况,我们最好也要开启YARN的Preemption特性(开启及配置方法可参见我之前的文章),保证极端情况下,本队列的任务可以强制拿回属于自己队列的资源。

当然,通过上述两种手段只能降低风险,但无法完全杜绝(除非我们不考虑系统资源的利用率,每个队列同一时刻只允许一个任务运行)。举个极端例子,比如我们有两个队列:父任务队列和子任务队列。某一时刻同时上来了两个父任务,并且他们同时创建了两个子任务,这两个子任务开始的时候只需要少量资源(比如MR任务是边运行边根据情况申请资源的),所以他们都在子任务队列运行起来了,但随着不断运行,一直申请资源,某个时刻资源不够用了(不管是自己队列的资源,还是抢占别的队列之后的),那这两个子任务就只能等待了,这样就又产生了死锁了。不过,从系统稳定性角度来说,一般我们要保证系统的(平均)负载低于某个阈值,典型的比如80%或50%(根据具体场景不同),而不是一味的追求太高的资源使用率(比如之前参加阿里菜鸟网络的一个技术分享会的时候,他们说他们的云平台如果检测到CPU使用率超过50%就会预警。其依据是现在的CPU都是一个物理核再虚拟一个核出来)。所以我认为对于YARN中的资源使用也一样,资源使用一直很高并非一件好事,我觉得资源平均使用率能超过50%对许多系统来说已经是一件非常不错的事情了。

三 oozie的集群阻塞

背景:

生产中使用oozie调度多个任务流,其中包括hive脚本,MapReduce程序,python脚本。

现象:

集群中多个oozie launcher的MR占用大量资源,真正跑数据的任务在hung on等待状态,导致集群阻塞。

问题分析:

  1. 使用oozie调度时启动数据任务前会启动对应的oozie launcher来监控调度任务
  2. 任务多导致oozie launcher的并发高导致oozie launcher会启动多个,占用大量资源
  3. oozie launcher的声明周期是数据任务开始之前到结束,资源不会释放
  4. 当资源别oozie launcher抢占到不能分配给数据任务时oozie会等待有空闲资源来分配给数据任务,导致整个集群陷入阻塞
  5. oozie launcher单个占用资源较大会使用2个container,2core,4G内存(默认)

问题解决方案:

  1. 较少单个oozie launcher的资源占用
  2. 限制oozie并发的数量

问题解决实现:

  1. 配置oozie launcher自己的资源池来实现资源隔离
  2. 配置参数减小oozie launcher的资源使用量

参数:

1
2
3
4
5
6
这个配置来设置oozie launcher任务要放置的队列名,目前没有全局配置,只能在每一个workflow.xml里面去配置该选项
oozie.launcher.mapred.job.queue.name root.oozie
减少oozie launcher单个资源占用
oozie.launcher.mapreduce.map.memory.mb 512
oozie.launcher.yarn.app.mapreduce.am.resource.mb 512
mapred.job.queue.name oozie真正调起的任务的队列设置(job的队列设置)

参考的连接:

1
2
https://niyanchun.com/oozie-deadlock-solution.html
https://blog.csdn.net/bigdataprimary/article/details/84314377