spark的rdd
RDD(Resilient Distributed Dataset,弹性分布式数据集)是 Apache Spark 最核心的概念和基石。理解了 RDD,就理解了 Spark 的核心工作原理。
下面为你系统、通俗地拆解什么是 RDD。
一、 什么是 RDD?
我们可以把 RDD 拆开来理解:
- Resilient(弹性):
- 容错性:如果某个节点上的数据丢失了,Spark 可以根据它的“血统(Lineage)”自动重新计算出来,不需要像 MapReduce 那样把中间结果写到磁盘上。
- 分区的弹性:分区数量可以动态调整。
- 内存/磁盘切换的弹性:数据优先缓存在内存中,内存不足时会自动溢写到磁盘。
- Distributed(分布式):
- 数据并不是存在一台机器上,而是切分成多个分区(Partitions),分布在集群中的多台机器(Executor)上进行并行计算。
- Dataset(数据集):
- 一个只读的、装有任意类型对象的集合(在 Python 中可以是 list,在 Java/Scala 中可以是 JVM 对象)。
通俗比喻:RDD 就像是一条分布在多台卡车上的流水线货物。每辆卡车装载货物的一部分(分区),大家同时开工(分布式计算)。如果某辆卡车在半路抛锚了,后方可以通过“生产配方”(血统)立刻重新生产出一车一模一样的货物(弹性)。
二、 RDD 的五大核心属性
在 Spark 内部,每个 RDD 都有以下五个核心特征:
- 分区列表(A list of partitions):一个 RDD 被切分成多少块。分区是 Spark 并行计算的最小单位。
- 计算每个分区的函数(A function for computing each split):Spark 知道如何对每个分区的数据进行计算。
- 对其他 RDD 的依赖列表(A list of dependencies on other RDDs):记录这个 RDD 是由哪些父 RDD 转换来的(这就是 血统 Lineage,容错的关键)。
- 键值对 RDD 的分区器(A Partitioner for key-value RDDs,可选):决定数据如何分发(如 HashPartitioner 或 RangePartitioner)。
- 优先位置列表(A list of preferred locations,可选):记录每个分区最好放在哪台机器上计算(数据本地性:移动计算,而不移动数据,以减少网络传输)。
三、 RDD 的核心操作(算子)
RDD 的算子分为两大类:Transformation(转换算子) 和 Action(行动算子)。
1. Transformation(转换算子)
- 特点:惰性求值(Lazy Evaluation)。当你调用转换算子时,Spark 并没有真正开始计算,而只是记录下了这个操作(画了一张计算图/DAG)。
- 作用:将一个 RDD 转换成另一个新的 RDD。
- 常用算子:
map():对每一条数据进行处理。filter():过滤数据。flatMap():扁平化映射(先 map 再压平)。groupByKey()/reduceByKey():按 Key 进行分组或聚合。
2. Action(行动算子)
- 特点:触发计算。当遇到 Action 算子时,Spark 才会真正提交 Job,开始从头到尾执行之前所有的 Transformation。
- 作用:获取计算结果,或者将结果保存到外部系统。
- 常用算子:
collect():将集群中的数据收集回 Driver 端(大数据量时慎用,容易 OOM)。count():计算元素个数。take(n):获取前 n 个元素。saveAsTextFile(path):将数据保存到 HDFS 或本地文件。
四、 依赖关系:宽依赖 vs 窄依赖
这是 Spark 性能调优和理解 Stage 划分的关键:
- 窄依赖(Narrow Dependency):
- 定义:父 RDD 的每个分区最多被子 RDD 的一个分区使用。
- 例子:
map(),filter(),flatMap()。 - 特点:不需要网络传输,数据在本地节点直接计算,效率极高(Pipeline 管道化)。
- 宽依赖(Wide Dependency,又称 Shuffle):
- 定义:父 RDD 的一个分区会被子 RDD 的多个分区使用。
- 例子:
reduceByKey(),groupByKey(),join()。 - 特点:必须经过 Shuffle 阶段,数据需要通过网络在不同节点间传输、重组,非常消耗 I/O 和时间。
Stage(阶段)的划分:Spark 会以宽依赖为边界,把一个复杂的计算任务切分成多个 Stage。
五、 RDD 的代码示例 (PySpark)
经典的 WordCount(单词计数) 例子:
python
from pyspark import SparkContext
# 1. 创建 SparkContext
sc = SparkContext("local", "WordCountApp")
# 2. 读取文件,创建初始 RDD
text_rdd = sc.textFile("hdfs://path/to/hello.txt")
# 3. 转换操作 (Transformation)
# 扁平化:将每行文本按空格切分成单词
words_rdd = text_rdd.flatMap(lambda line: line.split(" "))
# 映射:将每个单词变成 (word, 1) 的键值对
word_pairs = words_rdd.map(lambda word: (word, 1))
# 聚合:按相同的 word,累加它们的次数(此步骤会发生 Shuffle/宽依赖)
word_counts = word_pairs.reduceByKey(lambda a, b: a + b)
# 4. 行动操作 (Action) - 此时才真正开始计算
# 收集结果并打印
result = word_counts.collect()
for word, count in result:
print(f"{word}: {count}")
六、 RDD 的优缺点与演进
虽然 RDD 是 Spark 的基石,但在现代 Spark 开发中,我们很少直接写 RDD,而是写 DataFrame 或 Dataset。
RDD 的缺点:
- 没有 Schema(结构信息):Spark 知道 RDD 里面是 Scala/Python 对象,但不知道对象内部有哪些字段。
- 优化空间小:因为没有结构信息,Spark 的优化器(Catalyst)无法像 SQL 那样帮你自动优化执行计划。
- 序列化开销大:尤其是在 PySpark 中,JVM 和 Python 进程之间的数据传输和序列化非常耗时。
现代 Spark 推荐:
- DataFrame / Dataset:在 RDD 的基础上加上了 Schema(列名和类型)。
- 底层关系:DataFrame 的底层依然是 RDD,但它通过 Spark SQL Catalyst 优化器 和 Tungsten 内存管理器 进行了极大的优化。
总结:什么时候用 RDD?
- 你正在处理非结构化数据(如视频、音频流,或者未排版的纯文本)。
- 你需要非常底层的控制,比如精确控制数据的分区方式、自定义内存序列化方式。
- 你在维护老旧的 Spark 1.x 遗留系统。
- 否则,一律推荐使用 DataFrame / Dataset (Spark SQL) 进行开发。
右滑查看面试常问