Spark的Lineage(血统)
Spark 中的 Lineage(血统) 是 Spark 核心概念 RDD(弹性分布式数据集)最重要的特性之一。它是 Spark 实现容错性(Fault Tolerance)和高吞吐量的基础。
简单来说,Lineage 就是 RDD 的“家谱”或“生存指南”。 它记录了一个 RDD 是如何通过一系列的转换(Transformations)从父 RDD 演变而来的。
以下是关于 Spark Lineage 的详细解析:
1. 为什么需要 Lineage?
在传统的分布式计算(如 Hadoop MapReduce)中,为了防止数据丢失,通常会将中间结果写入磁盘并进行多副本复制。这样做虽然安全,但I/O开销巨大。
Spark 选择了不同的路径:
- RDD 是不可变的(Immutable): 一旦创建,就不能修改。如果想改变数据,只能基于旧 RDD 产生一个新的 RDD。
- 记录操作而非数据: Spark 不会时刻备份数据本身,而是记录下产生数据的步骤。
- 容错机制: 如果某个节点挂了,导致 RDD 的某一部分(Partition)丢失,Spark 不需要从磁盘恢复副本,而是根据 Lineage(血统)记录的步骤,重新计算丢失的那一部分数据。
2. Lineage 的工作原理
当你在 Spark 中编写代码时(例如 rdd.map(...).filter(...)),你实际上是在构建一个 DAG(有向无环图)。
- 记录依赖: 每个 RDD 都会包含一个指向其父 RDD 的引用。
- 惰性求值(Lazy Evaluation): 只有当触发 Action(如
collect,count,saveAsTextFile)时,Spark 才会根据 Lineage 生成物理执行计划并运行。 - 恢复数据: 如果运行过程中某个分区数据损坏,Spark 引擎会查看该 RDD 的 Lineage,找到其父 RDD,只重新执行计算该分区的转换操作。
3. 依赖关系:窄依赖 vs 宽依赖
Lineage 中父子 RDD 之间的依赖关系分为两种,这对 Spark 的性能和容错至关重要:
A. 窄依赖 (Narrow Dependency)
- 定义: 父 RDD 的一个分区(Partition)最多被子 RDD 的一个分区使用。
- 特点: “独生子女”或“多对一”。
- 常见操作:
map,filter,union,flatMap。 - 优势:
- Pipeline 执行: 数据可以在同一个节点内存中直接流转,不需要网络传输。
- 容错快: 只需要重新计算丢失的那一个分区,不影响其他分区。
B. 宽依赖 (Wide Dependency / Shuffle Dependency)
- 定义: 父 RDD 的一个分区被子 RDD 的多个分区使用。
- 特点: “超生”或“多对多”。这通常意味着发生了 Shuffle(洗牌)。
- 常见操作:
groupByKey,reduceByKey,join(未协同划分时),repartition。 - 劣势:
- 划分 Stage: 宽依赖是 Spark 划分 Stage(阶段)的边界。
- 开销大: 需要跨节点通过网络传输数据。
- 容错慢: 如果丢失数据,可能需要回溯到上一个 Stage 的所有父分区重新计算(虽然 Spark 会尝试将 Shuffle 的中间结果写盘以减少重算)。
4. Lineage 过长的问题与解决
虽然 Lineage 提供了容错,但如果链条太长(例如进行了成千上万次迭代计算),会带来两个问题:
- 恢复时间过长: 一旦出错,重头计算代价太大。
- 栈溢出: 序列化 Lineage 信息可能导致 Driver 端 StackOverflowError。
解决方案:
- Cache/Persist (缓存): 将计算结果保存在内存或本地磁盘。如果出错,直接读取缓存,不需要回溯 Lineage。但是,如果缓存也丢了,还是需要回溯 Lineage。
- Checkpoint (检查点): 将 RDD 数据写入可靠的分布式文件系统(如 HDFS)。Checkpoint 会切断(Cut)Lineage。一旦 Checkpoint 成功,Spark 就会忘记之前的 Lineage,因为数据已经安全存储了,不再需要通过重算来恢复。
5. 举个例子
假设有以下代码:
plaintext
val rdd1 = sc.textFile("hdfs://data.txt") // 1. 读取
val rdd2 = rdd1.flatMap(_.split(" ")) // 2. 扁平化 (窄依赖)
val rdd3 = rdd2.map((_, 1)) // 3. 映射 (窄依赖)
val rdd4 = rdd3.reduceByKey(_ + _) // 4. 聚合 (宽依赖 - Shuffle)
rdd4.saveAsTextFile("hdfs://output") // 5. 行动
Lineage 视图:
rdd4依赖rdd3rdd3依赖rdd2rdd2依赖rdd1
容错场景:
如果在计算 rdd3 时某个分区丢失了:
- Spark 查看 Lineage,发现
rdd3来自rdd2的map操作。 - Spark 找到对应的
rdd2分区。 - 如果
rdd2也在内存中丢失了,继续找rdd1。 - 最终从 HDFS 读取该分区对应的数据块,依次执行
flatMap->map,恢复rdd3的那个分区。
总结
- Lineage 是什么: RDD 的转换历史记录。
- 核心作用: 容错(重算机制)和 逻辑执行计划构建。
- 关键分类: 窄依赖(不 Shuffle,快)和 宽依赖(Shuffle,划分 Stage)。
- 优化: 使用 Cache 缓存常用数据,使用 Checkpoint 切断过长的 Lineage。