0%

spark核心数据结构之RDD

spark核心数据结构:RDD 深度解析与原理剖析

RDD(Resilient Distributed Dataset,弹性分布式数据集)是 Spark 最核心的抽象,贯穿了 Spark 分布式计算的整个生命周期。它不仅是数据的载体,更是 Spark 实现高效并行计算、容错机制和灵活编程模型的基础。本文基于 Spark 源码注释,详细讲解 RDD 的五大核心属性、特性及底层实现原理,帮助开发者从根本上理解 Spark 数据处理的逻辑。

RDD 核心定义与本质

RDD 是 Spark 对分布式数据的抽象,可理解为一个不可变、可分区、可并行计算且具备容错机制的分布式元素集合。其核心定义在 Spark 源码中通过五大属性描述,这些属性决定了 RDD 的行为和功能:

1
2
3
4
5
6
7
8
/* Internally, each RDD is characterized by five main properties:
*
* - A list of partitions
* - A function for computing each split
* - A list of dependencies on other RDDs
* - Optionally, a Partitioner for key-value RDDs
* - Optionally, a list of preferred locations to compute each split on
*/

RDD 五大核心属性详解

分区列表(Partitions):并行计算的基础

定义

protected def getPartitions: Array[Partition]
RDD 由多个分区(Partition)组成,每个分区是 RDD 数据的一个子集,分布在集群的不同节点上。分区是 Spark 并行计算的最小单位,一个分区对应一个 Task 任务

核心作用
  • 并行计算:多个分区可被分配到不同 Executor 并行处理,充分利用集群算力;
  • 数据本地化:分区数据存储在集群节点上,任务调度时优先在数据所在节点执行,减少网络传输;
  • 规模扩展:通过调整分区数量,适配不同数据量(如 1TB 数据可分为 1000 个分区,每个分区约 1GB)。
示例
  • 读取 HDFS 文件创建的 RDD,分区数默认与 HDFS 块数一致(默认 128MB / 块);
  • 通过 rdd.getNumPartitions 可获取分区数,通过 repartition(num) 可调整分区数。

分区计算函数(Compute):数据处理的逻辑

定义

def compute(split: Partition, context: TaskContext): Iterator[T]
该函数定义了如何计算 RDD 中单个分区的数据,是 RDD 转换和行动操作的核心执行逻辑。

核心作用
  • 惰性计算:compute 函数仅在 Action 操作触发时才会执行,避免不必要的中间计算;
  • 分区独立计算:每个分区的计算逻辑独立,无需依赖其他分区的数据(除非有 Shuffle 依赖);
  • 迭代器优化:返回值为 Iterator(迭代器),支持流式处理,减少内存占用(无需一次性加载整个分区数据)。
示例
  • map 操作的 compute 函数:对分区迭代器中的每个元素应用映射逻辑;
  • filter 操作的 compute 函数:对分区迭代器中的元素进行过滤,保留符合条件的元素。

依赖关系(Dependencies):容错机制的基石

定义

protected def getDependencies: Seq[Dependency[_]] = deps
RDD 之间存在依赖关系(Dependency),描述了当前 RDD 如何从父 RDD 转换而来。依赖关系是 Spark 容错机制的核心(通过血缘关系重算数据)。

依赖类型
依赖类型 特点 适用场景
窄依赖(Narrow Dependency) 子 RDD 的每个分区仅依赖父 RDD 的少数(通常一个)分区 mapfilterunion 等无 Shuffle 操作
宽依赖(Wide Dependency) 子 RDD 的每个分区依赖父 RDD 的多个分区 groupByKeyjoinreduceByKey 等 Shuffle 操作
核心作用
  • 容错性:当某个分区数据丢失时,Spark 可通过依赖关系(血缘 lineage)追溯到父 RDD,仅重算丢失的分区,而非整个数据集;
  • Stage 划分:DAG 调度器根据依赖类型划分 Stage,窄依赖的转换在同一 Stage 执行,宽依赖触发新 Stage(需 Shuffle)。

分区器(Partitioner):数据分布的控制器

定义

@transient val partitioner: Option[Partitioner] = None
分区器是 Key-Value 型 RDD(如 (K, V) 结构)的可选属性,定义了数据如何在分区间分布,决定了 Shuffle 操作后数据的分区规则。

常见分区器
  • HashPartitioner:根据 Key 的哈希值对分区数取模,将相同哈希值的 Key 分配到同一分区(默认分区器);
  • RangePartitioner:根据 Key 的范围划分分区,确保每个分区内的 Key 有序,适合排序场景(如 sortByKey);
  • 自定义 Partitioner:用户可通过继承 Partitioner 类实现自定义分区逻辑(如按业务标签分区)。
核心作用
  • 控制数据分布:避免数据倾斜(如通过自定义分区器分散热点 Key);
  • 优化 Shuffle 性能:合理的分区策略可减少跨节点数据传输,提高 Shuffle 效率。

首选位置(Preferred Locations):数据本地化的优化

定义

protected def getPreferredLocations(split: Partition): Seq[String] = Nil
该函数返回计算某个分区的首选节点列表(主机名或 IP),用于任务调度时的本地化优化。

核心作用
  • 减少网络传输:任务调度器优先将分区计算任务分配到数据所在节点(如 HDFS 块的存储节点),实现 “移动计算而非移动数据”;
  • 提升计算效率:本地化级别越高(如数据在当前 Executor 内存),计算速度越快。
示例
  • 从 HDFS 读取的 RDD,首选位置是该 HDFS 块的存储节点(通过 getPreferredLocations 返回 datanode 主机名);
  • 缓存到内存的 RDD,首选位置是缓存该分区的 Executor 所在节点。

RDD 的核心特性解析

RDD 的五大属性共同赋予了它三大核心特性,这些特性是 Spark 高效、可靠的基础。

弹性(Resilient):容错与动态适应能力

  • 容错性:通过血缘关系(lineage)实现数据容错,无需复制数据副本(区别于 HDFS 的多副本容错),节省存储空间;
  • 动态伸缩:可根据集群资源动态调整分区数量,适应数据量变化;
  • 内存与磁盘自适应:当内存不足时,自动将数据写入磁盘,避免 OOM。

分布式(Distributed):并行计算的基础

  • 数据分布存储:RDD 数据分散在集群多个节点的分区中,突破单机存储限制;
  • 并行计算:每个分区的计算可在不同节点并行执行,充分利用集群算力。

不可变性(Immutable):简化编程与优化

  • 只读特性:RDD 一旦创建无法修改,只能通过转换操作(如 mapfilter)生成新 RDD;
  • 无副作用:不可变性确保了并发操作的安全性,简化了分布式编程模型;
  • 优化空间:Spark 可基于不可变性对 DAG 进行优化(如合并相邻转换)。

RDD 操作类型:Transformations 与 Actions

RDD 支持两种类型的操作,共同构成 Spark 的编程模型:

转换操作(Transformations):生成新 RDD

转换操作是惰性执行的(Lazy Evaluation),仅记录 RDD 之间的依赖关系,不立即计算结果。常见转换操作:

  • map(f: T => U):对每个元素应用函数 f,生成新 RDD;
  • filter(f: T => Boolean):保留满足条件 f 的元素;
  • flatMap(f: T => TraversableOnce[U]):对每个元素应用函数 f 并压平结果;
  • groupByKey()/reduceByKey(f):按 Key 分组或聚合(宽依赖,触发 Shuffle)。

示例

1
2
3
val lines = sc.textFile("hdfs:///data.txt")  // 创建 RDD  
val words = lines.flatMap(_.split(" ")) // 转换操作(惰性)
val wordCounts = words.map((_, 1)).reduceByKey(_ + _) // 转换操作(惰性)

行动操作(Actions):触发计算并返回结果

行动操作会触发所有之前的转换操作执行,并返回结果(或写入外部存储)。常见行动操作:

  • collect():将 RDD 所有元素拉取到 Driver 节点(小数据用);
  • count():返回 RDD 元素数量;
  • take(n):返回前 n 个元素;
  • saveAsTextFile(path):将结果写入外部存储(如 HDFS)。

示例

1
2
wordCounts.collect().foreach(println)  // 行动操作(触发计算)  
wordCounts.saveAsTextFile("hdfs:///output") // 行动操作(触发计算并写入)

RDD 底层实现与数据处理流程

RDD 的五大属性和操作类型共同构成了 Spark 的数据处理流程,以 WordCount 为例:

  1. 创建 RDDsc.textFile("hdfs:///data.txt") 从 HDFS 读取数据,生成初始 RDD,分区数与 HDFS 块数一致,首选位置为 HDFS 块所在节点。
  2. 转换操作
    • flatMap:通过 compute 函数对每个分区的文本行进行拆分,生成单词 RDD(窄依赖,与父 RDD 分区一一对应);
    • map((_, 1)):对每个单词映射为 (word, 1)(窄依赖);
    • reduceByKey:通过 HashPartitioner 重新分区,触发 Shuffle(宽依赖),每个分区内相同 Key 进行聚合。
  3. 行动操作触发计算collect() 触发所有转换操作,Driver 根据依赖关系划分 Stage,调度 Task 到 Executor 执行,最终收集结果。

RDD 与其他数据结构的对比

数据结构 特点 适用场景
RDD 不可变、分区、基于内存、API 灵活(函数式编程) 批处理、迭代计算(机器学习)、复杂转换逻辑
DataFrame 结构化数据、列存储、优化执行计划(Catalyst) SQL 分析、结构化数据处理(如 CSV、Parquet)
DataSet 强类型、结合 RDD 与 DataFrame 优势 类型安全的结构化数据处理

欢迎关注我的其它发布渠道

表情 | 预览
Powered By Valine
v1.3.10