Flink 源码与运行原理

流式计算 / 源码阅读 / LESSON 03

Checkpoint 与 Savepoint 深度解析

厘清 Checkpoint、Savepoint、Barrier 与故障恢复之间的边界和协作方式。

阅读时间
90 分钟
学习路径
Flink 源码与运行原理
内容来源
Flink 深度笔记

预计阅读时间: 90 分钟 前置阅读: doc-01(JobMaster), doc-02(Barrier介绍) 下一次阅读: doc-04(StateBackend), doc-13(恢复)


1. Chandy-Lamport 分布式快照算法

1.1 算法原理

Flink 的 Checkpoint 基于 Chandy-Lamport(1985)分布式快照算法。该算法的核心思想是:通过在数据流中注入特殊的标记(Marker),让每个进程在收到标记后记录自己的状态,从而获得全局一致快照,而不需要停止整个系统。

Chandy-Lamport 算法的 4 个关键步骤:

1. 发起者(Initiator): 记录自己的状态,然后向所有出边发送 Marker
2. 接收者(Receiver):
   - 如果是第一次收到 Marker(来自任意入边),立即记录自己的状态,
     并向所有出边转发 Marker
   - 开始记录该入边之后到达的所有消息(Channel 状态)
3. Channel 状态: 从收到 Marker 的那个入边开始,到该入边再次收到 Marker 前,
   该 Channel 上到达的所有消息都属于 Channel 状态
4. 终止: 当所有进程都收到所有入边的 Marker 后,快照完成

1.2 Flink 的适配:Barrier

Flink 将这个算法中的 Marker 具体化为 CheckpointBarrier——在数据流中传播的特殊事件。Barrier 携带的关键信息:

// CheckpointBarrier.java — Barrier 结构
public class CheckpointBarrier extends RuntimeEvent {
    private final long id;                    // 单调递增的 Checkpoint ID
    private final long timestamp;             // Barrier 创建时间戳 (JM 端)
    private final CheckpointOptions checkpointOptions;  // 选项: aligned/unaligned, timeout 等

    // Barrier 是 RuntimeEvent,与 StreamRecord 在同一 Channel 中顺序传输
}

Barrier 与数据的顺序保证:Barrier 被注入到数据流中,与 StreamRecord 在同一个 Channel(ResultSubpartition → InputChannel)中传输,因此 Barrier 之前的数据一定在 Barrier 之前被处理,不会被"越过"。

1.3 Barrier 对齐(Aligned Checkpoint)

当算子有多个上游输入 Channel 时,它在收到第一个 Channel 的 Barrier 后需要等待其他 Channel 的 Barrier:

算子有 3 个输入 Channel:
  Channel-A:  data1 → data2 → Barrier(#1) → data3 → ...
  Channel-B:  data4 → Barrier(#1) → data5 → ...
  Channel-C:  data6 → data7 → data8 → Barrier(#1) → ...

Barrier 对齐过程:
  1. Channel-A 先到达 Barrier → 算子停止消费 Channel-A,记录 "Barrier received from A"
  2. Channel-B 随后到达 Barrier → 算子停止消费 Channel-B,记录 "received from B"
  3. Channel-C 的 Barrier 到达前,Channel-C 的数据 data6/data7/data8 继续被处理(正常消费)
  4. Channel-C 的 Barrier 到达 → 3 个输入都收到 Barrier → 对齐完成
  5. 算子执行 snapshotState() → 向下游转发 Barrier

对齐的性能代价:在等待慢 Channel 的 Barrier 期间,快 Channel 被阻塞,数据堆积在 Channel 的缓冲区中。这段时间称为 Barrier Alignment Time(对齐时间),是 Checkpoint 性能的关键指标。

1.4 非对齐 Checkpoint(Unaligned Checkpoint)

非对齐 Checkpoint 解决了 Barrier 对齐导致的阻塞问题:

Aligned(对齐):
  - 等待所有 Channel 的 Barrier 对齐 → 做状态快照
  - 快照内容 = 算子状态 + 所有 Barrier 之前的数据已处理
  - 阻塞: 快 Channel 等待慢 Channel 时暂停消费
  - 延迟代价高但快照体积小

Unaligned(非对齐):
  - 第一个 Channel 的 Barrier 到达后,立即开始做快照
  - 快照内容 = 算子状态 + in-flight data(未处理但已读取的数据)
  - 不阻塞: 所有 Channel 继续消费数据
  - 延迟低但快照体积大(需要持久化 in-flight buffer)

源码中的实现:

// CheckpointBarrierHandler.java — aligned vs unaligned 的分支
public abstract class CheckpointBarrierHandler implements Closeable {
    // Aligned 实现: 阻塞快 Channel,等待 Barrier 对齐
    // Unaligned 实现: 不阻塞,将 in-flight buffer 直接持久化

    public abstract void processBarrier(
            CheckpointBarrier receivedBarrier, InputChannelInfo channelInfo, boolean isRpcTriggered)
            throws IOException;

    protected void notifyCheckpoint(CheckpointBarrier checkpointBarrier) throws IOException {
        CheckpointMetaData checkpointMetaData =
                new CheckpointMetaData(
                        checkpointBarrier.getId(),
                        checkpointBarrier.getTimestamp(),
                        System.currentTimeMillis());

        CheckpointMetricsBuilder checkpointMetrics;
        if (checkpointBarrier.getId() == startAlignmentCheckpointId) {
            checkpointMetrics =
                new CheckpointMetricsBuilder()
                    .setAlignmentDurationNanos(latestAlignmentDurationNanos)   // 对齐耗时
                    .setBytesProcessedDuringAlignment(latestBytesProcessedDuringAlignment)
                    .setCheckpointStartDelayNanos(latestCheckpointStartDelayNanos); // 传播延迟
        }
        toNotifyOnCheckpoint.triggerCheckpointOnBarrier(
                checkpointMetaData, checkpointBarrier.getCheckpointOptions(), checkpointMetrics);
    }
}

1.5 两种模式对比

维度AlignedUnaligned
对齐时行为阻塞快 Channel,等待慢 Channel不阻塞,所有 Channel 继续消费
快照内容算子状态算子状态 + in-flight buffer
快照体积大(in-flight data)
Checkpoint 时间取决于最慢 Channel 的 Barrier 到达时间几乎恒定(不等待对齐)
端到端延迟对齐期间暂停处理(延迟尖刺)无延迟尖刺
适用场景反压低、低吞吐管线高吞吐、反压严重管线
Channel 间数据倾斜慢 Channel 数据多 → 对齐时间长不受倾斜影响

2. CheckpointCoordinator — JM 端的 Checkpoint 大脑

2.1 类结构

CheckpointCoordinator(2453 行)运行在 JobMaster 进程中,是 Checkpoint 的全局协调者。

CheckpointCoordinator 的核心状态:

private final long baseInterval;                       // Checkpoint 间隔(ms)
private final long checkpointTimeout;                   // Checkpoint 超时(ms)
private final long minPauseBetweenCheckpoints;          // 两次 Checkpoint 最小间隔
private final Map<Long, PendingCheckpoint> pendingCheckpoints;  // 进行中的 Checkpoint
private final CompletedCheckpointStore completedCheckpointStore; // 已完成的 Checkpoint
private final CheckpointIDCounter checkpointIdCounter;  // CheckpointID 生成器(单调递增)
private final ArrayDeque<Long> recentExpiredCheckpoints; // 最近过期的 Checkpoint ID(去重用)
private ScheduledTrigger currentPeriodicTrigger;        // 当前周期性触发器

2.2 triggerCheckpoint — 触发全流程

// CheckpointCoordinator.java — 核心触发方法 (简化逻辑)
public CompletableFuture<CompletedCheckpoint> triggerCheckpoint(
        CheckpointTriggerRequest request) {

    synchronized (lock) {
        // 1. 检查是否允许触发
        // - 不能有正在进行的 Checkpoint(maxConcurrentCheckpoints=1 时)
        // - 检查 minPauseBetweenCheckpoints
        // - 检查 maxConcurrentCheckpoints

        // 2. 分配 CheckpointID(单调递增,跨 JM 重启保持递增)
        long checkpointID = checkpointIdCounter.getAndIncrement();

        // 3. 获取 Checkpoint 存储位置
        CheckpointStorageLocation checkpointLocation =
            checkpointStorageView.initCheckpoint(checkpointID);

        // 4. 构造 PendingCheckpoint(追踪所有算子的 Ack)
        PendingCheckpoint pendingCheckpoint = new PendingCheckpoint(
            job, checkpointID, checkpointTimestamp,
            checkpointProperties, operatorStates, coordinatorStates,
            checkpointLocation, ...);

        pendingCheckpoints.put(checkpointID, pendingCheckpoint);

        // 5. 触发 Master Hooks(如外部存储的预提交)
        for (MasterTriggerRestoreHook<?> hook : masterHooks.values()) {
            hook.triggerCheckpoint(checkpointID, timestamp, ...);
        }

        // 6. 向所有 Source 算子发送 Checkpoint Trigger(RPC)
        for (ExecutionVertex source : sources) {
            source.triggerCheckpoint(checkpointID, timestamp, checkpointOptions);
        }

        return pendingCheckpoint.getCompletionFuture();
    }
}

触发时机

  1. 周期性触发:Timer 按 baseInterval 定时触发
  2. 手动触发:用户通过 REST API 或 flink savepoint 命令触发
  3. Checkpoint 完成后自动触发下一个

2.3 PendingCheckpoint — 追踪 Ack

PendingCheckpoint 内部状态:

┌─────────────────────────────────────────┐
│         PendingCheckpoint               │
├─────────────────────────────────────────┤
│ checkpointID: 42                        │
│ numberOfSubtasks: 100                   │
│ numberOfAcknowledgedSubtasks: 0→100       │
│ acknowledgedTasks: Set<ExecutionAttemptID>│
│ stateHandles: List<StateHandle>          │
│ masterStates: Map<String, StateHandle>   │
│ checkpointStorageLocation: .../chk-42    │
│ completionFuture: CompletableFuture<>    │
│                                         │
│ 当 numberOfAcknowledgedSubtasks ==      │
│     numberOfSubtasks 时                 │
│ → Checkpoint COMPLETE!                  │
└─────────────────────────────────────────┘

2.4 receiveAcknowledgeMessage — 接收 Ack

// CheckpointCoordinator.java — 接收算子 Ack
public boolean receiveAcknowledgeMessage(
        AcknowledgeCheckpoint message, String taskManagerLocationInfo) {

    synchronized (lock) {
        long checkpointId = message.getCheckpointId();
        PendingCheckpoint pendingCheckpoint = pendingCheckpoints.get(checkpointId);

        if (pendingCheckpoint == null) {
            // 去重: 检查是否是最近过期的 Checkpoint
            // 如果是则忽略(late ack),否则报错
            if (recentExpiredCheckpoints.contains(checkpointId)) {
                return false;  // 正常的 late ack
            }
            throw new IllegalArgumentException("unknown checkpoint: " + checkpointId);
        }

        // 记录 Ack
        boolean complete = pendingCheckpoint.acknowledgeTask(
            message.getTaskExecutionId(),
            message.getSubtaskState(),    // 算子的 StateHandle
            message.getCheckpointMetrics());

        if (complete) {
            // 所有算子都 Ack 了 → 完成 Checkpoint
            completedCheckpointStore.addCheckpoint(
                pendingCheckpoint.finalizeCheckpoint(), ...);
            // 清理 pending
            pendingCheckpoints.remove(checkpointId);
            recentExpiredCheckpoints.addLast(checkpointId);
            // 通知 Sink Committer 可以提交
            // (Kafka Sink 的两阶段提交在这里最终 commit)
        }

        return complete;
    }
}

Ack 的去重逻辑:使用 recentExpiredCheckpoints (ArrayDeque, 容量 16) 维护最近过期的 Checkpoint ID。当一个 Ack 到达时如果 Checkpoint 已完成/超时,检查是否在过期列表中——在则是正常的 late ack(忽略),不在则是未知的 CheckpointID(报错)。

2.5 Checkpoint 超时处理

// Registered at checkpoint creation
synchronized (lock) {
    ScheduledFuture<?> canceller = timer.schedule(
        () -> {
            synchronized (lock) {
                if (pendingCheckpoints.containsKey(checkpointId)) {
                    PendingCheckpoint timedOut = pendingCheckpoints.remove(checkpointId);
                    timedOut.abortCheckpointOnTimeout();
                    // → notify all operators to cancel this checkpoint
                }
            }
        },
        checkpointTimeout,
        TimeUnit.MILLISECONDS
    );
    pendingCheckpoint.setCancellerHandle(canceller);
}

3. Checkpoint 在算子端的执行

3.1 StreamTask 中的 Checkpoint 处理

// StreamTask.java — 收到 Barrier 后的处理
public void triggerCheckpointOnBarrier(
        CheckpointMetaData checkpointMetaData,
        CheckpointOptions checkpointOptions,
        CheckpointMetricsBuilder checkpointMetrics) throws IOException {

    // 1. 执行算子链中所有算子的 snapshotState()
    operatorChain.snapshotState(
        checkpointMetaData.getCheckpointId(),
        checkpointMetaData.getTimestamp(),
        checkpointOptions,
        checkpointStorageLocation);

    // 2. 完成 Checkpoint(发送 Ack 到 JM)
    operatorChain.finishCheckpoint(checkpointMetaData, checkpointMetrics);
}

3.2 单个算子的 snapshotState

// AbstractStreamOperator.java — 每个算子的快照
public void snapshotState(StateSnapshotContext context) throws Exception {
    // 1. 快照 KeyedState (通过 KeyedStateBackend)
    if (keyedStateBackend != null) {
        keyedStateBackend.snapshot(
            context.getCheckpointId(),
            context.getCheckpointTimestamp(),
            checkpointStorage,
            ...);
    }

    // 2. 快照 OperatorState (如 Kafka Source offset)
    if (operatorStateBackend != null) {
        operatorStateBackend.snapshot(
            context.getCheckpointId(),
            context.getCheckpointTimestamp(),
            checkpointStorage,
            ...);
    }

    // 3. 快照 Timer 状态 (WindowOperator 的 Timer 堆)
    if (internalTimerService != null) {
        internalTimerService.snapshotState(...);
    }
}

3.3 完整 Checkpoint 执行时序

Checkpoint 与 Savepoint 深度解析 图 01


4. 增量 Checkpoint(RocksDB Incremental)

4.1 为什么需要增量 Checkpoint

全量 Checkpoint 每次把 RocksDB 的所有 SST 文件写入外部存储(HDFS/S3),代价极高:

Checkpoint #1: 上传 SST_1, SST_2, SST_3 (3 个文件, 全量)
Checkpoint #2: 上传 SST_1, SST_2, SST_3, SST_4 (4 个文件, 全量) ← SST_1~3 重复上传!
Checkpoint #3: 上传 SST_1,..., SST_5 (5 个文件, 全量)

增量 Checkpoint 只上传与上次 Checkpoint 之间的差异 SST 文件:

Checkpoint #1: 上传 SST_1, SST_2, SST_3 (全量, 没有基线)
Checkpoint #2: 上传 SST_4 (只上传新增的) + SharedStateRegistry 记录 SST_1~3 来自 CHK#1
Checkpoint #3: 上传 SST_5 (只上传新增的) + 记录 SST_3 被 compaction 删除

4.2 SharedStateRegistry — SST 文件跨 Checkpoint 共享

SharedStateRegistry
├── 跟踪每个 SST 文件被哪些 Checkpoint 引用
├── 引用计数 GC: 当所有引用的 Checkpoint 都被丢弃后,SST 文件被删除
└── 文件去重: 多个 Checkpoint 多次引用同一个 SST → 只存一份

文件生命周期:
  SST_1 → 被 Checkpoint#1, #2 引用 (refCount=2)
       → Checkpoint#1 过期 → refCount=1
       → Checkpoint#2 过期 → refCount=0 → 文件被删除

4.3 RocksDB 增量 Snapshot 源码流程

// RocksDBKeyedStateBackend.java — 增量 Checkpoint 的核心
public RunnableFuture<SnapshotResult<KeyedStateHandle>> snapshot(
        long checkpointId, long timestamp,
        CheckpointStreamFactory streamFactory,
        CheckpointOptions checkpointOptions) throws Exception {

    // 1. Flush MemTable → SST (确保内存数据持久化到磁盘)
    for (RocksDBIncrementalSnapshotOperation op : snapshotOperations) {
        op.takeSnapshot();  // RocksDB 的 checkpoint API
    }

    // 2. 获取当前 Checkpoint 的 SST 文件列表
    // 底层调用: RocksDB.getLiveFiles() 或 getColumnFamilyMetaData()
    Set<SstFile> currentSstFiles = getLiveSstFiles();

    // 3. 与上次 Checkpoint 的 SST 文件列表做 diff
    // RocksDBIncrementalCheckpointUtils.chooseSstFilesForUpload()
    Set<SstFile> newSstFiles = diff(currentSstFiles, lastCheckpointSstFiles);

    // 4. 上传新增的 SST 文件到 DFS
    Map<Long, StreamStateHandle> uploadedFiles = rocksDBStateUploader.uploadFilesToCheckpointFs(
        newSstFiles, streamFactory, ...);

    // 5. 构造 IncrementalRemoteKeyedStateHandle
    // - 记录本次 Checkpoint 的新增 SST handles
    // - 通过 SharedStateRegistry 注册对上次 Checkpoint SST 的引用
    IncrementalRemoteKeyedStateHandle handle =
        new IncrementalRemoteKeyedStateHandle(
            backendUID, keyGroupRange, checkpointId,
            sharedStateRegistry,
            uploadedFiles,           // 新上传的
            lastCheckpointSstFiles   // 共享上次的
        );

    // 6. 更新基线
    lastCheckpointSstFiles = currentSstFiles;

    return handle;
}

4.4 增量 Checkpoint vs 全量

维度全量 Checkpoint增量 Checkpoint
每次上传数据量全部 SST 文件仅新增/修改的 SST
Checkpoint 耗时O(总状态大小)O(增量)
恢复耗时O(总状态大小)O(总状态大小) — 需要下载所有 SST
磁盘占用(DFS)每个 Checkpoint 独立共享 SST 文件,引用计数管理
适用场景小状态、快速恢复大状态(TB 级)、频繁 Checkpoint

5. Savepoint vs Checkpoint — 深入对比

5.1 本质区别

维度CheckpointSavepoint
触发方式自动(周期性)手动 (flink savepoint 或 REST API)
生命周期Job 结束后可被清除(execution.checkpointing.externalized-checkpoint-retention用户手动管理,永久保留
格式增量(默认),依赖 SharedStateRegistry自包含 (self-contained / canonical),不依赖其他 Checkpoint
并行度变化不支持(设计上不支持,实际中可以但不推荐)专门支持 — 可修改并行度、修改 JobGraph、升级 Flink 版本
所有权Flink 自动管理用户管理
元数据_metadata 文件 + SST 文件引用完整的自包含元数据 + 所有 SST 文件

5.2 Savepoint 的自包含格式(Canonical)

Savepoint 使用 Self-Contained 格式:所有状态数据(SST 文件)完整写入 Savepoint 目录,不依赖任何外部引用。

[hdfs://savepoints/savepoint-abc123]
├── _metadata                    # 所有算子的状态元数据
│   ├── OperatorID → KeyGroupRanges → StateHandle 列表
│   ├── MasterState (CheckpointCoordinator 状态)
│   └── JobGraph 快照 (用于恢复验证)
├── 00000000-0000-...            # SST 文件(全量,非增量引用)
├── 00000001-0000-...
└── ...

Savepoint 为什么必须是自包含的?

  • Checkpoint 可以增量引用,因为它们是时间序列(下一个总是存在)
  • Savepoint 可能被拷贝到其他集群、HDFS → S3 迁移 → 必须自包含

5.3 Savepoint 恢复流程

// ExecutionGraph.java — 从 Savepoint 恢复
public void restoreSavepoint(SavepointRestoreSettings settings) {
    // 1. 从 CheckpointStore 加载 Savepoint
    CompletedCheckpoint savepoint = checkpointStore.getLatestCheckpoint();

    // 2. 解析 StateAssignment
    // - KeyGroupRangeAssignment 重新计算每个 Subtask 的 KeyGroup 范围
    // - 将 StateHandle 重新分配给新的 Subtask
    Map<OperatorID, OperatorState> oldStates = savepoint.getOperatorStates();
    StateAssignmentOperation stateAssignment = new StateAssignmentOperation(
        oldStates, newExecutionVertices, maxParallelism);

    // 3. 为每个 ExecutionVertex 分配对应的 StateHandle
    Map<ExecutionAttemptID, List<StateHandle>> assigned =
        stateAssignment.assignStates();

    // 4. Deploy Task 时带上 StateHandle
    // → Task.doRun() → StreamTask.initializeState(stateHandles)
    // → StateBackend.initializeState(stateHandles)
}

6. Checkpoint 存储后端

6.1 CheckpointStorage 接口

// CheckpointStorage.java — 决定 Checkpoint 写到哪里
public interface CheckpointStorage {
    // 初始化 Checkpoint 存储位置
    CheckpointStorageLocation initCheckpoint(long checkpointId) throws IOException;

    // 解析存储位置(恢复时用)
    CompletedCheckpointStorageLocation resolveCheckpoint(String externalPointer);
}

// CheckpointStorageLocation — 一次 Checkpoint 的存储位置
public interface CheckpointStorageLocation {
    StreamFactory createCheckpointStream();  // 创建写入流
    String getLocationAsPointer();           // 返回外部指针(如 hdfs://path/chk-42)
}

6.2 存储类型

存储路径适合
JobManager (Memory)JM 堆内存本地测试
FileSystemhdfs://cluster/flink/checkpoints/job-id/chk-42生产环境
S3 / OSS / GCS对象存储云原生生产环境

6.3 CompletedCheckpointStore

CompletedCheckpointStore — 存储已完成的 Checkpoint 元数据

实现:
  - StandaloneCompletedCheckpointStore: JM 内存中,最多保留 N 个
  - ZooKeeperCompletedCheckpointStore: ZK 持久化
  - FileSystemCompletedCheckpointStore: HDFS/S3 文件持久化

7. Checkpoint 关键配置深度解析

参数默认深度说明
execution.checkpointing.interval间隔越小,恢复时数据回追越少,但 Checkpoint 开销越频繁。推荐 1min~10min。背压时如果 isProcessingBacklog=true,Flink 会自动使用 interval-during-backlog(通常更短)来加速 Checkpoint。
execution.checkpointing.timeout10min超时后 Checkpoint 被取消,算子的 snapshotState() 也会被中断。需要大于状态大小 × 网络带宽的估算值。
execution.checkpointing.min-pause0防止 Checkpoint 过于密集导致系统过载。例如 interval=60s, min-pause=30s 意味着 Checkpoint 完成后至少等 30 秒才能触发下一个。
execution.checkpointing.max-concurrent1并发 Checkpoint 可能导致状态文件冲突。除非启用了 unaligned checkpoint 并且确认没有冲突,否则保持 1。
execution.checkpointing.unalignedfalse启用后 Checkpoint 不再等待 Barrier 对齐。适合反压严重的管线,但会增加 Checkpoint 体积。
execution.checkpointing.aligned-checkpoint-timeout0超过此时间的对齐将自动降级为 unaligned。0=永不降级。推荐设为 interval 的 50%,作为 aligned 到 unaligned 的自适应切换。
state.backend.incrementaltrueRocksDB 增量 Checkpoint。强烈建议开启——对 100GB 状态,全量 Checkpoint 可能需 10 分钟,增量只需几秒。
execution.checkpointing.externalized-checkpoint-retentionRETAIN_ON_CANCELLATION: Job 取消后保留 Checkpoint(可用于手工恢复);DELETE_ON_CANCELLATION: 取消时删除。

8. Checkpoint 性能分析

8.1 Checkpoint 耗时组成

TotalCheckpointDuration =
│
├── BarrierInjectionTime           (0-100ms, Source 注入 Barrier 的延迟)
├── BarrierPropagationTime         (Barrier 穿越整个 DAG 的时间)
│   ├── AlignmentTime              (对齐时间,主要耗时)
│   └── NetworkLatency             (Channel 延迟)
├── SyncSnapshotTime               (算子的同步快照时间: flush MemTable 等)
└── AsyncSnapshotTime              (算子的异步快照时间: 上传 SST 到 DFS)
                                    ↑ 通常最长

典型 Checkpoint 耗时分布 (100GB RocksDB 状态,S3 存储):
  BarrierPropagation: 100ms  (对齐)
  SyncSnapshot:       200ms  (flush MemTable)
  AsyncSnapshot:      30s    (上传增量 SST,可以在后台继续处理数据)

8.2 大状态 Checkpoint 优化

  1. 增量 Checkpoint:最有效的优化
  2. 增加 Checkpoint 间隔:60s → 180s → 600s,减少 Checkpoint 频率
  3. 使用更快的存储:S3 → HDFS(本地机房)→ 本地 SSD
  4. 减小状态大小:设置 State TTL、使用 RocksDB 压缩、清理无用状态
  5. Unaligned Checkpoint:在反压严重时消除对齐时间

9. 源码导航(完整版)

文件关键类/方法职责
flink-runtime/.../checkpoint/CheckpointCoordinator.javatriggerCheckpoint(), receiveAcknowledgeMessage(), restoreSavepoint()Checkpoint 全局协调
flink-runtime/.../checkpoint/PendingCheckpoint.javaacknowledgeTask(), finalizeCheckpoint()单个进行中的 Checkpoint
flink-runtime/.../checkpoint/CheckpointBarrier.javaCheckpointBarrier(id, timestamp, options)Barrier 事件
flink-runtime/.../checkpoint/CompletedCheckpointStore.javaaddCheckpoint(), getLatestCheckpoint()已完成 Checkpoint 存储
flink-runtime/.../checkpoint/CheckpointIDCounter.javagetAndIncrement()ID 生成器
flink-runtime/.../checkpoint/CheckpointStorage.javainitCheckpoint(), resolveCheckpoint()决定 Checkpoint 写入位置
flink-runtime/.../state/CheckpointStorageLocation.javacreateCheckpointStream()单个 Checkpoint 的写入流
flink-streaming-java/.../io/checkpointing/CheckpointBarrierHandler.javaprocessBarrier(), notifyCheckpoint()Barrier 对齐/非对齐处理
flink-state-backends/.../RocksDBIncrementalCheckpointUtils.javachooseSstFilesForUpload()增量 Checkpoint SST 选择
flink-runtime/.../state/StateAssignmentOperation.javaassignStates()Savepoint 恢复时的状态重分配

10. 常见问题 / 面试题

Q1: Checkpoint 超时的主要原因和排查方法?

A: 原因:(1) 反压 → Barrier 传播慢 → 查看 backPressuredTime;(2) 状态太大 → snapshot 耗时长 → 查看 checkpointSize 指标;(3) 外部存储慢(S3/EBS)→ 查看 AsyncSnapshotTime;(4) 对齐时间长 → 查看 AlignmentDuration。排查:从 Checkpoint History 中看哪个 Subtask 最后 Ack → 定位到具体的慢算子。

Q2: 非对齐 Checkpoint 为什么能降低延迟?原理是什么?

A: Aligned checkpoint 需要等待所有 Channel 的 Barrier 对齐,快 Channel 在这期间阻塞。Unaligned 不阻塞——第一个 Channel 的 Barrier 到达后立即开始 snapshot,将 Channel 中已读但未处理的 in-flight data(InputChannel buffer 内容)也持久化。代价是 Checkpoint 体积更大(包含 in-flight buffer),且恢复时需要先恢复 in-flight data 再重放。

Q3: 为什么不把 Checkpoint 间隔设得非常短(如每 5 秒)?

A: (1) Checkpoint 本身有开销(CPU + I/O + 网络); (2) 如果一次 Checkpoint 耗时 > 5 秒,下一次 Checkpoint 被 min-pause 阻塞,系统陷入持续的 Checkpoint 循环; (3) 小间隔意味着很多 Checkpoint 都是几乎相同的状态(增量很小),浪费资源。推荐最小值 60 秒。

Q4: 增量 Checkpoint 恢复时会不会因为 SST 文件跨 Checkpoint 共享而变慢?

A: 恢复时仍需下载所有 SST 文件(新增的 + 引用的),所以恢复时间由总状态大小决定,与增量无关。但:下载可以并发(多个文件从 S3 并行下载),且 RocksDB 恢复后只加载到 Block Cache 而不全量加载到内存。

Q5: Checkpoint 失败后 Flink 如何处理?

A: (1) 失败的 Checkpoint 被丢弃(pendingCheckpoints.remove()); (2) 向所有算子发送 CancelCheckpointMarker,让算子清理该 Checkpoint 的中间状态; (3) 下一个 Checkpoint 使用新的 CheckpointID; (4) 如果连续失败次数超过 tolerableCheckpointFailureNumber(默认 0 = 无容忍),Job 会 Fail。

Q6: Savepoint 恢复时并行度可以改吗?

A: 可以。Savepoint 按 KeyGroup 存储状态,KeyGroup 再映射到 Subtask。修改并行度 = 改变 KeyGroup→Subtask 的映射,不改变 Key→KeyGroup 的映射。因此状态可以重分配。但 OperatorState(如 Kafka Source offset)的恢复需要特殊处理——FLIP-27 Source 的 SplitEnumerator 会重新分配 Partition→Subtask。


下一步