3. RDD 基础:五大特性与两种创建方式

好,咱们今天聊聊 RDD。这东西是 Spark 的基石,你后面所有的优化,说白了都是围绕 RDD 在转。我刚开始学 Spark 的时候,觉得 RDD 就是个数据集合,跟 Python 的 list 差不多。后来踩了坑才发现——差远了。

3.1 RDD 的五大特性

RDD 的全称是 Resilient Distributed Dataset,弹性分布式数据集。名字挺长,但核心就两点:能容错、能并行计算。Spark 官方文档里定义了五大特性,我用自己的话给你拆解一下。

一句话总结:RDD 是 Spark 对分布式内存的抽象,它让你像操作本地集合一样操作集群数据。

特性一:分区列表(Partitions)

RDD 的数据会被切分成多个分区,每个分区对应一个计算任务。分区数决定了并行度。我在项目中遇到过一个问题:默认分区数太小,导致集群资源没跑满,任务慢得像蜗牛。后来我手动调了分区数,性能直接翻倍。

特性二:依赖关系(Dependencies)

每个 RDD 都知道自己是从哪个父 RDD 来的。这种血缘关系(Lineage)是容错的关键。如果某个分区丢了,Spark 可以根据依赖关系重新计算,不用从头再来。

特性三:计算函数(Compute)

RDD 本身不存数据,它只存计算逻辑。当你要用数据时,Spark 会调用 compute 函数,从父 RDD 或数据源里拉数据。这叫做「惰性求值」——你定义了一堆操作,但 Spark 不会立刻执行,等到真正要结果时才动手。

特性四:分区器(Partitioner)

对于 Key-Value 类型的 RDD,Spark 会记录分区方式。比如 Hash 分区、Range 分区。分区器决定了数据怎么分布到各个节点上。选对了分区器,能大幅减少 Shuffle 开销。

特性五:优先位置(Preferred Locations)

Spark 会尽量让计算发生在数据所在的节点上,避免网络传输。这就是「数据本地性」。我记得有一次,任务一直卡在「PROCESS_LOCAL」级别,后来发现是数据没缓存好,调整之后快了不少。

RDD 五大特性结构图 RDD 分区列表 依赖关系 计算函数 分区器 优先位置 💡 说明: 五大特性共同保证了 RDD 的「弹性」和「分布式」能力。 分区列表决定并行度,依赖关系保证容错,计算函数实现惰性求值, 分区器优化 Shuffle,优先位置提升数据本地性。

3.2 创建 RDD 的两种方式

创建 RDD 就两条路:要么把已有的集合并行化,要么从外部数据源读。我平时工作中,90% 的场景都是读外部数据源,但并行化集合在测试和调试时特别好用。

方式一:并行化集合(Parallelized Collections)

把内存中的集合(比如 List、Array)转成 RDD。适合小数据量的测试场景。

// Scala 示例
val data = Array(1, 2, 3, 4, 5)
val rdd = sc.parallelize(data, numSlices = 3)  // 指定 3 个分区

// Python 示例
data = [1, 2, 3, 4, 5]
rdd = sc.parallelize(data, numSlices=3)

我的习惯:测试时用 parallelize 造数据,但生产环境千万别这么干。数据量一大,Driver 内存就爆了。

方式二:读取外部数据源

这是生产环境的主力方式。Spark 支持 HDFS、本地文件、Hive、HBase、JDBC、S3 等等。最常用的是 textFile 和 wholeTextFiles。

// 读取 HDFS 上的文本文件
val rdd = sc.textFile("hdfs://namenode:8020/user/data/logs/")

// 读取本地文件
val rdd = sc.textFile("file:///home/user/data/input.txt")

// 读取多个文件(支持通配符)
val rdd = sc.textFile("data/*.csv")

避坑指南:我曾经用 textFile 读一个超大文件,没指定分区数,结果 Spark 默认用 2 个分区,任务跑得极慢。后来我改成 sc.textFile(path, minPartitions=100),速度才上来。

3.3 两种创建方式的对比

对比维度 并行化集合 读取外部数据源
数据来源 Driver 内存中的集合 HDFS、本地文件、数据库等
适用场景 测试、调试、小数据量 生产环境、大数据量
分区控制 手动指定 numSlices 由文件块数决定,可手动调整
内存压力 数据在 Driver 内存,易 OOM 数据分布在集群,压力分散
容错性 依赖 Driver 重算 依赖数据源重读

3.4 分区数到底怎么定?

分区数直接影响并行度。分区太少,资源浪费;分区太多,调度开销大。我一般遵循这个原则:

  • CPU 密集型任务:分区数 = 集群总核心数 × 2~3
  • IO 密集型任务:分区数 = 集群总核心数 × 3~5
  • 从 HDFS 读数据:分区数 = 文件块数(默认 128MB/块)

经验之谈:别迷信公式。我习惯先跑一个小样本,看每个分区的处理时间。如果大部分分区在 100ms~1s 之间,说明分区数合理。如果有的分区跑了几分钟,有的几毫秒——数据倾斜了,得重新分区。

3.5 一个完整的创建示例

咱们写个完整的例子,把两种方式串起来看看。

// 1. 并行化集合创建 RDD
val data = 1 to 100
val rdd1 = sc.parallelize(data, 4)
println(s"分区数: ${rdd1.getNumPartitions}")  // 输出: 4

// 2. 读取外部文件创建 RDD
val rdd2 = sc.textFile("hdfs://cluster/data/sales.csv", 10)
println(s"分区数: ${rdd2.getNumPartitions}")  // 输出: 10(手动指定)

// 3. 查看分区内容(调试用)
rdd1.glom().collect().foreach { arr =>
  println(s"分区数据: ${arr.mkString(", ")}")
}

嗯,这里要注意:glom() 会把每个分区的数据收集成数组,方便你检查数据分布。但生产环境别乱用,它会触发 Shuffle。

3.6 小结

RDD 的五大特性是 Spark 的底层逻辑,理解了它们,你才能明白为什么 Spark 能容错、能并行。两种创建方式各有用途:并行化集合适合快速验证,外部数据源才是生产主力。分区数的设置没有银弹,多试、多观察,找到最适合你场景的值。

我个人习惯在写 Spark 作业前,先花 5 分钟想清楚 RDD 的分区策略。这 5 分钟,往往能省下后面几个小时的调优时间。