spark之DAG、Shuffle概述、框架核心概念、集群模式安装

DAG概念

概述

Spark会根据用户提交的计算逻辑中的RDD的转换和动作来生成RDD之间的依赖关系,同时这个计算链也就生成了逻辑上的DAG。接下来以“Word Count”为例,详细描述这个DAG生成的实现过程。

Spark Scala版本的Word Count程序如下:

1: val file=sc.textFile(“hdfs://hadoop01:9000/hello1.txt”) 2: val counts = file.flatMap(line => line.split(" "))
3: .map(word => (word, 1))
4: .reduceByKey(_ + _)
5: counts.saveAsTextFile("hdfs:// ")

file和counts都是RDD,其中file是从HDFS上读取文件并创建了RDD,而counts是在file的基础上通过flatMap、map和reduceByKey这三个RDD 转换生成的。最后,counts调用了动作saveAsTextFile,用户的计算逻辑就从这里开始提交的集群进行计算。那么上面这5行代码的具体实现是什么呢?

1)行1:sc是org.apache.spark.SparkContext的实例,它是用户程序和Spark的交互接口,会负责连接到集群管理者,并根据用户设置或者系统默 认 设 置 来 申 请 计 算 资 源 , 完 成 RDD 的 创 建 等 。 sc.textFile("hdfs:// ")就完成了一个
org.apache.spark.rdd.HadoopRDD的创建,并且完成了一次RDD的转

换:通过map转换到一个org.apache.spark.rdd.MapPartitions-RDD。也就是说,file实际上是一个MapPartitionsRDD,它保存了文件的所有行的数据内容。

2)行2:将file中的所有行的内容,以空格分隔为单词的列表,然后将这个按照行构成的单词列表合并为一个列表。最后,以每个单词为元素的列表被保存到MapPartitionsRDD。

3)行3:将第2步生成的MapPartittionsRDD再次经过map将每个单词word转为(word,1)的元组。这些元组最终被放到一个MapPartitionsRDD中。

4)行4:首先会生成一个MapPartitionsRDD,起到map端combiner的作用;然后会生成一个ShuffledRDD,它从上一个RDD的输出读取数
据,作为reducer的开始;最后,还会生成一个MapPartitionsRDD,起到reducer端reduce的作用。

5)行5:向HDFS输出RDD的数据内容。最后,调用org.apache.spark.SparkContext#runJob向集群提交这个计算任务。

RDD之间的关系可以从两个维度来理解:一个是RDD是从哪些RDD转换而来,也就是RDD的parent RDD(s)是什么;还有就是依赖于
parent RDD(s)的哪些Partition(s)。这个关系,就是RDD之间的依赖,org.apache.spark.Dependency。根据依赖于parent RDD(s)的Partitions的不同情况,Spark将这种依赖分为两种,一种是宽依赖,一种

是窄依赖。

RDD的依赖关系

RDD的依赖关系

RDD和它依赖的parent RDD(s)的关系有两种不同的类型,即窄依赖
(narrow dependency)和宽依赖(wide dependency)。
1)窄依赖指的是每一个parent RDD的Partition最多被子RDD的一个Partition使用,如下图所示。

在这里插入图片描述

2)宽依赖指的是多个子RDD的Partition会依赖同一个parent RDD的Partition。

我们可以从不同类型的转换来进一步理解RDD的窄依赖和宽依赖的区别,如下图所示。

在这里插入图片描述

窄依赖

对于窄依赖操作,它们只是将Partition的数据根据转换的规则进行转化,并不涉及其他的处理,可以简单地认为只是将数据从一个形式转换到另一个形式。

窄依赖底层的源码:

abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
//返回子RDD的partitionId依赖的所有的parent RDD的Partition(s) 
def getParents(partitionId: Int): Seq[Int]
override def rdd: RDD[T] = _rdd
}

class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) { 
override def getParents(partitionId: Int) = List(partitionId)
}

所以对于窄依赖,并不会引入昂贵的Shuffle。所以执行效率非常高。如果整个DAG中存在多个连续的窄依赖,则可以将这些连续的窄依赖整合到一起连续执行,中间不执行shuffle 从而提高效率,这样的优化方式称之为流水线优化。
此外,针对窄依赖,如果子RDD某个分区数据丢失,只需要找到父RDD对应依赖的分区,恢复即可。但如果是宽依赖,当分区丢失时,最糟糕的情况是要重算所有父RDD的所有分区。所以Spark为了解决宽依赖,某个子RDD分区数据丢失导致重新计算代价可能较大情况, Shuffle产生的中间一定要进行持久化(存到文件里)

窄依赖:没有shuffle,没有磁盘I/O 宽依赖:有shuffle,有磁盘I/O

宽依赖

对于groupByKey这样的操作,子RDD的所有Partition(s)会依赖于parent RDD的所有Partition(s),子RDD的Partition是parent RDD的所有Partition Shuffle的结果。

宽依赖的源码:

class ShuffleDependency[K, V, C]( @transient _rdd: RDD[_ <: Product2[K, V]], val partitioner: Partitioner,
val serializer: Option[Serializer] = None,
val keyOrdering: Option[Ordering[K]] = None,
val aggregator: Option[Aggregator[K, V, C]] = None, val mapSideCombine: Boolean = false)
extends Dependency[Product2[K, V]] {

override def rdd = _rdd.asInstanceOf[RDD[Product2[K, V]]]
//获取新的shuffleId
val shuffleId: Int = _rdd.context.newShuffleId()
//向ShuffleManager注册Shuffle的信息val shuffleHandle: ShuffleHandle =
_rdd.context.env.shuffleManager.registerShuffle( shuffleId, _rdd.partitions.size, this)

_rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this))
}

Shuffle概述

spark中一旦遇到宽依赖就需要进行shuffle的操作,所谓的shuffle的操作的本质就是将数据汇总后重新分发的过程。

这个过程数据要汇总到一起,数据量可能很大所以不可避免的需要进行数据落磁盘的操作,会降低程序的性能,所以spark并不是完全内存不读写磁盘,只能说它尽力避免这样的过程来提高效率 。

spark中的shuffle,在早期的版本中,会产生多个临时文件,但是这种多临时文件的策略造成大量文件的同时的读写,磁盘的性能被分摊给多个文件,每个文件读写效率都不高,影响spark的执行效率。所以在后续的spark中(1.2.0之后的版本)的shuffle中,只会产生一个文
件,并且数据会经过排序再附加索引信息,减少了文件的数量并通过排序索引的方式提升了性能。

DAG的生成与Stage的划分

DAG的生成

原始的RDD(s)通过一系列转换就形成了DAG。RDD之间的依赖关系,包含了RDD由哪些Parent RDD(s)转换而来和它依赖parent RDD(s)的哪些Partitions,是DAG的重要属性。

借助这些依赖关系,DAG可以认为这些RDD之间形成了Lineage(血统,血缘关系)。借助Lineage,能保证一个RDD被计算前,它所依赖的parent RDD都已经完成了计算;同时也实现了RDD的容错性,即如果一个RDD的部分或者全部的计算结果丢失了,那么就需要重新计算这部分丢失的数据。

RDD是分布式的,弹性的,容错的数据结构

Spark的Stage(阶段)

Spark在执行任务(job)时,首先会根据依赖关系,将DAG划分为不同的阶段(Stage)。

处理流程是:
1)Spark在执行Transformation类型操作时都不会立即执行,而是懒执行(计算)
2)执行若干步的Transformation类型的操作后,一旦遇到Action类型操作时,才会真正触发执行(计算)
3)执行时,从当前Action方法向前回溯,如果遇到的是窄依赖则应用流水线优化,继续向前找,直到碰到某一个宽依赖
4)因为宽依赖必须要进行shuffle,无法实现优化,所以将这一次段执行过程组装为一个stage
5)再从当前宽依赖开始继续向前找。重复刚才的步骤,从而将这个DAG还分为若干的stage

在这里插入图片描述

在stage内部可以执行流水线优化,而在stage之间没办法执行流水线优化,因为有shuffle。但是这种机制已经尽力的去避免了shuffle。

Spark的Job和Task

原始的RDD经过一系列转换后(一个DAG),会在最后一个RDD上触发一个动作,这个动作会生成一个Job。
所以可以这样理解:一个DAG对应一个Spark的Job。

在Job被划分为一批计算任务(Task)后,这批Task会被提交到集群上的计算节点去计算Spark的Task分为两种:
1)org.apache.spark.scheduler.ShuffleMapTask 2)org.apache.spark.scheduler.ResultTask
简单来说,DAG的最后一个阶段会为每个结果的Partition生成一个ResultTask,其余所有的阶段都会生成Shuff

fleMapTask。

可视化理解窄依赖和宽依赖

案例 单词统计

scala>val data=sc.textFile(“/home/software/hello.txt”,2)
scala> data.flatMap(.split(" ")).map((,1)).reduceByKey(+).collect

1)打开web页面控制台(ip:4040端口地址),刷新,会发现刚才的操作会出现在页面上

2)点击 Description下的 collect at…… 进入job的详细页面
3)点击 DAG Visualization 会出现如下图形

Spark框架核心概念

1.RDD。弹性分布式数据集,是Spark最核心的数据结构。有分区机制,所以可以分布式进行处理。有容错机制,通过RDD之间的依赖关系(血缘关系)来恢复数据。

2.依赖关系。RDD的依赖关系是通过各种Transformation(变换,懒方法,也叫算子)来得到的。父RDD和子RDD之间的依赖关系分两种:①窄依赖 ②宽依赖
①针对窄依赖:父RDD的分区和子RDD的分区关系是:一对一
窄依赖不会发生Shuffle,执行效率高,spark框架底层会针对多个连续的窄依赖执行流水线优化,把多个Task合并成一个Task来执行。从而提高性能。例如 map flatMap等方法都是窄依赖方法
②针对宽依赖:父RDD的分区和子RDD的分区关系是:一对多宽依赖会产生shuffle,会产生磁盘读写,无法优化。

3.DAG。有向无环图,当一整条RDD的依赖关系形成之后,就形成了一个DAG。一般来说, 一个DAG,最后都至少会触发一个Action操作,触发执行。一个Action对应一个Job任务。

4.Stage。一个DAG会根据RDD之间的依赖关系进行Stage划分,流程是:以Action为基准, 向前回溯,遇到宽依赖,就形成一个Stage。遇到窄依赖,则执行流水线优化(将多个连续的窄依赖放到一起执行)

5.task。任务。一个分区对应一个task。每个task会在spark服务器的一个进程里来运行。可以这样理解:一个Stage是一组Task的集合

6.RDD的Transformation(变换)操作:懒执行,并不会立即执行

7.RDD的Action(执行)操作:触发真正的执行

Spark集群模式安装

实现步骤:
1)上传解压spark安装包
2)进入spark安装目录的conf目录3)配置spark-env.sh文件

配置示例: #本机ip地址

SPARK_LOCAL_IP=hadoop01

#spark的shuffle中间过程会产生一些临时文件,此项指定的是其存放目录,不配置默认是在
/tmp目录下

SPARK_LOCAL_DIRS=/home/software/spark/tmp export JAVA_HOME=/home/software/jdk1.8

4)在conf目录下,编辑slaves文件
配置示例:

hadoop01 hadoop02 hadoop03
5)配置完后,将spark目录发送至其他节点,并更改对应的 SPARK_LOCAL_IP 配置

启动集群

1)如果你想让 01 虚拟机变为master节点,则进入01 的spark安装目录的sbin目录执行: sh start-all.sh

2)通过jps查看各机器进程, 01:Master +Worker

02:Worker 03:Worker
3)通过浏览器访问管理界面

http://192.168.234.11:8080
在这里插入图片描述

4)通过spark shell 连接spark集群进入spark的bin目录
执行:sh spark-shell.sh --master spark://192.168.234.11:7077

6)在集群中读取文件:

sc.textFile(“/root/work/words.txt”)

默认读取本机数据 这种方式需要在集群的每台机器上的对应位置上都一份该文件 浪费磁盘7)所以应该通过hdfs存储数据
sc.textFile(“hdfs://hadoop01:9000/mydata/words.txt”);

注:可以在spark-env.sh 中配置选项HADOOP_CONF_DIR 配置为hadoop的etc/hadoop的地址 使默认访问的是hdfs的路径
注:如果修改默认地址是hdfs地址 则如果想要访问文件系统中的文件 需要指明协议为file 例如

sc.text(“file:///xxx/xx”)

案例—:WordCount

实现步骤
1)创建spark的项目
在scala中创建项目 导入spark相关的jar包

2)开发spark相关代码

代码示例:

import org.apache.spark.SparkConf import org.apache.spark.SparkContext

object WordCountDriver {

def main(args: Array[String]): Unit = {

val conf=new SparkConf().setMaster("spark://hadoop01:7077").setAppName("wordcount") val sc=new SparkContext(conf)

val data=sc.textFile("hdfs://hadoop01:9000/words.txt", 2)
val result=data.flatMap { x => x.split(" ") }.map { x => (x,1) }.reduceByKey(_+_)

result.saveAsTextFile("hdfs://hadoop01:9000/wcresult")
}
}

3)将写好的项目打成jar,上传到服务器,进入bin目录
执行:spark-submit --class cn.tedu.WordCountDriver /home/software/spark/conf/wc.jar

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值