Spark RDD编程模型详解
RDD(Resilient Distributed Dataset)是Spark的核心抽象,代表一个不可变、分区化的元素集合,可以并行操作。它是Spark最早也是最基础的编程模型,提供了强大的容错能力和高效的数据处理能力。
一、RDD核心特性(五大特性)
1. 分区列表(Partitions)
- RDD由多个分区组成,每个分区是数据的子集
- 分区是并行处理的基本单位
- 分区数决定并行度
val rdd = sc.parallelize(1 to 100, 10) // 10个分区
println(rdd.partitions.size) // 输出:10
2. 计算函数(Compute Function)
- 每个分区都有相同的计算函数
- 函数作用于分区内的数据元素
val squared = rdd.map(x => x * x) // 计算函数:平方
3. 依赖关系(Dependencies)
- RDD之间存在依赖关系(血统)
- 窄依赖:父RDD的每个分区最多被子RDD的一个分区使用
- 宽依赖:父RDD的每个分区可能被子RDD的多个分区使用
4. 分区器(Partitioner)
- 决定数据如何分区
- 核心分区器:
HashPartitioner
:哈希分区(默认)RangePartitioner
:范围分区
val partitioned = rdd.partitionBy(new HashPartitioner(5))
5. 数据位置(Preferred Locations)
- 优先将计算任务调度到数据所在节点
- "移动计算而非移动数据"原则
val hadoopRDD = sc.newAPIHadoopFile(...)
hadoopRDD.preferredLocations(hadoopRDD.partitions(0))
二、RDD创建方式
1. 从集合创建
val data = Array(1, 2, 3, 4, 5)
val rdd = sc.parallelize(data, 3) // 3个分区
2. 从外部存储创建
// 文本文件
val textRDD = sc.textFile("hdfs://path/to/file.txt")
// Hadoop输入格式
val hadoopRDD = sc.newAPIHadoopFile(...)
3. 从其他RDD转换
val filtered = textRDD.filter(line => line.contains("error"))
三、RDD操作类型
1. 转换操作(Transformations)
- 惰性操作,只记录转换关系
- 返回新RDD
- 常见转换:
map()
:一对一转换filter()
:过滤元素flatMap()
:一对多转换groupByKey()
:按键分组(宽依赖)reduceByKey()
:按键聚合(优化版groupByKey)
val words = textRDD.flatMap(line => line.split(" "))
val wordCounts = words.map(word => (word, 1)).reduceByKey(_ + _)
2. 行动操作(Actions)
- 触发实际计算
- 返回结果或写入外部存储
- 常见行动:
count()
:返回元素总数collect()
:返回所有元素(数组)take(n)
:返回前n个元素saveAsTextFile(path)
:保存到文件系统
println(wordCounts.count())
wordCounts.saveAsTextFile("hdfs://output/wordcount")
四、RDD依赖关系
1. 窄依赖(Narrow Dependencies)
- 每个父分区最多被一个子分区使用
- 无shuffle操作
- 高效容错(只需重算丢失分区)
- 示例:map, filter, union
2. 宽依赖(Wide Dependencies)
- 父分区可能被多个子分区使用
- 需要shuffle操作
- 容错代价高(需重算所有父分区)
- 示例:groupByKey, reduceByKey, join
五、RDD持久化(Persistence)
1. 持久化级别
级别 | 描述 | 空间使用 | CPU时间 | 内存 | 磁盘 |
---|---|---|---|---|---|
MEMORY_ONLY | 默认级别 | 高 | 低 | 是 | 否 |
MEMORY_ONLY_SER | 序列化存储 | 低 | 高 | 是 | 否 |
MEMORY_AND_DISK | 内存不足溢写到磁盘 | 中等 | 中等 | 是 | 部分 |
DISK_ONLY | 仅磁盘存储 | 低 | 高 | 否 | 是 |
2. 使用方式
val rdd = sc.textFile("large-file.txt").persist(StorageLevel.MEMORY_AND_DISK)
// 或使用快捷方法
rdd.cache() // 等同于 MEMORY_ONLY
六、RDD编程最佳实践
1. 避免使用groupByKey
// 低效:传输所有数据
rdd.groupByKey().mapValues(_.sum)
// 高效:局部聚合
rdd.reduceByKey(_ + _)
2. 合理设置并行度
// 读取时指定分区数
val rdd = sc.textFile("data", 100)
// 重分区
val repartitioned = rdd.repartition(200)
// 减少分区(无shuffle)
val coalesced = rdd.coalesce(50)
3. 广播变量优化
val lookupTable = Map(1 -> "A", 2 -> "B")
val broadcastVar = sc.broadcast(lookupTable)
rdd.map(x => broadcastVar.value.getOrElse(x, "Unknown"))
4. 累加器使用
val errorCounter = sc.longAccumulator("ErrorCounter")
rdd.foreach { x =>
if (x < 0) errorCounter.add(1)
}
println(s"Total errors: ${errorCounter.value}")
七、RDD执行流程
- DAG构建:根据RDD转换操作构建有向无环图
- Stage划分:以宽依赖为边界划分Stage
- 任务调度:将Stage分解为TaskSet
- 任务执行:Executor执行具体任务
- 结果返回:将结果返回Driver或写入存储
八、RDD与DataFrame对比
特性 | RDD | DataFrame |
---|---|---|
数据类型 | 任意Java/Scala对象 | 结构化数据(Row对象) |
优化 | 无 | Catalyst优化器 |
序列化 | Java序列化(慢) | Tungsten二进制格式(快) |
内存使用 | 高 | 低(列式存储) |
API | 函数式 | SQL+DSL |
执行引擎 | 基础执行引擎 | Tungsten优化引擎 |
九、RDD应用场景
- 非结构化数据处理:文本、日志、二进制数据
- 精细控制操作:自定义分区、精确内存管理
- 底层算法实现:实现新的分布式算法
- 遗留系统兼容:迁移旧版Spark代码
十、RDD编程示例:词频统计
// 创建RDD
val textRDD = sc.textFile("hdfs://logs/access.log", 100)
// 转换操作
val words = textRDD.flatMap(line => line.split("\\s+"))
val cleanWords = words.filter(_.length > 3)
val wordPairs = cleanWords.map(word => (word, 1))
val wordCounts = wordPairs.reduceByKey(_ + _)
// 持久化中间结果
wordCounts.cache()
// 行动操作
val top10 = wordCounts.takeOrdered(10)(Ordering.by(-_._2))
top10.foreach(println)
// 保存结果
wordCounts.saveAsTextFile("hdfs://output/wordcount")
十一、RDD性能调优
1. 数据本地化
// 检查本地化级别
spark.ui.taskLocality
// 提高本地化策略
conf.set("spark.locality.wait", "10s")
2. 内存管理
// 调整内存分配比例
conf.set("spark.memory.fraction", "0.7")
conf.set("spark.memory.storageFraction", "0.5")
3. Shuffle优化
// 调整缓冲区大小
conf.set("spark.shuffle.file.buffer", "64k")
// 启用堆外内存
conf.set("spark.shuffle.unsafe.file.output.buffer", "128k")
4. 数据倾斜处理
// 加盐处理
val saltedRDD = rdd.map {
case (key, value) =>
val salt = (key.hashCode % 10).toString
(salt + "_" + key, value)
}
// 两阶段聚合
val partialAgg = saltedRDD.reduceByKey(_ + _)
val finalAgg = partialAgg.map {
case (saltedKey, value) =>
val key = saltedKey.split("_")(1)
(key, value)
}.reduceByKey(_ + _)
总结:RDD编程黄金法则
- 宽依赖最小化:减少Shuffle操作
- 数据复用缓存:合理使用persist
- 任务均衡分配:调整分区大小
- 避免数据倾斜:加盐处理倾斜Key
- 资源充分利用:监控CPU/内存使用
尽管Spark现在推荐使用DataFrame/Dataset API,但理解RDD模型对于:
- 深入理解Spark内部机制
- 处理非结构化数据
- 实现特殊需求算法
仍然至关重要。RDD作为Spark的基石,其核心思想贯穿整个Spark生态。