Oozie是Apache下面的一个用于流程调度(workflow scheduler)的系统,主要用于管理Hadoop生态圈中的各种任务,目前支持丰富的任务类型:Java MR、Streaming MR、Pig、Hive、Sqoop、Spark、Shell等。如果想详细了解Ooize强大的调度功能,可参考其官方(http://oozie.apache.org/)文档。本文主要讨论使用Oozie来调度任务时可能出现的死锁问题。

严格来说并不是Ooize导致的死锁,而是YARN的调度机制导致的死锁。我们先来解释一下何时会产生死锁,以及原因。之前在《Hadoop系列六——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任务完成返回,这样便产生了死锁。

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

  • 多任务队列。可以看到死锁主要是因为任务抢间占同一资源导致,所以我们可以通过划分多个队列,将父子任务分到不同的队列里面去,各自使用各自队列的资源。比如我们可以专门划分一个队列(假设队列名为root.oozie_launcher)用于放oozie launcher任务,Oozie提供了一个oozie.launcher.mapred.job.queue.name这个配置来设置oozie launcher任务要放置的队列名,目前没有全局配置,只能在每一个workflow.xml里面去配置该选项。当然,单单这样做还是不能比较好的解决这个问题,因为这样只是解决了父子任务竞争同一资源的问题,子任务之间的竞争还没有解决。比如父任务特别多,拉起了非常多的子任务,这些子任务之间因为相互抢占资源,导致都不能返回,那父任务也就会一直等待下去。但我们不可能为每个子任务分配一个单独的队列,而且也无法预估每个子任务到底需要多少资源。这个时候我们就需要另外一种辅助手段了。
  • 并发任务数限制。通过多任务队列的方式我们避免了父子任务的竞争,通过限制队列内并发任务数来限制同一队列内任务的竞争。限制的方式有很多种,在《Hadoop系列六——YARN调度策略》一文中我们已经提到了很多配置项,这里以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%对许多系统来说已经是一件非常不错的事情了。

上述的这些方案也只是个人的一些观点和解决方案,如果你有更好的避免死锁的方案,欢迎讨论指正。