在 Spark 中,Lineage(血统) 和 Checkpointing(检查点) 是两种核心的容错机制,它们协同工作以确保作业在发生节点故障时能够恢复并继续执行,而无需从头重新运行整个作业。它们解决的问题和实现方式有所不同:
1. Lineage (血统) - 基于逻辑重建的容错
- 核心思想: Lineage 记录了 RDD(弹性分布式数据集)的生成逻辑。它本质上是一个有向无环图(DAG),描述了:
- 初始 RDD 是从哪个数据源(如 HDFS 文件)创建的。
- 一个 RDD 是通过对其父 RDD 应用了哪些转换操作(
map
,filter
,join
,groupByKey
等)得到的。 - RDD 之间的依赖关系(窄依赖 - Narrow Dependency / 宽依赖 - Wide Dependency)。
- 如何用于容错:
- 故障发生: 当一个 Executor 节点失败(或其中某个 Task 失败)时,存储在该节点上的 RDD Partition 数据会丢失。
- 识别丢失分区: Driver 程序(或负责调度的组件)会检测到有分区丢失。
- 利用血统重建: Spark 不是尝试从备份恢复丢失的数据分区(传统方式),而是利用 Lineage DAG:
- Spark 会回溯 Lineage 图,找到丢失分区的源头(可能是初始数据源,也可能是依赖的中间 RDD)。
- Spark 根据 Lineage 记录的转换操作逻辑,重新计算(Recompute)那些丢失的分区及其所有依赖的分区(如果必要)。
- 这个过程只重新计算丢失部分及其依赖链,而不是整个数据集或整个作业。
- 优点:
- 高效恢复: 通常只重新计算丢失的小部分数据,尤其是在窄依赖链中,开销相对较小。
- 零额外存储开销: 不需要为容错而持久化中间数据(除了原始输入数据),节省存储空间。
- 逻辑清晰: 提供了数据生成过程的完整视图。
- 局限性:
- 血统链过长: 如果作业包含非常长的转换序列(特别是很多宽依赖),回溯和重新计算整个链条的代价会非常高,可能接近甚至超过从头开始运行作业的时间。
- 迭代算法: 在迭代算法(如机器学习训练)中,每一轮迭代都依赖前一轮的结果。Lineage 会记录每一轮的转换,导致血统图随着迭代次数线性增长。一旦失败,需要从第一轮迭代重新计算所有步骤,效率极低。
- 宽依赖代价高: 重新计算宽依赖(如
groupByKey
,join
涉及 Shuffle)本身开销就很大,因为需要重新 Shuffle 大量数据。
2. Checkpointing (检查点) - 基于物理存储的容错
- 核心思想: Checkpointing 是将 RDD 的物理数据(实际分区内容)持久化到一个可靠的、容错的分布式存储系统(如 HDFS, S3)的过程。它本质上是为 RDD 在特定时刻创建一个全局一致的快照。
- 作用:
- 截断血统链: 这是 Checkpointing 最主要的作用。当对一个 RDD 执行 Checkpoint 后:
- 该 RDD 的数据被物化(Materialize)到可靠存储。
- Spark 会切断该 RDD 之前的 Lineage 依赖链。这个被 Checkpoint 的 RDD 被视为一个新的“起点”。
- 后续对该 RDD 的操作,其 Lineage 将从这个 Checkpoint 点开始记录,而不是从原始的源头开始。
- 加速故障恢复: 如果后续计算中发生故障,且丢失的分区属于被 Checkpoint 的 RDD 或其下游 RDD,Spark 可以直接从可靠存储中读取 Checkpoint 的数据,而不是根据冗长的原始 Lineage 去重新计算。这大大缩短了恢复时间。
- 支持迭代算法: 在迭代算法中,定期对迭代的中间结果(如每 10 轮迭代后的模型 RDD)做 Checkpoint:
- 将血统链限制在当前迭代轮次内。
- 发生故障时,可以从最近的 Checkpoint 点恢复(如第 10 轮),只需重新计算第 10 轮之后的迭代,而不是从第 1 轮开始。
- 截断血统链: 这是 Checkpointing 最主要的作用。当对一个 RDD 执行 Checkpoint 后:
- 触发方式:
RDD.checkpoint()
: 标记一个 RDD 需要进行 Checkpoint。这是一个惰性操作。- Action 触发: 只有当在该 RDD 或其下游 RDD 上执行了一个 Action(如
count()
,saveAsTextFile()
,collect()
)时,Checkpointing 过程才会真正执行。
- 存储位置: 必须配置为可靠的、容错的存储系统(如 HDFS, S3)。本地文件系统不适合生产环境的 Checkpointing,因为它不具备容错性。
- 与
cache()
/persist()
的区别:cache()
/persist()
将 RDD 数据存储在 Executor 的内存或本地磁盘,目的是加速后续对同一 RDD 的重复访问。这些数据在 Executor 失效时会丢失,且不会切断 Lineage。checkpoint()
将数据存储在可靠的分布式存储,目的是容错和截断 Lineage。它通常涉及额外的 I/O 开销(写入远程存储)。执行checkpoint()
时,Spark 通常会先persist()
该 RDD 到内存/磁盘(如果可能),以避免在 Checkpointing 过程中重复计算。
Lineage 与 Checkpointing 的协作关系
- 默认容错: Spark 默认且主要依赖 Lineage 进行容错。它高效且无需额外存储。
- 识别瓶颈: 当作业存在非常长的 Lineage 链(例如复杂转换流程或大量迭代)或包含昂贵的宽依赖操作时,Lineage 的恢复代价变得不可接受。
- 引入 Checkpointing: 在这些关键点(例如迭代算法的每 N 轮之后,或在关键的 Shuffle 结果 RDD 上)显式调用
checkpoint()
。 - 截断与加速: Checkpointing 将 RDD 物化到可靠存储,并切断其之前的 Lineage。这显著缩短了故障后需要回溯和重新计算的 Lineage 长度。
- 协同恢复: 发生故障时:
- 如果丢失的分区可以从最近的 Checkpoint 数据中恢复,则直接从可靠存储读取。
- 如果丢失的分区在 Checkpoint 点之后,则利用 Checkpoint 点之后的(较短的)Lineage 进行重新计算。
总结:容错流程图
+-----------------------+
| Spark Application |
+-----------+-----------+
|
| (Defines Transformations)
v
+-----------------------+ Records
| Lineage (DAG) | <--- How Data
| (Logical Plan of RDDs) | is Computed
+-----------+------------+
|
| (Executes Actions)
v
+--------------------------+
| Executor (Task Runs) |
| - Processes Data Partit. |
+------------+--------------+
|
+-------------------------------+-----------------+
| Failure (Node/Task Crash) | Success
| |
v v
+---------------------+ +---------------------+
| Lost RDD Partitions | | Job Completes |
+---------------------+ +---------------------+
|
| Driver Detects Loss
v
+-------------------------------------------------+
| Spark Consults Lineage DAG for Recovery Plan: |
| 1. Does a Checkpoint exist for this RDD or an |
| ancestor? |
| * YES: Recover partition from Checkpoint (HDFS/S3)|
| * NO: Recompute partition using Lineage: |
| - Find ultimate source data/ancestor RDD|
| - Re-apply all recorded transformations |
+-------------------------------------------------+
|
v
+---------------------+
| Recovery Tasks |
| (Scheduled on |
| healthy nodes) |
+---------------------+
|
v
+---------------------+
| Job Continues |
+---------------------+
关键点:
- Lineage 是基础: 提供了通过重新计算进行容错的能力,通常高效。
- Checkpointing 是优化: 用于解决 Lineage 在特定场景(长血统、迭代)下恢复效率低下的问题,通过物化数据和截断血统链来实现。
- 协作: Checkpointing 依赖于 Lineage 来知道需要物化什么数据,并在物化后修改 Lineage(切断之前的链)。恢复时优先使用 Checkpoint 数据,否则回退到 Lineage 重新计算。
- 成本权衡: Lineage 几乎无存储成本但有潜在高计算恢复成本;Checkpointing 有明确的存储和 I/O 成本但能显著降低恢复计算成本。开发者需要根据应用特性(迭代次数、血统长度、Shuffle 规模)在合适的地方引入 Checkpointing 以达到最佳的容错效率。