今天面试被问到了这个问题,感觉自己回答的并不好。写一下这篇文章记录一下:
Spark基本概念
RDD:弹性分布式数据集 (Resilient Distributed DataSet)。
Spark 是数据不动,代码动的架构!!!!
RDD 的基本概念
RRD全称叫做弹性分布式数据集(Resilient Distributed Dataset),从它的名字中可以拆解出三个概念。
- Resilient :弹性的,包括存储和计算两个方面。RDD 中的数据可以保存在内存中,也可以保存在磁盘中。RDD 具有自动容错的特点,可以根据血缘重建丢失或者计算失败的数据;
- Distributed :RDD 的元素是分布式存储的,并且用于分布式计算;
- Dataset :本质上还是一个存放元素的数据集。
RDD 的基本特性
RDD 有三个基本特性 : 分区,不可变,并行操作
分区
每一个 RDD 包含的数据被存储在系统的不同节点上。逻辑上我们可以将 RDD 理解成一个大的数组,数组中的每个元素就代表一个分区 (Partition) 。
在物理存储中,每个分区指向一个存储在内存或者硬盘中的数据块 (Block) ,其实这个数据块就是每个 task 计算出的数据块,它们可以分布在不同的节点上。
所以,RDD 只是抽象意义的数据集合,分区内部并不会存储具体的数据,只会存储它在该 RDD 中的 index,通过该 RDD 的 ID 和分区的 index 可以唯一确定对应数据块的编号,然后通过底层存储层的接口提取到数据进行处理。
在集群中,各个节点上的数据块会尽可能的存储在内存中,只有当内存没有空间时才会放入硬盘存储,这样可以最大化的减少硬盘 IO 的开销。
不可变
不可变性是指每个 RDD 都是只读的,它所包含的分区信息是不可变的。由于已有的 RDD 是不可变的,所以我们只有对现有的 RDD 进行转化 (Transformation) 操作,才能得到新的 RDD ,一步一步的计算出我们想要的结果。
这样会带来这样的好处:我们在 RDD 的计算过程中,不需要立刻去存储计算出的数据本身,我们只要记录每个 RDD 是经过哪些转化操作得来的,即:依赖关系,这样一方面可以提高计算效率,一方面是错误恢复会更加容易。如果在计算过程中,第 N 步输出的 RDD 的节点发生故障,数据丢失,那么可以根据依赖关系从第 N-1 步去重新计算出该 RDD,这也是 RDD 叫做"弹性"分布式数据集的一个原因。
并行操作
因为 RDD 的分区特性,所以其天然支持并行处理的特性。即不同节点上的数据可以分别被处理,然后生成一个新的 RDD。
======================================================
RDD 的结构
每个 RDD 里都会包括分区信息、依赖关系等等的信息,如下图所示:
Partitions
Partitions 就是上面所说的,代表着 RDD 中数据的逻辑结构,每个 Partion 会映射到某个节点内存或者硬盘的一个数据块。
SparkContext
SparkContext 是所有 Spark 功能的入口,代表了与 Spark 节点的连接,可以用来创建 RDD 对象以及在节点中的广播变量等等。一个线程只有一个 SparkContext。
SparkConf
SparkConf 是一些配置信息。
Partitioner
Partitioner 决定了 RDD 的分区方式,目前两种主流的分区方式:Hash partioner 和 Range partitioner。Hash 就是对数据的 Key 进行散列分布,Rang 是按照 Key 的排序进行的分区。也可以自定义 Partitioner。
Dependencies
Dependencies 也就是依赖关系,记录了该 RDD 的计算过程,也就是说这个 RDD 是通过哪个 RDD 经过怎么样的转化操作得到的。
这里有个概念,根据每个 RDD 的分区计算后生成的新的 RDD 的分区的对应关系,可以分成窄依赖和宽依赖。
窄依赖就是父 RDD 的分区可以一一对应到子 RDD 的分区,宽依赖是说父 RDD 的每个分区可以被多个子 RDD 分区使用。如图:
由于窄依赖的特性,窄依赖允许子 RDD 的每个分区可以被并行处理产生,而且支持在同一个节点上链式执行多条指令,无需等待其它父 RDD 的分区操作。
Spark 区分宽窄依赖的原因主要有两点:
- 窄依赖支持在同一节点上进行链式操作,比如在执行了 map 后,紧接着执行 filter 操作。相反,款依赖需要所有父分区都是可用的。
- 从失败恢复的角度考虑,窄依赖失败恢复更有效,因为只要重新计算丢失的父分区即可,而宽依赖涉及到 RDD 的各级多个父分区。
Checkpoint
检查点机制,在计算过程中有一些比较耗时的 RDD,我们可以将它缓存到硬盘或者 HDFS 中,标记这个 RDD 有被检查点处理过,并且清空它的所有依赖关系。同时,给它新建一个依赖于 CheckpointRDD 的依赖关系,CheckpintRDD 可以用来从 硬盘中读取 RDD 和生成新的分区信息。
这么做之后,当某个 RDD 需要错误恢复时,回溯到该 RDD,发现它被检查点记录过,就可以直接去硬盘读取该 RDD,无需重新计算。
Preferred Location
针对每一个分片,都会选择一个最优的位置来计算,数据不动,代码动。
Storage Level
用来记录 RDD 持久化时存储的级别,常用的有:
- MEMORY_ONLY:只存在缓存中,如果内存不够,则不缓存剩余的部分。这是 RDD 默认的存储级别。
- MEMORY_AND_DISK:缓存在内存中,不够则缓存至内存。
- DISK_ONLY:只存硬盘。
- MEMORY_ONLY_2 和 MEMORY_AND_DISK_2等:与上面的级别和功能相同,只不过每个分区在集群两个节点上建立副本。
Iterator
迭代函数和计算函数是用来表示 RDD 怎样通过父 RDD 计算得到的。
迭代函数首先会判断缓存中是否有想要计算的 RDD,如果有就直接读取,如果没有就查找想要计算的 RDD 是否被检查点处理过。如果有,就直接读取,如果没有,就调用计算函数向上递归,查找父 RDD 进行计算。