基于YARN的Spark程序工作过程

来源:互联网 发布:淘宝lolcdk是真的吗 编辑:程序博客网 时间:2024/05/21 00:56

一. YARN的理解

YARN是Hadoop 2.x版本的产物,它最基本的设计思想是将JobTracker的两个主要功能,即资源管理,作业调度和监

分解成为两个独立的进程。再详细介绍Spark程序工作过程前,先简单的介绍一下YARN,即Hadoop的操作系统,

不仅支持MapReduce计算框架,而且还支持流式计算框架,迭代计算框架,MPI并行计算框架等,实现时采用了基于

事件的驱动机制。

YARN的架构图,如下所示:


1. ResourceManager

ResourceManager类似JobTracker,包括两个主要的组件:调度器(Scheduler)和应用程序管理器

(ApplicationManager)。分别介绍,如下所示:

(1)Scheduler的主要功能是负责分配资源到各个正在运行的应用程序中,它是基于资源的请求来执行调度功能的。

Scheduler是基于Container的抽象概念,包括内存,CPU,磁盘和网络等;

(2)ApplicationMaster的主要功能是负责接送提交的作业,协商第一个执行该任务的Container,并提供失败作业的

重启。每个应用的ApplicationMaster负责与Scheduler谈判资源占用的Container数量,追踪状态和监控进程。

2. NodeManager

NodeManager类似TaskTracker,它的主要功能是负责启动Container,监控Container的资源(CPU,内存,磁盘和

网络等),并将信息上报给ResourceManager。


二. Spark基本框架

一个Spark应用程序由一个Driver程序和多个Job构成。一个Job由多个Stage组成。一个Stage由多个没有Shuffle关系

的Task组成。Spark基本框架,如下所示:


Spark应用程序都离不开SparkContext和Executor两部分,Executor负责执行任务,运行Executor的机器称为Worker 

Node,SparkContext由用户程序启动,通过Cluster Manager和Executor通信。Cluster Manager负责集群的资源管理

和调度,现在支持Standalone,Apache Mesos和Hadoop的YARN三种类型。如下所示:

(1)Standalone:Spark原生的资源管理,由Master负责资源的分配,可以在亚马逊的EC2上运行。

(2)Apache Mesos:与Hadoop MapReduce兼容性良好的一种资源调度框架。

(3)Hadoop YARN:主要指的是YARN中的ResourceManager。

详细来说,以SparkContext为应用程序运行的总入口,在SparkContext的初始化过程中,Spark会分别创建

DAGScheduler作业调度和TaskScheduler任务调度两级调度模块。如下所示:

(1)DAGScheduler:根据Job构建基于Stage的DAG,并提交Stage给TaskScheduler。其划分Stage的依据是RDD

之间的依赖关系。

(2)TaskScheduler:将Taskset提交给Worker(集群)运行,每个Executor运行什么Task就是在此处分配的。

作业调度模块是基于任务阶段的高层调度模块,它为每个Spark作业计算具有依赖关系的多个调度阶段(通常根据

shuffle来划分),然后为每个阶段构建出一组具体的任务(通常会考虑数据的本地性等),然后以TaskSets(任务

组)的形式提交给任务调度模块来具体执行。而任务调度模块则负责具体启动任务、监控和汇报任务运行情况。

说明:一个Application中可能会产生多个Job,一个Job包含多个Stage,一个Stage包含多个Task。


三. RDD及其计算方式(Transformation和Action)

Spark建立在统一抽象的RDD之上,使得它可以以基本一致的方式应对不同的大数据处理场景,包括MapReduce,

Streaming,SQL,Machine Learning以及Graph等。我们现在先不关心什么是RDD,什么是窄依赖和宽依赖,什么

是Lineage,我们现在只关心RDD提供的两类操作:转换(Transformation)和动作(Action)。Transformation就是

根据现有的数据集创建一个新的数据集,而Action就是在数据集上运行计算后,返回一个值给Driver程序。

1. 转换(Transformation)操作如下所示:




2. 动作(Action)操作如下所示:



所有Spark中的Transformation都是惰性的,即并不会马上发生计算,它只是记住应用到基础数据集上的这些

Transformation。而这些Transformation,只会在有一个Action发生,要求返回结果给Driver程序时,才真正进行计

算。这个设计让Spark更加有效率的运行。比如,我们可以实现通过Map创建一个数据集,然后再用Reduce,而只返

回Reduce的结果给Driver,而不是整个大的数据集。貌似与Linux中的管道做操比较类似。要熟练应用上述的

Transformation和Action操作,写出简捷高效的代码解决遇到的问题。[7][8]

(1)join算子

var rdd1 = sc.makeRDD(Array(("A","1"),("B","2"),("C","3")),2)var rdd2 = sc.makeRDD(Array(("A","a"),("C","c"),("D","d")),2)
1)join
rdd1.join(rdd2).collect 2 Array[(String, (String, String))] = Array((A,(1,a)), (C,(3,c)))

2)leftOuterJoin

rdd1.leftOuterJoin(rdd2).collect 2 Array[(String, (String, Option[String]))] = Array((B,(2,None)), (A,(1,Some(a))), (C,(3,Some(c))))
3)rightOuterJoin
rdd1.rightOuterJoin(rdd2).collect 2 Array[(String, (Option[String], String))] = Array((D,(None,d)), (A,(Some(1),a)), (C,(Some(3),c)))

4)fullOuterJoin

rdd1.fullOuterJoin(rdd2).collect 2 Array[(String, (Option[String], Option[String]))] = Array((B,(Some(2),None)), (D,(None,Some(d))), (A,(Some(1),Some(a))), (C,(Some(3),Some(c))))

5)cartesian

rdd1.cartesian(rdd2).collect 2 Array[((String, String), (String, String))] = Array(((A,1),(A,a)), ((A,1),(C,c)), ((A,1),(D,d)), ((B,2),(A,a)), ((C,3),(A,a)), ((B,2),(C,c)), ((B,2),(D,d)), ((C,3),(C,c)), ((C,3),(D,d)))

(2)sortBy和sortByKey算子

sortBy是对标准RDD进行排序,而sortByKey函数是对PairRDD进行排序,也就是有Key和Value的RDD。如下所示:

1)sortBy算法

val result = sc.parallelize(List(3,1,90,3,5,12)).sortBy(x => x, false, 1)Array[Int] = Array(90, 12, 5, 3, 3, 1)

2)sortByKey算法

val a = sc.parallelize(List("wyp", "iteblog", "com", "397090770", "test"), 2)val b = sc. parallelize (1 to a.count.toInt , 2)val c = a.zip(b)c.sortByKey().collectArray[(String, Int)] = Array((397090770,4), (com,3), (iteblog,2), (test,5), (wyp,1))

像soryBy函数中的第一个参数可以对排序方式进行重写一样,在OrderedRDDFunctions类中有个变量ordering是隐形

的:private val ordering = implicitly[Ordering[K]],即默认的排序规则,我们通过String对Int进行排序。如下所示:

implicit val sortIntegersByString = new Ordering[Int] {  override def compare(a: Int, b: Int) = a.toString.compare(b.toString)}

这样sortIntegersByString修改了默认的排序规则,将默认按照Int大小排序改成了对字符串的排序。

说明:除了sortByKey外,还有groupByKey,reduceByKey,aggregateByKey,combineByKey等。

(3)zip算子

1)zip

zip函数将传进来的两个参数中相应位置上的元素组成一个pair数组。如果其中一个参数元素比较长,那么多余的参数

会被删掉。

val numbers = Seq(0, 1, 2, 3, 4)val series = Seq(0, 1, 1, 2, 3)numbers zip seriesSeq[(Int, Int)] = List((0,0), (1,1), (2,1), (3,2), (4,3), (5,5)

2)zipAll

zipAll函数和zip函数类似,但是如果其中一个元素个数比较少,那么将用默认的元素填充。 

val xs = List(1, 2, 3)val ys = List('a', 'b')val zs = List("I", "II", "III", "IV")val x = 0val y = '_'val z = "_"xs.zipAll(ys, x, y)List[(Int, Char)] = List((1,a), (2,b), (3,_))xs.zipAll(zs, x, z)List[(Int, java.lang.String)] = List((1,I), (2,II), (3,III), (0,IV))
3)zipped
val values = List.range(1, 5)(values, values).zipped toMapscala.collection.immutable.Map[Int,Int] = Map(1 -> 1, 2 -> 2, 3 -> 3, 4 -> 4)val sumOfSquares = (values, values).zipped map (_ * _) sumsumOfSquares: Int = 30

4)zipWithIndex

zipWithIndex函数将元素和其所在的下标组成一个pair。

val series = Seq(0, 1, 1, 2, 3, 5, 8, 13)series.zipWithIndexSeq[(Int, Int)] = List((0,0), (1,1), (1,2), (2,3), (3,4), (5,5), (8,6), (13,7))

5)unzip

unzip函数可以将一个元组的列表转变成一个列表的元组。

val seriesIn = Seq(0, 1, 1, 2, 3, 5, 8, 13)val fibonacci = seriesIn.zipWithIndexfibonacci.unzip(Seq[Int], Seq[Int]) = (List(0, 1, 1, 2, 3, 5, 8, 13),List(0, 1, 2, 3, 4, 5, 6, 7))

(4)coalesce算子

def coalesce(numPartitions: Int, shuffle: Boolean = false)(implicit ord: Ordering[T] = null): RDD[T],该函数用于将

RDD进行重分区,使用HashPartitioner。第一个参数为重分区的数目,第二个为是否进行shuffle,默认为false。需要

说明的是,如果重分区的数目大于原来的分区数,那么必须指定shuffle参数为true。

(5)repartition算子

def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T],该函数其实就是coalesce函数第二个参数

为true的实现,即默认会进行shuffle操作。

(6)randomSplit算子

def randomSplit(weights: Array[Double], seed: Long = Utils.random.nextLong): Array[RDD[T]],该函数根据weights

权重,将一个RDD切分成多个RDD。该权重参数为一个Double数组,第二个参数为random的种子,基本可忽略。

(7)glom算子

def glom(): RDD[Array[T]],该函数是将RDD中每一个分区中类型为T的元素转换成Array[T],这样每一个分区就只有

一个数组元素。

(8)map算子

1)mapPartitions

mapPartitions和map的一个重要区别是,map针对的是RDD中的每个元素,而mapPartitions针对的是RDD中的每个

分区。这样的话,在映射过程中需要频繁创建额外对象的时候,使用mapPartitions要比map高。比如,在将RDD中的

所有数据通过JDBC链接写入数据库的时候,如果使用map,那么需要为每个元素创建一个连接,但是如果使用

mapPartitions,那么仅需要为每个分区创建一个连接即可。

函数原型,如下所示:

def mapPartitions[U](f: (Iterator[T]) => Iterator[U], preservesPartitioning: Boolean = false)(implicit arg0: 

ClassTag[U]): RDD[U]

f即输入函数,它处理每个分区里面的内容。每个分区中的内容将以Iterator[T]传递给输入函数f,f的输出结果是

Iterator[U]。最终的RDD是由所有分区经过输入函数处理后的结果合并起来的。其中,参数preservesPartitioning表示

是否保留父RDD的partitioner分区信息。

举个例子,如下所示:

var rdd1 = sc.makeRDD(1 to 5,2) //rdd1有两个分区scala> var rdd3 = rdd1.mapPartitions{ x => {  | var result = List[Int]()  |     var i = 0  |     while(x.hasNext){    |       i += x.next()    |     }  |     result.::(i).iterator  | }}//rdd3将rdd1中每个分区中的数值累加rdd3: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[84] at mapPartitions at :23scala> rdd3.collectres65: Array[Int] = Array(3, 12)scala> rdd3.partitions.sizeres66: Int = 2
解析:result.::(i).iterator的意思是将i添加到List[Int]中,并且转换成为迭代器。可以使用:::运算符或列表List.:::()方法或

List.concat()方法来添加两个或多个列表。

2)mapPartitionsWithIndex

函数原型,如下所示:

def mapPartitionsWithIndex[U](f: (Int, Iterator[T]) => Iterator[U], preservesPartitioning: Boolean = false)(implicit 

arg0: ClassTag[U]): RDD[U]

mapPartitionsWithIndex与mapPartitions的功能类似,只是多传入split index,因此函数必须是(Int, Iterator[T]) => 

Iterator[U]类型。其中,第一个参数为分区的索引。

举个例子,如下所示:

var rdd1 = sc.makeRDD(1 to 5,2)//rdd1有两个分区var rdd2 = rdd1.mapPartitionsWithIndex{  (x,iter) => {    var result = List[String]()    var i = 0    while(iter.hasNext){      i += iter.next()    }    result.::(x + "|" + i).iterator  }}//rdd2将rdd1中每个分区的数字累加,并在每个分区的累加结果前面加了分区索引scala> rdd2.collectres13: Array[String] = Array(0|3, 1|12)

(9)aggregate算子 [9]

1)aggregate

2)aggregateByKey

3)treeAggregate

(10)groupBy和groupByKey算子

1)groupBy

各种变体,如下所示:

def groupBy[K: ClassTag](f: T => K): RDD[(K, Iterable[T])]

def groupBy[K: ClassTag](f: T => K, numPartitions: Int): RDD[(K, Iterable[T])]

def groupBy[K: ClassTag](f: T => K, p: Partitioner): RDD[(K, Iterable[T])]

举个例子,如下所示:

val a = sc.parallelize(1 to 9, 3)a.groupBy(x => { if (x % 2 == 0) "even" else "odd" }).collectres42: Array[(String, Seq[Int])] = Array((even,ArrayBuffer(2, 4, 6, 8)), (odd,ArrayBuffer(1, 3, 5, 7, 9)))

2)groupByKey

各种变体,如下所示:

def groupByKey(): RDD[(K, Iterable[V])]

def groupByKey(numPartitions: Int): RDD[(K, Iterable[V])]

def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])]

举个例子,如下所示:

val a = sc.parallelize(List("dog", "tiger", "lion", "cat", "spider", "eagle"), 2)val b = a.keyBy(_.length)b.groupByKey.collectres11: Array[(Int, Seq[String])] = Array((4,ArrayBuffer(lion)), (6,ArrayBuffer(spider)),(3,ArrayBuffer(dog, cat)), (5,ArrayBuffer(tiger, eagle)))

说明:根据字符串的长度进行分组。


四. DAGScheduler作业调度和TaskScheduler任务调度

简而言之,DAGScheduler负责任务的逻辑调度,即将作业拆分成不同阶段的具有依赖关系的多批任务;而

TaskScheduler负责具体任务的实际物理调度。

1. 作业调度相关概念


(1)Task(任务):单个分区数据集上的最小处理流程单元。

(2)TaskSet(任务集):由一组关联的,但互相之间没有shuffle依赖关系的任务所组成的任务集。

(3)Stage(调度阶段):一个任务集对应的调度阶段。

(4)Job(作业):由一个RDD Action生成的一个或多个调度阶段所组成的一次计算作业。

(5)Application(应用程序):Spark应用程序,由一个或多个作业组成。

2. RDD的依赖关系

RDD之间的依赖关系可以分为窄依赖和宽依赖,其实前者就是n:1的关系,后者是n:m的关系,如下所示:


(1)窄依赖:每个父RDD的Partition最多被子RDD的一个Partition所使用。比如map、filter等算子。窄依赖的具体实

现有2种,一种是OneToOneDependency,另一种是RangeDependency。

(2)宽依赖:一个父RDD的Partition会被多个子RDD的Partition所使用。比如groupByKey、reduceByKey等算子。

宽依赖的具体实现仅有1种,即ShuffleDependency。宽依赖支持2种Shuffle Manager,分别是

HashShuffleManager(基于Hash的Shuffle机制)和SortShuffleManager(基于Sort的Shuffle机制)。

说明:特别需要说明的是join算子既可能是窄依赖,也可能是宽依赖。

3. Job中Stage的划分

Stage之间根据依赖关系变成了一个大粒度的DAG,如下所示:


(1)Spark中每个Action对应一个Job。

(2)DAGScheduler根据ShuffleDependency将Job分解成具有依赖关系的多个Stage。

(3)Stage分为ShuffleMapStage和ResultStage。一个Job中包含多个ShuffleMapStage和一个ResultStage。

(4)一个Stage包含多个Tasks,Task的个数即该Stage的finalRDD的Partition个数。

(5)一个Stage中的Task完全相同,ShuffleMapStage包含的都是ShuffleMapTask,而ResultStage包含的都是

ResultTask。

说明:提交作业时DAGScheduler会从RDD依赖链尾部开始,遍历整个依赖链划分调度阶段。划分阶段以

ShuffleDependency为依据,当没有ShuffleDependency时整个Job只会有一个Stage。以上图为例,G是finalRDD,G

和B之间是宽依赖,所以划分到一个Stage3中。B和F是窄依赖,并且C、D、E和F都是宽依赖,所以C、D、E和F划

分到Stage2中。B和A是窄依赖,所以A划分到Stage1中。


4. Spark作业调度系统


(1)RDD Objects可以理解为用户实际代码中创建的RDD,这些代码逻辑上组成了一个DAG。

(2)当一个RDD操作触发计算,向DAGScheduler提交作业时,DAGScheduler将Job分解成具有依赖关系的多个

Stage。

(3)DAGScheduler通过TaskScheduler接口提交任务集,并且由TaskSetManager管理这个任务集的生命周期。

说明:在不同的资源管理框架下,TaskScheduler的实现方式也是不同的。对于Local、Standalone和Mesos来说,它

们的TaskScheduler就是TaskSchedulerImpl,而对于YARN Cluster和YARN Client来说,它们的TaskScheduler的实现

继承于TaskSchedulerImpl。

(4)最终一个具体的Task在Executor中执行。

说明:Executor是某个Application运行在Worker节点上的一个进程,而Worker节点对于Standalone模式来说就是通过

slave文件配置的Worker节点,在Spark On YARN模式中就是NodeManager节点。


参考文献:

[1] YARN简介:http://www.ibm.com/developerworks/cn/data/library/bd-yarn-intro/index.html

[2] Hadoop新MapReduce框架Yarn详解:http://www.ibm.com/developerworks/cn/opensource/os-cn-hadoop-yarn/

[3] Spark应用程序的运行架构:http://www.voidcn.com/blog/hwssg/article/p-2531423.html

[4] RDD:基于内存的集群计算容错抽象:http://shiyanjun.cn/archives/744.html

[5] 理解Spark的核心RDD:http://www.infoq.com/cn/articles/spark-core-rdd/

[6] Zip函数族详解:http://www.iteblog.com/archives/1225

[7] Spark RDD算子源码解读:http://blog.csdn.net/tanglizhe1105/article/details/49582815

[8] Spark算子系列文章:http://lxw1234.com/archives/2015/07/363.htm

[9] Apache Spark RDD API By Example:http://homepage.cs.latrobe.edu.au/zhe/ZhenHeSparkRDDAPIExamples.html