基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

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(有向无环图)

  1. 记录依赖: 每个 RDD 都会包含一个指向其父 RDD 的引用。
  2. 惰性求值(Lazy Evaluation): 只有当触发 Action(如 collect, count, saveAsTextFile)时,Spark 才会根据 Lineage 生成物理执行计划并运行。
  3. 恢复数据: 如果运行过程中某个分区数据损坏,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 提供了容错,但如果链条太长(例如进行了成千上万次迭代计算),会带来两个问题:

  1. 恢复时间过长: 一旦出错,重头计算代价太大。
  2. 栈溢出: 序列化 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 依赖 rdd3
  • rdd3 依赖 rdd2
  • rdd2 依赖 rdd1

容错场景:
如果在计算 rdd3 时某个分区丢失了:

  1. Spark 查看 Lineage,发现 rdd3 来自 rdd2map 操作。
  2. Spark 找到对应的 rdd2 分区。
  3. 如果 rdd2 也在内存中丢失了,继续找 rdd1
  4. 最终从 HDFS 读取该分区对应的数据块,依次执行 flatMap -> map,恢复 rdd3 的那个分区。

总结

  • Lineage 是什么: RDD 的转换历史记录。
  • 核心作用: 容错(重算机制)和 逻辑执行计划构建。
  • 关键分类: 窄依赖(不 Shuffle,快)和 宽依赖(Shuffle,划分 Stage)。
  • 优化: 使用 Cache 缓存常用数据,使用 Checkpoint 切断过长的 Lineage。
00:00
00:00