第4章:RDD基础:RDD概念、RDD五大特性、RDD创建方式、RDD分区
好,咱们今天聊聊RDD。说实话,RDD是Spark里最核心的概念,没有之一。你如果搞懂了RDD,Spark的骨架你就抓住了。我当年刚接触Spark时,第一反应是:这玩意儿不就是个集合吗?后来踩了不少坑才明白——它远比集合复杂,也远比集合强大。
4.1 RDD到底是什么?
RDD的全称是Resilient Distributed Dataset,弹性分布式数据集。名字挺长,咱们拆开看:
- 弹性(Resilient):能自动从故障中恢复。节点挂了?没关系,血统(Lineage)能帮你重新算出来。
- 分布式(Distributed):数据分散在集群的多个节点上,不是单机玩的。
- 数据集(Dataset):本质上就是一堆数据的抽象,你可以像操作本地集合一样操作它。
说白了,RDD就是一个只读的、分区的、可并行计算的数据集合。注意“只读”这两个字——RDD一旦创建就不能修改,只能通过转换生成新的RDD。这跟函数式编程的思路一脉相承。
核心理解:RDD不是真实存储的数据,而是对数据的描述。它告诉Spark“数据在哪儿、怎么算”,而不是“数据是什么”。
4.2 RDD的五大特性
这五个特性是RDD的身份证。我建议你背下来,面试常考,实际开发中也经常用到。
| 特性 | 说明 | 我的理解 |
|---|---|---|
| 1. 分区列表 | RDD由多个分区(Partition)组成,每个分区是数据的一个子集 | 分区决定了并行度,分区数越多,并行度越高 |
| 2. 计算函数 | 每个分区都有一个计算函数,用于对分区内的数据进行处理 | 说白了就是告诉Spark“怎么算” |
| 3. 依赖关系 | RDD之间通过依赖关系形成DAG(有向无环图) | 这是容错和优化的基础,我经常用它来排查数据倾斜 |
| 4. 分区器(可选) | 对于Key-Value类型的RDD,可以指定分区器(如HashPartitioner) | 分区器决定了数据如何分布到不同节点 |
| 5. 优先位置列表(可选) | Spark会优先在数据所在的节点上执行计算,减少网络传输 | 这就是“数据本地性”的体现,能极大提升性能 |
嗯,这里要注意:特性4和5是可选的。不是所有RDD都有分区器和优先位置,但前三个是必须的。
避坑指南:我曾经在项目中遇到过一个性能问题,任务跑得特别慢。排查了半天,发现是分区数设置得太少,导致数据倾斜。后来我养成了一个习惯——创建RDD时,先看一眼分区数,再决定要不要重新分区。
4.3 RDD的创建方式
创建RDD主要有两种方式:从集合创建和从文件创建。咱们一个一个说。
4.3.1 从集合创建
这种方式适合小数据量的测试和原型开发。你想想看,在本地调试时,从文件读数据多麻烦?直接用集合多方便。
// 从List创建
val data = List(1, 2, 3, 4, 5)
val rdd = sc.parallelize(data)
// 指定分区数
val rddWithPartitions = sc.parallelize(data, 4)
// 或者用makeRDD(效果一样)
val rdd2 = sc.makeRDD(data)
我个人习惯用parallelize,因为名字更直观——并行化集合。注意第二个参数可以指定分区数,默认是CPU核心数。
4.3.2 从文件创建
这才是生产环境中最常用的方式。Spark支持从本地文件系统、HDFS、S3等各种存储系统读取数据。
// 从本地文件读取
val rdd = sc.textFile("file:///home/user/data.txt")
// 从HDFS读取
val rdd = sc.textFile("hdfs://namenode:9000/user/data/")
// 支持通配符
val rdd = sc.textFile("/user/data/*.log")
// 指定最小分区数
val rdd = sc.textFile("/user/data/", 10)
这里有个坑:textFile的第二个参数是最小分区数,不是精确分区数。Spark会根据文件大小和块大小来调整。我曾经遇到过设置10个分区,结果只生成了3个的情况——因为文件太小了。
注意:从HDFS读取文件时,Spark默认使用HDFS的块大小(默认128MB)来决定分区数。如果你的文件很小,分区数可能比预期的少。这时候可以用repartition或coalesce手动调整。
4.4 RDD分区详解
分区是RDD并行计算的基础。你想想看,如果没有分区,数据都在一个节点上,那跟单机有什么区别?
4.4.1 分区数怎么定?
分区数没有绝对的标准,但有几个经验法则:
- 分区数 = CPU核心数的2~3倍:这样能充分利用CPU,避免空闲
- 每个分区大小建议在100MB~1GB之间:太小了任务调度开销大,太大了容易OOM
- 避免数据倾斜:如果某个分区数据量特别大,会导致其他分区空闲等待
我记得有一次做日志分析,默认分区数导致某个分区处理了80%的数据,其他分区闲得发慌。后来我用repartition重新分区,任务时间从40分钟降到了12分钟。
4.4.2 查看分区信息
// 查看分区数
println(rdd.getNumPartitions)
// 查看每个分区的数据(调试用)
rdd.glom().collect().foreach(println)
glom这个方法挺有意思,它把每个分区的数据变成一个数组,方便你查看数据分布。我调试数据倾斜时经常用它。
4.4.3 重新分区
// 增加分区数(会触发shuffle)
val rdd2 = rdd.repartition(10)
// 减少分区数(尽量不触发shuffle)
val rdd3 = rdd.coalesce(4)
这里有个重要区别:repartition会触发shuffle,而coalesce尽量不触发。所以减少分区时用coalesce更高效。但要注意,coalesce默认只能减少分区,不能增加。
核心总结:RDD是Spark的基石。理解RDD的五大特性,掌握创建方式和分区策略,你就能写出高效、稳定的Spark程序。下一章我们会深入RDD的算子操作,那才是真正发挥RDD威力的地方。
好了,这一章就到这里。记住:RDD不是数据,而是对数据的描述。这个认知会帮你避免很多坑。