spark rdd详解
RDD(Resilient Distributed Dataset,弹性分布式数据集)是 Apache Spark 的核心概念和最基础的数据抽象。理解 RDD 是掌握 Spark 编程和底层原理的关键。
以下是对 Spark RDD 的超详细解析,涵盖其定义、五大特性、算子分类、依赖关系、容错机制以及生命周期。
一、 什么是 RDD?
- R - Resilient(弹性):表示数据可以自动容错。如果某个节点上的数据丢失或损坏,Spark 可以根据“血统(Lineage)”自动重建丢失的分区,无需人工干预。
- D - Distributed(分布式):数据被切分成多个分区(Partitions),分布在集群的不同节点上,进行并行计算。
- D - Dataset(数据集):它是一个只读的、装载着同类元素的集合。
通俗理解:RDD 就像一个“虚拟的数组”,你感觉它是一个整体,但实际上它被切成了很多碎片(分区),存放在不同的服务器上。你对这个“数组”进行的操作,会被 Spark 自动分发到各个服务器上并行执行。
二、 RDD 的五大核心属性(底层结构)
在 Spark 源码中,任何一个 RDD 都具备以下五个核心特征:
- A list of partitions(分区列表)
- 一个 RDD 被分成多个分区,分区是 Spark 进行并行计算的最小单位。
- A function for computing each split(计算函数)
- Spark 会将计算函数作用在每一个分区上(移动计算而非移动数据)。
- A list of dependencies on other RDDs(依赖关系列表)
- 记录当前 RDD 是由哪些父 RDD 转换而来的(即 Lineage 血统),这是容错的基础。
- A Partitioner for Key-Value RDDs(分区器,可选)
- 如果是键值对(Key-Value)类型的 RDD,会有一个分区器(如 HashPartitioner 或 RangePartitioner),决定数据如何分发到不同的分区。
- A list of preferred locations(优先位置列表,可选)
- 记录每个分区期望存储的位置(比如 HDFS 块所在的本地节点)。Spark 在调度任务时,会尽量将计算任务分配到数据所在的物理节点上(实现数据本地化)。
三、 RDD 的两大算子(Operations)
RDD 的操作被称为“算子”,分为两类:转换算子(Transformation)和行动算子(Action)。
1. 转换算子(Transformation)
- 特点:惰性求值(Lazy Evaluation)。转换算子只记录 RDD 转换的轨迹(构建 DAG 图),并不立即触发真正的计算。
- 返回值:返回一个新的 RDD。
- 常见算子:
map(func):对每个元素应用函数。filter(func):过滤元素。flatMap(func):扁平化映射(1对多)。groupByKey():按 Key 分组。reduceByKey(func):按 Key 分组并聚合(推荐,在 Map 端有 Combine 预聚合,效率高)。join(otherRDD):连接两个 RDD。
2. 行动算子(Action)
- 特点:立即执行。一旦调用行动算子,Spark 就会提交作业(Job),开始真正的分布式计算。
- 返回值:不再返回 RDD,而是返回具体的结果(如 Array, List, Long)或者将数据写入外部存储(如 HDFS)。
- 常见算子:
collect():将所有数据收集到 Driver 端(数据量大时易 OOM,需谨慎使用)。count():返回元素个数。first()/take(n):获取第一个或前 n 个元素。reduce(func):对数据集进行聚合。saveAsTextFile(path):保存到本地或 HDFS。
四、 RDD 的依赖关系与 Stage 划分
RDD 之间的依赖关系决定了 Spark 的调度模式和数据流向。依赖分为两种:
1. 窄依赖(Narrow Dependency)
- 定义:每一个父 RDD 的分区最多被子 RDD 的一个分区使用(1对1 或 多对1)。
- 特点:不需要进行 Shuffle(数据混洗),可以直接在内存中进行流水线(Pipeline)计算。
- 典型算子:
map,filter,flatMap,union。
2. 宽依赖(Wide Dependency / Shuffle Dependency)
- 定义:同一个父 RDD 的分区会被多个子 RDD 的分区所使用(1对多 或 多对多)。
- 特点:必须进行 Shuffle。必须跨节点传输数据,会有大量的磁盘 IO 和网络传输,是性能瓶颈。
- 典型算子:
groupByKey,reduceByKey,join(未分区的情况下)。
窄依赖 (Narrow):
[Parent Partition 1] -----> [Child Partition 1]
[Parent Partition 2] -----> [Child Partition 2]
宽依赖 (Wide - Shuffle):
[Parent Partition 1] ---\--> [Child Partition 1]
X
[Parent Partition 2] ---/--> [Child Partition 2]
3. Stage(阶段)的划分
Spark 提交 Job 后,DAG 调度器(DAGScheduler)会从后往前划分 Stage。
- 划分依据:遇到宽依赖(Shuffle)就切分出一个新的 Stage。
- 窄依赖会被合并到同一个 Stage 中,以 pipeline 的方式并行执行。
- Stage 之间有先后顺序,前一个 Stage 执行完并把 Shuffle 数据写到磁盘后,后一个 Stage 才能开始。
五、 RDD 的容错与持久化机制
1. 容错机制:Lineage(血统)
RDD 内部保存了它是由哪些父 RDD 计算得来的。如果某个分区数据丢失,Spark 不需要从头读取所有数据,而是只需要根据 Lineage 重新计算该分区即可。
2. 持久化(缓存)
如果一个 RDD 被多次重复使用,每次使用都会重新计算,效率极低。可以通过持久化将 RDD 的计算结果保存起来。
cache():默认将 RDD 缓存在内存中(等价于persist(StorageLevel.MEMORY_ONLY))。persist(level):可以指定存储级别(如内存、磁盘、多副本等):MEMORY_ONLY(仅内存)MEMORY_AND_DISK(内存不够写磁盘)MEMORY_ONLY_SER(序列化后存内存,省空间但费 CPU)
- 注意:持久化是惰性的,只有在第一次触发 Action 算子时才会真正写入缓存。
3. 检查点(Checkpoint)
当 Lineage(血统)过长时,容错成本会很高。Checkpoint 可以将 RDD 数据写入可靠的外部存储(如 HDFS),并斩断(截断)该 RDD 之前的血统关系。
- 与 Cache 的区别:
- Cache 把数据缓存在内存/本地磁盘,Executor 挂了或者作业结束了,缓存就消失了。Lineage 依然保留。
- Checkpoint 把数据写到 HDFS,物理上永久保存。Lineage 被切断。
六、 RDD 工作流程示例(WordCount)
经典的 WordCount 代码如何体现 RDD 的生命周期:
# 1. 创建 RDD (从外部源读取)
lines_rdd = sc.textFile("hdfs://...")
# 2. 转换算子 (窄依赖)
words_rdd = lines_rdd.flatMap(lambda line: line.split(" "))
# 3. 转换算子 (窄依赖)
pairs_rdd = words_rdd.map(lambda word: (word, 1))
# 4. 转换算子 (宽依赖 - 触发 Shuffle)
word_counts_rdd = pairs_rdd.reduceByKey(lambda a, b: a + b)
# 5. 行动算子 (触发 Job 执行,保存数据)
word_counts_rdd.saveAsTextFile("hdfs://output")
执行过程:
- Spark 解析代码,构建出 DAG 图(
lines->words->pairs->word_counts)。 - 遇到
reduceByKey(宽依赖),将 Job 划分为两个 Stage:- Stage 1:读取 HDFS ->
flatMap->map-> 写出 Shuffle Map File。 - Stage 2:读取 Shuffle Map File ->
reduceByKey-> 保存到 HDFS。
- Stage 1:读取 HDFS ->
- 任务分配到 Executor 上并行执行。
七、 RDD 的局限性与现代 Spark
尽管 RDD 非常强大,但在现代 Spark 开发中,我们更多地使用 DataFrame 和 Dataset。
RDD 的局限性:
- 缺乏结构信息:Spark 只知道 RDD 里是一个个 Java/Python 对象,不知道对象的内部结构,无法针对字段进行自动优化。
- 序列化开销大:在垃圾回收(GC)和网络传输时,Java 对象的序列化和反序列化开销极大。
进化:
- DataFrame = RDD + Schema(结构信息)。Spark 拥有了类似 SQL 的执行计划优化器(Catalyst),性能大幅提升。
- Dataset = DataFrame 的强类型版本(支持 Scala/Java 编译期类型检查)。
总结
- RDD 是基石:虽然 DataFrame/Dataset 更常用,但它们的底层依然是 RDD。
- 核心掌握:理解分区、惰性求值、宽/窄依赖、Shuffle、持久化机制,是进行 Spark 调优(解决倾斜、内存溢出)的必备底层知识。