Spark(二)-- Spark 底层逻辑 (六) -- 逻辑执行图生成(一)

目录

6.1 逻辑执行图生成

6.1.1 RDD 的生成

textFile 算子的背后

map 算子的背后

flatMap 算子的背后

6.1.2 RDD 之间的依赖关系

什么是 RDD 之间的依赖关系?

reduceByKey 算子会生成 ShuffledRDD

整体上的流程图

6.1.3 RDD 之间的依赖关系详解

窄依赖

宽依赖

如何分辨宽窄依赖 ?

6.1.4 常见的窄依赖类型

一对一窄依赖

Range 窄依赖

多对一窄依赖

再谈宽窄依赖的区别


6.1 逻辑执行图生成

导读

  1. 如何生成 RDD

  2. 如何控制 RDD 之间的关系

6.1.1 RDD 的生成

重点内容

本章要回答如下三个问题

  • 如何生成 RDD

  • 生成什么 RDD

  • 如何计算 RDD 中的数据

val sc = ...

val textRDD = sc.parallelize(Seq("Hadoop Spark", "Hadoop Flume", "Spark Sqoop"))
val splitRDD = textRDD.flatMap(_.split(" "))
val tupleRDD = splitRDD.map((_, 1))
val reduceRDD = tupleRDD.reduceByKey(_ + _)
val strRDD = reduceRDD.map(item => s"${item._1}, ${item._2}")

println(strRDD.toDebugString)
strRDD.collect.foreach(item => println(item))

明确逻辑计划的边界

在 Action 调用之前, 会生成一系列的 RDD, 这些 RDD 之间的关系, 其实就是整个逻辑计划

例如上述代码, 如果生成逻辑计划的, 会生成如下一些 RDD, 这些 RDD 是相互关联的, 这些 RDD 之间, 其实本质上生成的就是一个 计算链

20190519000019

接下来, 采用迭代渐进式的方式, 一步一步的查看一下整体上的生成过程

textFile 算子的背后

研究 RDD 的功能或者表现的时候, 其实本质上研究的就是 RDD 中的五大属性, 因为 RDD 透过五大属性来提供功能和表现, 所以如果要研究 textFile 这个算子, 应该从五大属性着手, 那么第一步就要看看生成的 RDD 是什么类型的 RDD

  1. textFile 生成的是 HadoopRDD

    20190519202310

    20190519202411

     

    除了上面这一个步骤以外, 后续步骤将不再直接基于代码进行讲解, 因为从代码的角度着手容易迷失逻辑, 这个章节的初心有两个, 一个是希望大家了解 Spark 的内部逻辑和原理, 另外一个是希望大家能够通过本章学习具有代码分析的能力

  2. HadoopRDD 的 Partitions 对应了 HDFS 的 Blocks

    20190519203211

    其实本质上每个 HadoopRDD 的 Partition 都是对应了一个 Hadoop 的 Block, 通过 InputFormat 来确定 Hadoop 中的 Block 的位置和边界, 从而可以供一些算子使用

  3. HadoopRDD 的 compute 函数就是在读取 HDFS 中的 Block

    本质上, compute 还是依然使用 InputFormat 来读取 HDFS 中对应分区的 Block

  4. textFile 这个算子生成的其实是一个 MapPartitionsRDD

    textFile 这个算子的作用是读取 HDFS 上的文件, 但是 HadoopRDD 中存放是一个元组, 其 Key 是行号, 其 Value 是 Hadoop 中定义的 Text 对象, 这一点和 MapReduce 程序中的行为是一致的

    但是并不适合 Spark 的场景, 所以最终会通过一个 map 算子, 将 (LineNum, Text) 转为 String 形式的一行一行的数据, 所以最终 textFile 这个算子生成的 RDD 并不是 HadoopRDD, 而是一个 MapPartitionsRDD

map 算子的背后

20190519101943

  • map 算子生成了 MapPartitionsRDD

    由源码可知, 当 val rdd2 = rdd1.map() 的时候, 其实生成的新 RDD 是 rdd2rdd2 的类型是 MapPartitionsRDD, 每个 RDD 中的五大属性都会有一些不同, 由 map 算子生成的 RDD 中的计算函数, 本质上就是遍历对应分区的数据, 将每一个数据转成另外的形式

  • MapPartitionsRDD 的计算函数是 collection.map( function )

    真正运行的集群中的处理单元是 Task, 每个 Task 对应一个 RDD 的分区, 所以 collection 对应一个 RDD 分区的所有数据, 而这个计算的含义就是将一个 RDD 的分区上所有数据当作一个集合, 通过这个 Scala 集合的 map 算子, 来执行一个转换操作, 其转换操作的函数就是传入 map 算子的 function

  • 传入 map 算子的函数会被清理

    20190519190306

    这个清理主要是处理闭包中的依赖, 使得这个闭包可以被序列化发往不同的集群节点运行

flatMap 算子的背后

20190519190541

flatMap 和 map 算子其实本质上是一样的, 其步骤和生成的 RDD 都是一样, 只是对于传入函数的处理不同, map 是 collect.map( function ) 而 flatMap 是 collect.flatMap( function )

从侧面印证了, 其实 Spark 中的 flatMap 和 Scala 基础中的 flatMap 其实是一样的

textRDD → splitRDD → tupleRDD

由 textRDD 到 splitRDD 再到 tupleRDD 的过程, 其实就是调用 map 和 flatMap 算子生成新的 RDD 的过程, 所以如下图所示, 就是这个阶段所生成的逻辑计划

20190519211533

总结

如何生成 RDD ?

生成 RDD 的常见方式有三种

  • 从本地集合创建

  • 从外部数据集创建

  • 从其它 RDD 衍生

通过外部数据集创建 RDD, 是通过 Hadoop 或者其它外部数据源的 SDK 来进行数据读取, 同时如果外部数据源是有分片的话, RDD 会将分区与其分片进行对照

通过其它 RDD 衍生的话, 其实本质上就是通过不同的算子生成不同的 RDD 的子类对象, 从而控制 compute 函数的行为来实现算子功能

生成哪些 RDD ?

不同的算子生成不同的 RDD, 生成 RDD 的类型取决于算子, 例如 map 和 flatMap 都会生成 RDD 的子类 MapPartitions的对象

如何计算 RDD 中的数据 ?

虽然前面我们提到过 RDD 是偏向计算的, 但是其实 RDD 还只是表示数据, 纵观 RDD 的五大属性中有三个是必须的, 分别如下

  • Partitions List 分区列表

  • Compute function 计算函数

  • Dependencies 依赖

虽然计算函数是和计算有关的, 但是只有调用了这个函数才会进行计算, RDD 显然不会自己调用自己的 Compute 函数, 一定是由外部调用的, 所以 RDD 更多的意义是用于表示数据集以及其来源, 和针对于数据的计算

所以如何计算 RDD 中的数据呢? 一定是通过其它的组件来计算的, 而计算的规则, 由 RDD 中的 Compute 函数来指定, 不同类型的 RDD 子类有不同的 Compute 函数

6.1.2 RDD 之间的依赖关系

导读

  1. 讨论什么是 RDD 之间的依赖关系

  2. 继而讨论 RDD 分区之间的关系

  3. 最后确定 RDD 之间的依赖关系分类

  4. 完善案例的逻辑关系图

什么是 RDD 之间的依赖关系?

20190519211533

  • 什么是关系(依赖关系) ?

    从算子视角上来看, splitRDD 通过 map 算子得到了 tupleRDD, 所以 splitRDD 和 tupleRDD 之间的关系是 map

    但是仅仅这样说, 会不够全面, 从细节上来看, RDD 只是数据和关于数据的计算, 而具体执行这种计算得出结果的是一个神秘的其它组件, 所以, 这两个 RDD 的关系可以表示为 splitRDD 的数据通过 map 操作, 被传入 tupleRDD, 这是它们之间更细化的关系

    但是 RDD 这个概念本身并不是数据容器, 数据真正应该存放的地方是 RDD 的分区, 所以如果把视角放在数据这一层面上的话, 直接讲这两个 RDD 之间有关系是不科学的, 应该从这两个 RDD 的分区之间的关系来讨论它们之间的关系

  • 那这些分区之间是什么关系?

    如果仅仅说 splitRDD 和 tupleRDD 之间的话, 那它们的分区之间就是一对一的关系

    但是 tupleRDD 到 reduceRDD 呢? tupleRDD 通过算子 reduceByKey 生成 reduceRDD, 而这个算子是一个 Shuffle 操作, Shuffle 操作的两个 RDD 的分区之间并不是一对一, reduceByKey 的一个分区对应 tupleRDD 的多个分区

reduceByKey 算子会生成 ShuffledRDD

reduceByKey 是由算子 combineByKey 来实现的, combineByKey 内部会创建 ShuffledRDD 返回, 具体的代码请大家通过 IDEA 来进行查看, 此处不再截图, 而整个 reduceByKey 操作大致如下过程

20190520010402

去掉两个 reducer 端的分区, 只留下一个的话, 如下

20190520010518

所以, 对于 reduceByKey 这个 Shuffle 操作来说, reducer 端的一个分区, 会从多个 mapper 端的分区拿取数据, 是一个多对一的关系

至此为止, 出现了两种分区见的关系了, 一种是一对一, 一种是多对一

整体上的流程图

20190520011115

6.1.3 RDD 之间的依赖关系详解

导读

上个小节通过例子演示了 RDD 的分区间的关系有两种形式

  • 一对一, 一般是直接转换

  • 多对一, 一般是 Shuffle

本小节会说明如下问题:

  1. 如果分区间得关系是一对一或者多对一, 那么这种情况下的 RDD 之间的关系的正式命名是什么呢?

  2. RDD 之间的依赖关系, 具体有几种情况呢?

窄依赖

假如 rddB = rddA.transform(…​), 如果 rddB 中一个分区依赖 rddA 也就是其父 RDD 的少量分区, 这种 RDD 之间的依赖关系称之为窄依赖

换句话说, 子 RDD 的每个分区依赖父 RDD 的少量个数的分区, 这种依赖关系称之为窄依赖

20190520130939

举个栗子

  @Test
  def narrowDependency(): Unit ={
    //需求:求得两个RDD之间的笛卡尔积

    //1.生成RDD
    val conf = new SparkConf().setMaster("local[6]").setAppName("cartesian")
    val sc = new SparkContext(conf)

    //2.计算
    val rdd1 = sc.parallelize(Seq("1","2","3","4","5"))
    val rdd2 = sc.parallelize(Seq("a","b","c"))
    val resultRDD = rdd1.cartesian(rdd2)

    //3.获取结果
    resultRDD.collect().foreach(println(_))

    sc.stop()
  }
  • 上述代码的 cartesian 是求得两个集合的笛卡尔积

  • 上述代码的运行结果是 rddA 中每个元素和 rddB 中的所有元素结合, 最终的结果数量是两个 RDD 数量之和

  • rddC 有两个父 RDD, 分别为 rddA 和 rddB

对于 cartesian 来说, 依赖关系如下

20190520144103

上述图形中清晰展示如下现象

  1. rddC 中的分区数量是两个父 RDD 的分区数量之乘积

  2. rddA 中每个分区对应 rddC 中的两个分区 (因为 rddB 中有两个分区), rddB 中的每个分区对应 rddC 中的三个分区 (因为 rddA 有三个分区)

它们之间是窄依赖, 事实上在 cartesian 中也是 NarrowDependency 这个所有窄依赖的父类的唯一一次直接使用, 为什么呢?

因为所有的分区之间是拷贝关系, 并不是 Shuffle 关系

  • rddC 中的每个分区并不是依赖多个父 RDD 中的多个分区

  • rddC 中每个分区的数量来自一个父 RDD 分区中的所有数据, 是一个 FullDependence, 所以数据可以直接从父 RDD 流动到子 RDD

  • 不存在一个父 RDD 中一部分数据分发过去, 另一部分分发给其它的 RDD

宽依赖

并没有所谓的宽依赖, 宽依赖应该称作为 ShuffleDependency

在 ShuffleDependency 的类声明上如下写到

Represents a dependency on the output of a shuffle stage.

上面非常清楚的说道, 宽依赖就是 Shuffle 中的依赖关系, 换句话说, 只有 Shuffle 产生的地方才是宽依赖

那么宽窄依赖的判断依据就非常简单明确了, 是否有 Shuffle ?

举个 reduceByKey 的例子, rddB = rddA.reduceByKey( (curr, agg) ⇒ curr + agg ) 会产生如下的依赖关系

20190520151040

  • rddB 的每个分区都几乎依赖 rddA 的所有分区

  • 对于 rddA 中的一个分区来说, 其将一部分分发给 rddB 的 p1, 另外一部分分发给 rddB 的 p2, 这不是数据流动, 而是分发

如何分辨宽窄依赖 ?

其实分辨宽窄依赖的本身就是在分辨父子 RDD 之间是否有 Shuffle, 大致有以下的方法

  • 如果是 Shuffle, 两个 RDD 的分区之间不是单纯的数据流动, 而是分发和复制

  • 一般 Shuffle 的子 RDD 的每个分区会依赖父 RDD 的多个分区

但是这样判断其实不准确, 如果想分辨某个算子是否是窄依赖, 或者是否是宽依赖, 则还是要取决于具体的算子, 例如想看 cartesian 生成的是宽依赖还是窄依赖, 可以通过如下步骤

  1. 查看 map 算子生成的 RDD

    20190520155245

  2. 进去 RDD 查看 getDependence 方法

    20190520155314

总结

  • RDD 的逻辑图本质上是对于计算过程的表达, 例如数据从哪来, 经历了哪些步骤的计算

  • 每一个步骤都对应一个 RDD, 因为数据处理的情况不同, RDD 之间的依赖关系又分为窄依赖和宽依赖 *

(笔记:1.先看是否一对一  --> 窄依赖

2.如果不是一对一,多对一 --> 不能确定  --> 如果是数据的复制还是窄依赖,有分发的话就是宽依赖)

6.1.4 常见的窄依赖类型

导读

常见的窄依赖其实也是有分类的, 而且宽窄以来不太容易分辨, 所以通过本章, 帮助同学明确窄依赖的类型

一对一窄依赖

其实 RDD 中默认的是 OneToOneDependency, 后被不同的 RDD 子类指定为其它的依赖类型, 常见的一对一依赖是 map 算子所产生的依赖, 例如 rddB = rddA.map(…​)

20190520160405

  • 每个分区之间一一对应, 所以叫做一对一窄依赖

Range 窄依赖

Range 窄依赖其实也是一对一窄依赖, 但是保留了中间的分隔信息, 可以通过某个分区获取其父分区, 目前只有一个算子生成这种窄依赖, 就是 union 算子, 例如 rddC = rddA.union(rddB)

20190520161043

  • rddC 其实就是 rddA 拼接 rddB 生成的, 所以 rddC 的 p5 和 p6 就是 rddB 的 p1 和 p2

  • 所以需要有方式获取到 rddC 的 p5 其父分区是谁, 于是就需要记录一下边界, 其它部分和一对一窄依赖一样

多对一窄依赖

多对一窄依赖其图形和 Shuffle 依赖非常相似, 所以在遇到的时候, 要注意其 RDD 之间是否有 Shuffle 过程, 比较容易让人困惑, 常见的多对一依赖就是重分区算子 coalesce, 例如 rddB = rddA.coalesce(2, shuffle = false), 但同时也要注意, 如果 shuffle = true 那就是完全不同的情况了

20190520161621

  • 因为没有 Shuffle, 所以这是一个窄依赖

再谈宽窄依赖的区别

宽窄依赖的区别非常重要, 因为涉及了一件非常重要的事情: 如何计算 RDD ?

宽窄以来的核心区别是: 窄依赖的 RDD 可以放在一个 Task 中运行

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值