极客时间已完结课程限时免费阅读

13 | 弹性分布式数据集:Spark大厦的地基(上)

13 | 弹性分布式数据集:Spark大厦的地基(上)-极客时间

13 | 弹性分布式数据集:Spark大厦的地基(上)

讲述:巴莫

时长11:47大小10.78M

你好,我是蔡元楠。
今天我要与你分享的主题是“弹性分布式数据集”。
上一讲中提到,Spark 最基本的数据抽象是弹性分布式数据集(Resilient Distributed Dataset, 下文用 RDD 代指)。
Spark 基于 RDD 定义了很多数据操作,从而使得数据处理的代码十分简洁、高效。所以,要想深入学习 Spark,我们必须首先理解 RDD 的设计思想和特性。

为什么需要新的数据抽象模型?

传统的 MapReduce 框架之所以运行速度缓慢,很重要的原因就是有向无环图的中间计算结果需要写入硬盘这样的稳定存储介质中来防止运行结果丢失。
而每次调用中间计算结果都需要要进行一次硬盘的读取,反复对硬盘进行读写操作以及潜在的数据复制和序列化操作大大提高了计算的延迟。
因此,很多研究人员试图提出一个新的分布式存储方案,不仅保持之前系统的稳定性、错误恢复和可扩展性,还要尽可能地减少硬盘 I/O 操作。
一个可行的设想就是在分布式内存中,存储中间计算的结果,因为对内存的读写操作速度远快于硬盘。而 RDD 就是一个基于分布式内存的数据抽象,它不仅支持基于工作集的应用,同时具有数据流模型的特点。

RDD 的定义

弹性分布式数据集是英文直译的名字,乍一看这个名字相信你会不知所云。如果你去 Google 或者百度搜索它的定义,你会得到如下结果:
RDD 表示已被分区、不可变的,并能够被并行操作的数据集合。
这个定义很不直观,我认识的很多 Spark 初学者在查阅了很多资料后还是对 RDD 一头雾水,很难理解这个抽象的概念。接下来,让我们一起来对这个晦涩的概念抽丝剥茧,见其真义。
在上述定义以及 RDD 的中文译名中,我们不难发现,RDD 有以下基本特性:分区、不可变和并行操作。接下来让我分别讲解这些特点。

分区

顾名思义,分区代表同一个 RDD 包含的数据被存储在系统的不同节点中,这也是它可以被并行处理的前提。
逻辑上,我们可以认为 RDD 是一个大的数组。数组中的每个元素代表一个分区(Partition)。
在物理存储中,每个分区指向一个存放在内存或者硬盘中的数据块(Block),而这些数据块是独立的,它们可以被存放在系统中的不同节点。
所以,RDD 只是抽象意义的数据集合,分区内部并不会存储具体的数据。下图很好地展示了 RDD 的分区逻辑结构:
RDD 中的每个分区存有它在该 RDD 中的 index。通过 RDD 的 ID 和分区的 index 可以唯一确定对应数据块的编号,从而通过底层存储层的接口中提取到数据进行处理。
在集群中,各个节点上的数据块会尽可能地存放在内存中,只有当内存没有空间时才会存入硬盘。这样可以最大化地减少硬盘读写的开销。
虽然 RDD 内部存储的数据是只读的,但是,我们可以去修改(例如通过 repartition 转换操作)并行计算单元的划分结构,也就是分区的数量。

不可变性

不可变性代表每一个 RDD 都是只读的,它所包含的分区信息不可以被改变。既然已有的 RDD 不可以被改变,我们只可以对现有的 RDD 进行转换(Transformation)操作,得到新的 RDD 作为中间计算的结果。从某种程度上讲,RDD 与函数式编程的 Collection 很相似。
lines = sc.textFile("data.txt")
lineLengths = lines.map(lambda s: len(s))
totalLength = lineLengths.reduce(lambda a, b: a + b)
在上述的简单例子中,我们首先读入文本文件 data.txt,创建了第一个 RDD lines,它的每一个元素是一行文本。然后调用 map 函数去映射产生第二个 RDD lineLengths,每个元素代表每一行简单文本的字数。最后调用 reduce 函数去得到第三个 RDD totalLength,它只有一个元素,代表整个文本的总字数。
那么这样会带来什么好处呢?显然,对于代表中间结果的 RDD,我们需要记录它是通过哪个 RDD 进行哪些转换操作得来,即依赖关系,而不用立刻去具体存储计算出的数据本身。
这样做有助于提升 Spark 的计算效率,并且使错误恢复更加容易。
试想,在一个有 N 步的计算模型中,如果记载第 N 步输出 RDD 的节点发生故障,数据丢失,我们可以从第 N-1 步的 RDD 出发,再次计算,而无需重复整个 N 步计算过程。这样的容错特性也是 RDD 为什么是一个“弹性”的数据集的原因之一。后边我们会提到 RDD 如何存储这样的依赖关系。

并行操作

由于单个 RDD 的分区特性,使得它天然支持并行操作,即不同节点上的数据可以被分别处理,然后产生一个新的 RDD。

RDD 的结构

通过上述讲解,我们了解了 RDD 的基本特性——分区、不可变和并行计算。而且,我们还提到每一个 RDD 里都会包括分区信息、所依赖的父 RDD 以及通过怎样的转换操作才能由父 RDD 得来等信息。
实际上 RDD 的结构远比你想象的要复杂,让我们来看一个 RDD 的简易结构示意图:
SparkContext 是所有 Spark 功能的入口,它代表了与 Spark 节点的连接,可以用来创建 RDD 对象以及在节点中的广播变量等。一个线程只有一个 SparkContext。SparkConf 则是一些参数配置信息。感兴趣的同学可以去阅读官方的技术文档,一些相对不重要的概念我就不再赘述了。
Partitions 前文中我已经提到过,它代表 RDD 中数据的逻辑结构,每个 Partition 会映射到某个节点内存或硬盘的一个数据块。
Partitioner 决定了 RDD 的分区方式,目前有两种主流的分区方式:Hash partitioner 和 Range partitioner。Hash,顾名思义就是对数据的 Key 进行散列分区,Range 则是按照 Key 的排序进行均匀分区。此外我们还可以创建自定义的 Partitioner。

依赖关系

Dependencies 是 RDD 中最重要的组件之一。如前文所说,Spark 不需要将每个中间计算结果进行数据复制以防数据丢失,因为每一步产生的 RDD 里都会存储它的依赖关系,即它是通过哪个 RDD 经过哪个转换操作得到的。
细心的读者会问这样一个问题,父 RDD 的分区和子 RDD 的分区之间是否是一对一的对应关系呢?Spark 支持两种依赖关系:窄依赖(Narrow Dependency)和宽依赖(Wide Dependency)。
窄依赖就是父 RDD 的分区可以一一对应到子 RDD 的分区,宽依赖就是父 RDD 的每个分区可以被多个子 RDD 的分区使用。
显然,窄依赖允许子 RDD 的每个分区可以被并行处理产生,而宽依赖则必须等父 RDD 的所有分区都被计算好之后才能开始处理。
如上图所示,一些转换操作如 map、filter 会产生窄依赖关系,而 Join、groupBy 则会生成宽依赖关系。
这很容易理解,因为 map 是将分区里的每一个元素通过计算转化为另一个元素,一个分区里的数据不会跑到两个不同的分区。而 groupBy 则要将拥有所有分区里有相同 Key 的元素放到同一个目标分区,而每一个父分区都可能包含各种 Key 的元素,所以它可能被任意一个子分区所依赖。
Spark 之所以要区分宽依赖和窄依赖是出于以下两点考虑:
窄依赖可以支持在同一个节点上链式执行多条命令,例如在执行了 map 后,紧接着执行 filter。相反,宽依赖需要所有的父分区都是可用的,可能还需要调用类似 MapReduce 之类的操作进行跨节点传递。
从失败恢复的角度考虑,窄依赖的失败恢复更有效,因为它只需要重新计算丢失的父分区即可,而宽依赖牵涉到 RDD 各级的多个父分区。

小结

弹性分布式数据集作为 Spark 的基本数据抽象,相较于 Hadoop/MapReduce 的数据模型而言,各方面都有很大的提升。
首先,它的数据可以尽可能地存在内存中,从而大大提高的数据处理的效率;其次它是分区存储,所以天然支持并行处理;而且它还存储了每一步骤计算结果之间的依赖关系,从而大大提升了数据容错性和错误恢复的正确率,使 Spark 更加可靠。
下一讲,我们会继续深入研究 RDD 的容错机制、任务执行机制,以及 Spark 定义在 RDD 上的各种转换与动作操作。

思考题

窄依赖是指父 RDD 的每一个分区都可以唯一对应子 RDD 中的分区,那么是否意味着子 RDD 中的一个分区只可以对应父 RDD 中的一个分区呢?如果子 RDD 的一个分区需要由父 RDD 中若干个分区计算得来,是否还算窄依赖?
最后,欢迎你把对弹性分布式数据集的疑问写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 5

提建议

上一篇
12 | 我们为什么需要Spark?
下一篇
14 | 弹性分布式数据集:Spark大厦的地基(下)
unpreview
 写留言

精选留言(34)

  • Milittle
    2019-05-15
    对于思考题:需要重点理解原文中这句话: 窄依赖就是父 RDD 的分区可以一一对应到子 RDD 的分区,宽依赖就是父 RDD 的每个分区可以被多个子 RDD 的分区使用。 这句话说明了,窄依赖的父RDD必须有一个对应的子RDD,也就是说父RDD的一个分区只能被子RDD一个分区使用,但是反过来子RDD的一个分区可以使用父RDD的多个分区。那就回复今天的思考题,第一个疑问窄依赖子RDD的分区不一定只对应父RDD的一个分区,只要满足被子RDD分区利用的父RDD分区不被子RDD的其他分区利用就算窄依赖。第二个疑问其实上面已经做了回答,只有当子RDD分区依赖的父RDD分区不被其他子RDD分区依赖,这样的计算就是窄依赖,否则是宽依赖。 最后,总结以下,就是只有父RDD的分区被多个子RDD的分区利用的时候才是宽依赖,其他的情况就是窄依赖。如果有哪里理解不对的地方,请老师指正,谢谢~
    展开

    作者回复: 你说的很准确!点赞

    共 6 条评论
    66
  • windcaller
    2019-07-26
    窄:一子多父,一子一父 宽:一父多子
    共 2 条评论
    32
  • 追梦
    2019-05-16
    老师,下面这句话对吗? 一个stage的所有task都执行完毕之后,会在各个节点本地的磁盘文件中写入计算中间结果,然后Driver就会调度运行下一个stage。下一个stage的task的输入数据就是上一个stage输出的中间结果。 说明:spark的中间计算结果会直接落到磁盘上的???
    展开

    作者回复: 不对,Spark的中间计算结果如果没有特别指定持久化到硬盘,都会存在内存里以方便下一次调用,这也是它运行速度比MapReduce快的主要原因。

    共 4 条评论
    4
  • 吟游雪人
    2019-05-16
    新的RDD不就是上一步的RDD计算出的结果么?为什么说是不保存计算结果?

    作者回复: 不保存计算结果指的是不写入硬盘。像MapReduce中每一步中间计算结果都要写入HDFS。新的RDD是上一步RDD计算的结果,但是并没有立刻进行计算,看过下一讲就明白了,只有碰到action操作才会开始执行,而且如果没有进行特别的缓存或者持久化操作,结果只在内存中,没有被写入硬盘,所以说不保存结果。

    共 3 条评论
    4
  • 2019-05-15
    老师好!能不能请老师讲讲Spark和Flink的对比呢?这二者谁在机器学习乃至深度学习中更有优势呢?

    作者回复: 你好,在第21讲我会带大家比较Spark和Flink。

    3
  • 2019-05-15
    文中提到依赖关系的区分考虑基于两点: 1、性能考虑,窄依赖可以使得每个数据节点并行计算结果,也可以支持链式计算;宽依赖需要父分区的中每个分区中的数据节点计算,只能串行计算。 2、故障恢复考虑,窄依赖可以更快、更准的恢复数据,宽依赖则相对较慢。 那么基于以上考虑,父rdd与子rdd是多对多的关系,则划分到宽依赖;一对一、一对多或多对一的关系都可以划分到窄依赖。 分区方式:hash分区、rang分区,以及自定义分区 疑问:因为分区指向某个节点中的数据块,那么分区的key是分区在RDD中的index还是其引用的数据块中的某个数据字段?我认为是后者。 另外,hash分区和rang分区的应用场景分别是什么呢? RDD具有不可变性,只能通过转换来创建新的RDD,但是不代表RDD中分区指向的节点数据块也不可变,那么如何保证数据块不可变性呢?我认为可能是使用CopyOnWrite技术实现的。 Spark优于MapReduce的地方之一在于:MapReduce的中间计算结果实时落盘,而Spark使用存储依赖关系和延迟存储中间数据来提高性能。
    展开
    2
  • 渡码
    2019-05-21
    思考题: 如果子RDD依赖多个父RDD的多个完整分区,也就是说不存在父RDD一个分区数据分到子RDD多个分区的情况,那就是窄依赖。因为此时父RDD不需要对key重新做分区计算,子RDD也不需要等父RDD所有分区计算完毕。
    共 1 条评论
    1
  • Little Spirits
    2019-05-16
    多个父分区到一个子分区,对于任何一个父分区而言都是pipeline的所以是窄依赖,而一个父分区到多个子分区对父分区而言不是pipeline的所以是宽依赖

    作者回复: 建议看一下高赞的回答,那位同学的理解要更加全面

    1
  • miwucc
    2019-05-15
    子rdd依赖多个父rdd来产出结果。明显是宽依赖。因为需要等待多个父rdd结果完毕才能开始计算。宽依赖还是窄依赖关键看是否要等待更多父rdd准备完毕。
    共 1 条评论
    1
  • 邱从贤※klion26
    2019-05-15
    思考题中窄依赖应该是不关心一个子 rdd 的 partition 对应多少个 父rdd 的 partition,只要partition可以独立计算就行,比如 map 操作,子 rdd 只有 3 个 partition,父 rdd 有6 个 partition,那么父 rdd 肯定有两个 partition 的数据会落到子 rdd 的一个 partition 中,但是落到子 rdd 同一个 partition 的两个 partition 不需要等待就可以进行计算,因此是窄依赖
    1
  • CoderLean
    2019-05-15
    算啊,父子rdd可以通过函数进行转换,对于转换因子是基本不变的,那也应该支持逆转换。,而一个子rdd是无法推导出父rdd的,因为父rdd的数据是由函数转换后拆分给多个子rdd的。另外,之前没学过spark,对于flink内部算子也没有那么深入的理论,学完这个后要可以回去看看flink的算子是怎么实现的
    2
  • Rainbow
    2019-05-15
    uion也是窄依赖
    1
  • 2019-05-15
    我想还可以算做是窄依赖,因为子RDD分区所依赖的对个父RDD分区是互斥的,所以每个子RDD分区所依赖的多个父RDD分区可以被看做一组分区。父RDD的组分区与子分区是一一对应关系,满足窄依赖可以并行计算,而无需所以父分区都计算完毕才可以开始计算的特性。

    作者回复: 👍🏻

    共 2 条评论
    1
  • 李飞
    2021-04-11
    最高赞的理解不对吧!
  • Geek_04168
    2020-10-05
    一子多父属于窄依赖,但我理解这种情况下,多个父依赖分布在不同的节点上,是不支持文中“窄依赖可以支持在同一个节点上链式执行多条命令”
  • 茂杨
    2020-09-12
    讲的真是清楚啊。 RDD是抽象的数据结构,保存在内存当中,但是要记住这个内存可不是连续的,而是分布式的。 这就引出了分区的概念。它是隐藏在RDD下面的真正的存储空间。
  • bwv825
    2020-06-15
    “而宽依赖则必须等父 RDD 的所有分区都被计算好之后才能开始处理”。此处应为“子 RDD 的所有分区”吧。宽依赖表示一个父分区可以被多个子分区使用,但不代表子分区有多个父分区。
  • 北冥有鱼
    2020-05-09
    只要父RDD的分区数据能完整的交给子RDD就算窄依赖。也就是说,一个子RDD可以依赖多个父RDD。宽窄依赖是划分stage的依据,在同一个stage中,计算逻辑是可以合并的(如map,filter),而不用每次计算逻辑都生成数据,再遍历数据。而不同的stage间需要shuffle,shuffle会生成中间数据,下一个stage需要等待上一个stage执行完再执行,逻辑也就不能和上一个stage进行合并了。同时spark要保证任务失败,能更好更快的重算,需要把依赖的父partition保留起来,以及shuffle后的结果数据保留起来… 理解不对的地方,请老师指正:)
    展开
  • Eden2020
    2020-03-26
    主要区分一个计算是否要等待依赖计算完成了,有就是宽依赖,没有就是窄依赖,针对实际的计算需求是很容易区分的
  • 刘润森
    2020-03-02
    spark集群读不了文件,spark本地读取得到,怎么办?