前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住给大家分享一下。点击跳转到网站:https://www.captainai.net/dongkelun
前言
本文是 Flink Checkpoint 系列的第一篇,从概念层面介绍 Checkpoint 是什么、为什么需要、核心算法原理以及整体执行流程。后续文章会逐层深入到源码实现。
前置知识:
- Flink SQL Checkpoint 学习总结 — Checkpoint 参数详解与重启验证
版本
Flink 1.15.3
什么是 Checkpoint
Checkpoint 是 Flink 提供的一种分布式快照机制,周期性地保存算子状态和数据源消费位置到持久化存储。当作业失败时,Flink 可以从最近一次成功的 Checkpoint 恢复,实现 Exactly-Once 语义。
核心特性:
- 自动触发:按配置的时间间隔周期性执行,无需人工干预
- 增量保存:RocksDB StateBackend 支持增量 Checkpoint,只保存上次 Checkpoint 以来的变更
- 异步执行:状态快照在后台异步写入,不阻塞数据处理主流程
- Exactly-Once:结合 Barrier 机制和两阶段提交,保证故障恢复后数据不丢不重
为什么需要 Checkpoint
流处理作业是 7x24 小时运行的,运行过程中可能发生各种故障:TaskManager 宕机、网络抖动、JobManager 重启等。如果没有状态持久化机制,故障后所有中间状态都会丢失,需要从数据源重新消费,代价巨大。
Checkpoint 的作用就是将分布式作业的整体状态在某个时刻”凝固”下来,以便故障时回到那个时刻继续处理。这和数据库的 checkpoint(检查点)是同一个思想。
核心算法:Chandy-Lamport 分布式快照
Flink 的 Checkpoint 基于 Chandy-Lamport 分布式快照算法。
什么是快照
快照(Snapshot)就是把算子的当前状态保存下来。对于 Source 算子来说,快照就是当前消费到的 offset(如 Kafka 各分区的位点);对于窗口算子来说,快照就是当前窗口中的数据;对于 Sink 算子来说,快照就是当前已写入但未提交的事务 ID。把所有这些算子在同一时刻的状态一起保存,就叫”分布式快照”。
当作业故障时,从快照恢复状态,就可以接着快照时的位置继续处理,数据不丢不重。
Barrier 机制
Barrier 的作用很简单:标记快照边界。Barrier(n) 之前的数据属于 Checkpoint n,之后的数据属于 Checkpoint n+1。所有算子以 Barrier 为基准,在同一个逻辑时刻完成状态快照,保证全局一致性。
Barrier 是插入数据流中的特殊标记事件,由 Source Task 在收到 CheckpointCoordinator(运行在 JobManager 上的核心协调者,负责触发、收集确认和完成 Checkpoint)的触发请求后自行生成并插入到输出数据流中。
Barrier 在拓扑中逐级传播:
1 | CheckpointBarrier (n) — 由 Source 生成并插入 |
Barrier 的传播和算子的状态快照是异步的:Source 生成 Barrier 后立即插入输出流,不等自己的异步快照写完;中间算子收到 Barrier 后转发给下游,也不等自己的快照写完。因此 Barrier 能在 ms 级别贯穿整个拓扑,而大状态快照在后台异步执行。整体流程是”Barrier 先走、快照后写“。
Checkpoint 模式
Flink 支持两种 Checkpoint 模式,通过 execution.checkpointing.mode 配置(默认 EXACTLY_ONCE):
| 模式 | 配置值 | 行为 |
|---|---|---|
| 精确一次 | EXACTLY_ONCE(默认) |
Barrier 到齐后快照,保证每条数据只处理一次。阻塞策略详见下方三种模式 |
| 至少一次 | AT_LEAST_ONCE |
Barrier 到齐后快照,但期间不阻塞通道,恢复时可能有重复。不使用对齐/非对齐策略 |
EXACTLY_ONCE 下根据对齐策略又分为三种模式,通过额外参数配置:
| 策略 | 配置方式 | 行为 |
|---|---|---|
| 对齐 | 默认(execution.checkpointing.unaligned=false) |
多输入时 Barrier 阻塞通道,等所有通道到齐后快照 |
| 非对齐 | execution.checkpointing.unaligned=true |
Barrier 到齐即快照,不等,含 in-flight 数据 |
| 超时自动切换 | unaligned=true + aligned-checkpoint-timeout > 0 |
先尝试对齐,超时转非对齐 |
源码中 CheckpointOptions.forConfig() 的决策逻辑:
1 | // CheckpointOptions.forConfig() 中的决策链路 |
即必须开启 unaligned=true + 设置 aligned-checkpoint-timeout,Flink 才会进入”先对齐,超时转非对齐”的模式。如果只设 aligned-checkpoint-timeout 但没设 unaligned=true,isUnalignedEnabled=false,直接走”对齐”分支,超时参数不生效。
AT_LEAST_ONCE不走以上策略,没有对齐和非对齐的概念。
对齐(Alignment)
当一个算子有多个输入通道时(如 union、join、ConnectedStreams),问题来了:不同通道的 Barrier(n) 到达时间可能不同。如果其中一个通道的 Barrier(n) 先到了,另一个通道还在消费 Barrier(n) 之前的数据,这时如果直接快照,快照中就会包含”跨代”数据——一个通道的 Checkpoint n 数据和另一个通道的 Checkpoint n-1 数据混在一起,恢复时状态不一致。
对齐就是为了解决这个问题:第一个通道的 Barrier(n) 到达后,该通道暂停消费(blockChannel),等所有通道的 Barrier(n) 到齐后才执行快照。这样可以保证快照中所有算子的数据都在同一个 Barrier 边界上。
1 | 输入1: ...数据...Barrier(n) → 收到 Barrier(n),阻塞通道,等其余通道 |
对齐的代价:阻塞的通道会积压数据,产生反压。如果下游处理慢、反压本来就存在,对齐会进一步加剧反压,甚至导致 Checkpoint 超时失败。
非对齐(Unaligned Checkpoint)
非对齐模式下,Barrier 到达一个通道后不等其他通道,立即触发快照。快照中会把 Barrier 之后还未处理的 in-flight Buffer 也一并保存下来,恢复时重放这些数据。
1 | 输入1: ...数据...Barrier(n)...数据... → 收到 Barrier(n),立即快照(含之后的数据) |
非对齐的代价:快照体积更大(包含 in-flight 数据),恢复时重放更多数据。
对齐 vs 非对齐
Flink 两种都支持,通过配置 execution.checkpointing.unaligned: true 开启非对齐模式(默认关闭)。
| 对齐 | 非对齐 | |
|---|---|---|
| 触发时机 | 所有通道 Barrier 到齐后 | Barrier 到达即触发 |
| 转发时机 | 对齐完成后(触发快照时) | Barrier 到达立即转发 |
| 数据一致性 | 精准一致,不包含跨 Barrier 数据 | 精准一致,快照包含 in-flight Buffer |
| 反压影响 | 对齐阻塞通道 → 反压加剧 | 不等 → 不加剧反压 |
| 快照大小 | 小(只包含状态) | 大(含 in-flight Buffer) |
| 适用场景 | 正常处理、低反压 | 反压严重、大状态作业 |
Flink 1.13 之后还引入了超时自动切换:配置 execution.checkpointing.aligned-checkpoint-timeout,先尝试对齐,超过时间未到齐则自动切换为非对齐。在源码中对应 AlternatingCollectingBarriers 的 alignmentTimeout() 方法。
整体执行流程
Checkpoint 的完整生命周期从触发到完成,分以下几个阶段。注意各阶段是并行的——Barrier 从 Source 注入后在数据流中向下传播,不同算子的快照可以同时进行。
1 | CheckpointCoordinator(JobManager 上) |
整体流程的关键点:Barrier 是沿着数据流走的”快照令牌”——Source 生成它,每个算子收到后触发自己的快照,然后转发给下游。全部确认收齐后,这个 Checkpoint 才算成功。
StateBackend 与 Checkpoint Storage
Checkpoint 涉及两个容易混淆的概念:
- StateBackend:决定运行时状态在 TaskManager 里怎么组织。比如用 HashMap 存在堆内存,还是用 RocksDB 存在本地磁盘。
- Checkpoint Storage:决定 Checkpoint 时把这些状态快照写到哪。比如写到 HDFS 文件系统,还是 JobManager 内存。
StateBackend 有 HashMapStateBackend 和 EmbeddedRocksDBStateBackend 两种:
| StateBackend | 本地存储 | 适用场景 |
|---|---|---|
| HashMapStateBackend | JVM 堆内存 | 状态量小(< 1GB)、追求低延迟 |
| EmbeddedRocksDBStateBackend | RocksDB(本地磁盘) | 状态量大(GB ~ TB 级别) |
两种 StateBackend 的 Checkpoint 写入方式也不同:
- HashMapStateBackend:Checkpoint 时序列化全部状态到 DFS,全量写入
- EmbeddedRocksDBStateBackend:默认使用增量 Checkpoint,只上传上次 Checkpoint 以来变更的 SST 文件,效率远高于全量
Checkpoint Storage 则只有 JobManagerCheckpointStorage(存 JM 内存,测试用)和 FileSystemCheckpointStorage(存 HDFS/本地文件系统,生产用)。生产中一般配 state.checkpoints.dir,Flink 会自动帮你选 FileSystemCheckpointStorage。
Checkpoint vs Savepoint
| 对比维度 | Checkpoint | Savepoint |
|---|---|---|
| 触发方式 | 自动周期性触发 | 用户手动触发 |
| 存储格式 | StateBackend 原生格式 | 标准 CANONICAL 格式(也可指定 NATIVE 原生格式) |
| 生命周期 | 自动清理(根据保留策略) | 用户手动管理,长期保留 |
| 用途 | 故障恢复 | 作业升级、迁移、分支开发 |
| 性能 | 快(RocksDB 增量更快) | 默认慢(全量 CANONICAL),指定 NATIVE 后同 Checkpoint |
| 配置 | state.checkpoints.dir |
state.savepoints.dir |
| 跨版本兼容 | 不保证 | CANONICAL 保证,NATIVE 不保证 |
Savepoint 底层复用了 Checkpoint 的完整机制,两者在代码层面走的是同一套流程(CheckpointCoordinator.triggerCheckpoint),区别在于 CheckpointType 的不同:
1 | CheckpointType: CheckpointType{name='Checkpoint', ...} |
更多 Savepoint 的实际操作见 Flink Savepoints 总结。
关键概念汇总
| 概念 | 说明 |
|---|---|
| Checkpoint | 分布式快照,周期性自动执行,用于故障恢复 |
| Barrier | 插入数据流中的特殊标记,标识快照边界 |
| 对齐 | 多输入算子等待所有通道 Barrier 到齐后再快照 |
| 非对齐 | Barrier 到达即快照,包含 in-flight 数据,适用于反压场景 |
| PendingCheckpoint | 正在执行中的 Checkpoint |
| CompletedCheckpoint | 已完成的 Checkpoint,可用于恢复 |
| StateBackend | 状态存储后端,决定运行时状态在内存还是 RocksDB |
| Checkpoint Storage | 快照存储,决定 Checkpoint 文件写到哪(JM 内存或 HDFS) |
| Exactly-Once | Checkpoint + Barrier + 两阶段提交保证的语义级别 |
| CheckpointCoordinator | 运行在 JobManager 上的 Checkpoint 协调者 |
下一篇
Flink Checkpoint 源码(一):CheckpointCoordinator 触发链路 将进入源码分析,从 CheckpointCoordinator 的 startCheckpointScheduler() 和 triggerCheckpoint() 开始,逐层剖析 Checkpoint 的触发链路。