分布式计算原理
一 宽依赖和窄依赖
1、宽窄依赖含义
- Spark中RDD的高效与DAG(有向无环图)有着很大的关系,在DAG调度中需要对计算过程划分stage,而划分依据就是RDD之间的依赖关系。针对不同的转换函数,RDD之间的依赖关系分类窄依赖(narrow dependency)和宽依赖(wide dependency, 也称 shuffle dependency)
- 窄依赖是指父RDD的每个分区只被子RDD的一个分区所使用,子RDD分区通常对应有限个父RDD分区(O(1),与数据规模无关)
- 相应的,宽依赖是指父RDD的每个分区都可能被多个子RDD分区所使用,子RDD分区通常对应所有的父RDD分区(O(n),与数据规模有关)
- 具体可以查看下图所示
2、窄依赖的优化有利性
-
相比于宽依赖,窄依赖对优化很有利 ,主要基于以下两点:
-
宽依赖往往对应着shuffle操作,需要在运行过程中将同一个父RDD的分区传入到不同的子RDD分区中,中间可能涉及多个节点之间的数据传输;而窄依赖的每个父RDD的分区只会传入到一个子RDD分区中,通常可以在一个节点内完成转换。
-
当RDD分区丢失时(某个节点故障),spark会对数据进行重算。
- 对于窄依赖,由于父RDD的一个分区只对应一个子RDD分区,这样只需要重算和子RDD分区对应的父RDD分区即可,所以这个重算对数据的利用率是100%的;
- 对于宽依赖,重算的父RDD分区对应多个子RDD分区,这样实际上父RDD 中只有一部分的数据是被用于恢复这个丢失的子RDD分区的,另一部分对应子RDD的其它未丢失分区,这就造成了多余的计算;更一般的,宽依赖中子RDD分区通常来自多个父RDD分区,极端情况下,所有的父RDD分区都要进行重新计算。
- 如下图所示,b1分区丢失,则需要重新计算a1,a2和a3,这就产生了冗余计算(a1,a2,a3中对应b2的数据)。
-
-
总结:首先,窄依赖允许在一个集群节点上以流水线的方式(pipeline)计算所有父分区。例如,逐个元素地执行map、然后filter操作;而宽依赖则需要首先计算好所有父分区数据,然后在节点之间进行Shuffle,这与MapReduce类似。第二,窄依赖能够更有效地进行失效节点的恢复,即只需重新计算丢失RDD分区的父分区,而且不同节点之间可以并行计算;而对于一个宽依赖关系的Lineage图,单个节点失效可能导致这个RDD的所有祖先丢失部分分区,因而需要整体重新计算。所以也就是说对于窄依赖,它的父RDD的重算都是必须的,不会产生冗余。
3、款窄依赖算子
- 窄依赖算子:map, flatMap, filter, union, join(父RDD是hash-partitioned ), mapPartitions, mapValues
- 宽依赖算子:distinct, …ByKey, join(父RDD不是hash-partitioned), partitionBy,groupBy
4、WordCount运行中的宽窄依赖
- 宽依赖对应Shuffle过程,由此产生另一个Stage。
二 DAG(有向无环图)工作原理
1、有向无环图
- 根据RDD之间的依赖关系,形成一个DAG(有向无环图)
- DAGScheduler将DAG划分为多个Stage
- 划分依据:是否发生宽依赖(Shuffle)
- 划分规则:从后往前,遇到宽依赖切割为新的Stage
- 每个Stage由一组并行的Task组成
2、划分Stage
- 划分Stage的必要性
- 移动计算,而不是移动数据
- 保证一个Stage内不会发生数据移动
- 分析上图
- A—>B是宽依赖,对应Stage1—>Stage3
- B—>G是窄依赖,对应Stage3内部过程
- C—>D,D—>F,E—>F都是是窄依赖,对应Stage2内部过程
- F—>G是宽依赖,对应Stage2—>Stage3
3、Shuffle过程
- 在分区之间重新分配数据
- 父RDD中同一分区中的数据按照算子要求重新进入子RDD的不同分区中
- 中间结果写入磁盘
- 由子RDD拉取数据,而不是由父RDD推送
- 默认情况下,Shuffle不会改变分区数量
4、Shuffle实践
- 比较下方两段代码
sc.textFile("hdfs:/data/test/input/names.txt")
.map(name=>(name.charAt(0),name))
.groupByKey()
.mapValues(names=>names.toSet.size)
.collect()
sc.textFile("hdfs:/data/test/input/names.txt")
.distinct(numPartitions=6)
.map(name=>(name.charAt(0),1))
.reduceByKey(_+_)
.collect()
- 结论:
- 第一段代码只有一个宽依赖算子groupByKey,所以只有一个Shuffle过程,那么也就只有两个Stage;
- 第二段代码有两个宽依赖算子distinct和reduceByKey,所以产生两个Shuffle过程,会得到三个Stage;
- Shuffle过程极其耗费资源,所以虽然两段代码的最后结果是一致的,但是第一段代码更节省资源,对优化更有利。
5、Spark的Job调度
- 集群(Standalone|Yarn)
- 一个Spark集群可以同时运行多个Spark应用
- 应用
- 我们所编写的完成某些功能的程序
- 一个应用可以并发的运行多个Job
- Job
- Job对应着我们应用中的行动算子,每次执行一个行动算子,都会提交一个Job
- 一个Job由多个Stage组成
- Stage
- 一个宽依赖做一次阶段的划分
- 阶段的个数=宽依赖个数+1
- 一个Stage由多个Task组成
- Task
- 每一个阶段的最后一个RDD的分区数,就是当前阶段的Task个数
三 RDD持久化之cache&persist&checkpoint
1、cache和persist
- cache和persist都是用于将一个RDD进行缓存的,这样在之后使用的过程中就不需要重新计算了,可以大大节省程序运行时间。
- 设置cache或者persist后,都是遇到第一个行动算子开始生效,第一个行动算子结束后完成生效,当遇到第二个行动算子才能看出效果。
- 缓存本身耗费一定时间,所以完成第一个行动算子设置缓存要比不设置缓存耗费时间长。
- cache源码
/**
* Persist this RDD with the default storage level (`MEMORY_ONLY`).
*/
def cache(): this.type = persist()
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
- persist源码
def persist(newLevel: StorageLevel): this.type = {
if (isLocallyCheckpointed) {
// This means the user previously called localCheckpoint(), which should have already
// marked this RDD for persisting. Here we should override the old storage level with
// one that is explicitly requested by the user (after adapting it to use disk).
persist(LocalRDDCheckpointData.transformStorageLevel(newLevel), allowOverride = true)
} else {
persist(newLevel, allowOverride = false)
}
}
- 从上面代码总结区别:cache()调用了persist(),但cache只有一个默认的缓存级别MEMORY_ONLY ,而persist可以根据情况设置其它的缓存级别。也就是说rdd.cache()等价于rdd.persist(StorageLevel.MEMORY_ONLY),而persist还有MEMORY_AND_DISK,DISK_ONLY等缓存级别。
- 缓存应用场景
- 从文件加载数据之后,因为重新获取文件成本较高
- 经过较多的算子变换之后,重新计算成本较高
- 单个非常消耗资源的算子之后
- 使用注意事项
- cache()或persist()后不要再有其他算子
- cache()或persist()遇到Action算子完成后才生效,也就是说它遇到第二个Action算子才会看到效果,如运算速度变快等。
- 缓存实践
package nj.zb.kb09.gaoji
import org.apache.spark.rdd.RDD
import org.apache.spark.storage.StorageLevel
import org.apache.spark.{
SparkConf, SparkContext}
object CacheDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("cache")
val sc = new SparkContext(conf)
val rdd1: RDD[String] = sc.textFile(&#