<Zhuuu_ZZ>Spark(四)分布式计算原理

一 宽依赖和窄依赖

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("in/users.csv"
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值