一、两代项目的一致性边界
第一版直连
text
SQLServer Source → Flink State → Postgres JDBC Sink
Source 与 Sink 在同一个 Job,但普通 JDBC Commit 仍不受 Flink 两阶段提交协调,因此语义是:
text
At-least-once + Postgres ON CONFLICT 幂等
第二版 Kafka 架构
text
数据库 → Flink CDC → Kafka
Exactly Once(依赖 Checkpoint 与 Kafka 事务)
Kafka → Flink → Postgres / Doris JDBC
At-least-once + target-side idempotence
不能因为前半段 Exactly Once,就把整条链路都描述成 Exactly Once。
二、Checkpoint 保存什么
Checkpoint 保存 Source 位置、Keyed State 和算子状态,使任务重启后能够从一致快照恢复。
两代项目都逐步采用 HDFS 存储生产 Checkpoint。第一版按 Job 创建独立目录,第二版还区分本地与集群环境:本地默认使用文件路径,集群使用 HDFS,并允许命令行覆盖。
需要共同调整的参数包括:
text
interval
timeout
min pause
max concurrent checkpoints
tolerable failures
externalized checkpoint retention
restart attempts / delay
三、Source-to-Kafka 的事务协同
第二版 Kafka Sink 使用:
java
.setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)
.setTransactionalIdPrefix(transactionalIdPrefix)
Checkpoint 成功时提交 Kafka 事务。如果 Checkpoint 失败,本轮未提交事务对 Read Committed 消费者不可见。
这要求:
- Transactional ID 前缀稳定且不冲突;
- Kafka 事务超时大于最坏 Checkpoint 时间;
- Operator UID 稳定;
- Checkpoint 能持续成功,而不是长期失败。
四、Kafka 消费位置
Kafka Source 关闭自动提交,让 Offset 随 Checkpoint 提交:
java
enable.auto.commit=false
commit.offsets.on.checkpoint=true
Consumer Group 决定"谁共享一份消费进度",Startup Mode 决定"没有可恢复状态时从哪里开始"。
Flink 自身从 Checkpoint 恢复时,优先使用 Checkpoint 中的 Source State;Group 的 committed Offset 更多用于没有相应 Flink 状态时的启动边界。
五、JDBC Buffer 为什么必须在 Checkpoint 前 Flush
如果 Checkpoint 成功时 Buffer 仍在内存,Kafka Offset 已前进,但数据尚未落库,重启后这部分记录可能无法再次读取。
因此 AbstractJdbcCdcSink.snapshotState() 会调用:
java
@Override
public final void snapshotState(
FunctionSnapshotContext context) throws Exception {
runtime.flushGuard.flushForCheckpoint();
}
失败必须向上抛出,使 Checkpoint 失败。
六、一次 JDBC Flush 的事务顺序
java
try (Connection conn = targetDs.getConnection()) {
conn.setAutoCommit(false);
try {
for (RouteBatch batch : batches) {
writer.write(conn, batch.route, batch.records);
}
conn.commit();
clearCommittedBuffers();
} catch (Exception e) {
conn.rollback();
throw e;
}
}
必须遵守:
text
先写全部路由
↓
全部成功后 Commit
↓
Commit 成功后清 Buffer
如果提前清 Buffer,Rollback 后将没有数据可重试。
七、为什么目标端仍可能重复
text
数据库 Commit 成功
↓
Checkpoint 尚未完成
↓
TaskManager 故障
↓
从上一个成功 Checkpoint 重放
这批记录会再次写入。目标端必须保证:
- Insert / Update 通过业务主键 Upsert;
- Delete 重复执行不改变最终结果;
- 主键缺失时明确失败;
- 数据修正与不可变数据使用不同冲突策略。
八、顺序状态也必须恢复
SQLServer 保序不是无状态过滤。每个业务 Key 的最新 LSN、seqval、command_id 都是 Keyed State。
如果这部分状态没有进入 Checkpoint,任务恢复后旧事件可能重新被视为新事件。稳定 Key、稳定 Operator UID 和 Checkpoint 缺一不可。
九、Operator UID 为什么不能只用数字下标
第一版后期已经使用真实表清单 Hash 构建 UID;第二版继续把系统、算子角色、Topic 或表身份纳入 UID。
text
错误:source-group-0
更稳妥:job + role + identities hash
UID 的目标不是永远不变,而是在逻辑身份相同的前提下稳定;拓扑身份改变时,应明确暴露状态不兼容。
十、必须做的故障注入
- Source 正常读取时强制终止 TaskManager;
- Kafka 事务未提交时触发失败;
- JDBC 批量执行中途抛错并验证 Rollback;
- 数据库 Commit 后、Checkpoint 完成前停止任务;
- 从 Checkpoint 和 committed Offset 分别恢复;
- 验证目标表无重复业务结果;
- 修改表分组后验证 UID 与状态兼容;
- HDFS 不可写时确认任务不会伪装成功。
十一、小结
text
Checkpoint:保存可恢复状态
Kafka 事务:协调 Source-to-Kafka 消息提交
Offset:描述 Kafka 消费位置
JDBC 事务:保证一次 Flush 原子性
目标端幂等:承受故障后的重复执行
下一篇分析第二版最重要的代码重构:如何把第一版超过千行的 PostgresSink 拆成公共 JDBC 主流程和数据库差异 SPI。