基于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
- 基于YARN的Spark程序工作过程
- 在基于Yarn的集群上运行Spark程序
- Yarn运行Mapreduce程序的工作原理
- Spark on YARN工作原理
- 基于Spark on Yarn的淘宝数据挖掘平台
- 基于Yarn运行spark应用的进程分析
- 基于Spark on Yarn的apriori算法java实现
- spark远程debug之调试spark on yarn 程序(基于CDH平台,1.6.0版本)
- Spark on Yarn安装过程遇到的错误
- spark程序的运行过程
- YARN的工作流程
- YARN的工作流程
- yarn的工作流程
- yarn的工作流程
- YARN的工作原理
- Spark UI (基于Yarn) 分析与定制
- Spark UI (基于Yarn) 分析与定制
- 基于virtualbox安装spark-yarn-cluster
- python字符编码问题
- 虚拟机安装centos7.2
- leetcode(104). Maximum Depth of Binary Tree
- swift-stroybord和xib关联文件
- Image类跟Sprite类有什么区别?
- 基于YARN的Spark程序工作过程
- 【Java编程教材】Java教程之多线程编程
- tensorflow教程学习二MNIST
- 将已有项目下的html转为haml
- JS的逻辑思维练习
- TensorFlow学习——CIFAR-10(二)代码实现
- ios-drawrect方法解析
- Java集合类: Set、List、Map、Queue使用场景梳理
- BestCoder Round #2 解题报告