第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)来决定分区数。如果你的文件很小,分区数可能比预期的少。这时候可以用repartitioncoalesce手动调整。

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不是数据,而是对数据的描述。这个认知会帮你避免很多坑。