核心结论:正常路径成功只能证明链路能跑;只有在明确提交边界上主动制造故障,并用 Source 位点、Checkpoint、事务日志和目标数据共同举证,才能证明链路如何恢复。
一、为什么"杀掉 TaskManager 后自动重启"不算完整测试
它最多证明:
text
Restart Strategy 生效
任务能够重新进入 Running
它没有证明:
- 数据库快照是否从正确 Chunk 恢复;
- Kafka 未提交事务是否对消费者不可见;
- JDBC 部分批次是否 Rollback;
- Commit 成功但 Checkpoint 失败后是否出现重复;
- SQLServer 旧事件是否覆盖新状态;
- Delete 是否在恢复后仍然生效;
- 元数据故障是否导致数据被静默跳过。
故障注入必须先定义要证明的语义,再选择故障点。
二、先定义六个可靠性不变量
不变量 1:不丢业务 Key
源端在测试边界内最终存在的业务 Key,目标端不能无故缺失。
不变量 2:不残留已删除数据
源端最终不存在的 Key,目标端不能因为 Delete 丢失而继续存在。
不变量 3:最终版本不回退
同一 Key 的目标版本不能被旧 LSN、旧版本号或旧更新时间覆盖。
不变量 4:业务主键唯一
At-least-once 重放允许物理写入再次发生,但目标业务结果不能出现重复唯一键。
不变量 5:失败不能伪装成功
关键数据无法解析、Route 缺失、JDBC Rollback 或 Checkpoint 存储不可用时,任务必须失败或进入明确隔离流程,不能只记录日志后继续。
不变量 6:恢复点可解释
任何一次恢复都能说明来自哪个 Checkpoint/Savepoint、Source 位点是什么、哪些外部事务已提交。
三、把链路拆成可注入故障的边界
text
源数据库
│ A. Snapshot/Log 读取
↓
Flink CDC Source State
│ B. Source → Kafka Transaction
↓
Kafka Topic / Partition / Offset
│ C. Kafka Source Checkpoint
↓
Order Process Keyed State
│ D. JDBC Buffer
↓
JDBC Transaction
│ E. Commit
↓
Checkpoint Complete
│ F. 外部恢复与运维
↓
目标业务结果
每个字母位置的故障结果不同,不能用一次随机 Kill 覆盖全部语义。
四、故障矩阵
| 编号 | 注入位置 | 注入动作 | 正确预期 |
|---|---|---|---|
| F01 | Snapshot Chunk 中间 | 终止 Source Reader/TaskManager | 从成功 Checkpoint 恢复,已确认 Chunk 不重扫,未确认 Chunk 可重做 |
| F02 | Source 日志读取中 | 删除网络连接 | Connector 重连或任务失败恢复,位点不越过未确认记录 |
| F03 | Kafka 事务提交前 | 终止 Source-to-Kafka Job | Read Committed 消费者看不到未提交事务 |
| F04 | Kafka 发送完成、Checkpoint 前 | 终止任务 | 未提交事务被中止,恢复后重新产生且业务只可见一次 |
| F05 | Kafka Consumer 处理后、Checkpoint 前 | 终止下游 Job | 从旧 Offset 重放,目标端依赖幂等保持唯一 |
| F06 | JDBC Batch 第 N 条 | 人为抛 SQL 异常 | 整个事务 Rollback,Buffer 保留,Checkpoint 失败 |
| F07 | JDBC Commit 后、Checkpoint ACK 前 | 终止 TaskManager | 恢复后可能重写,最终业务结果不重复、不回退 |
| F08 | 后台定时 Flush | 断开目标库 | 异常被保存;下条记录同步重试,失败则任务失败 |
| F09 | SQLServer 顺序算子 | 交换旧/新 LSN 到达顺序 | 旧事件被丢弃并产生指标,最终版本不回退 |
| F10 | Checkpoint Storage | 让 HDFS 路径不可写 | Checkpoint 失败,超过策略后任务不伪装健康 |
| F11 | Redis/配置元数据 | 删除某表 Route | 按策略明确失败或隔离,不能无告警静默漏表 |
| F12 | Schema 变更中 | 新旧 Schema 交错发送 | 兼容变更正确落地,破坏性变更被阻断 |
五、建立一套可复现测试数据
准备专用测试表:
text
id 业务主键
version 单调递增版本
status 业务状态
updated_at 源端更新时间
payload_hash 关键字段摘要
生成可预测操作:
text
1~10000:初始 Insert
1~3000:Update version=2
3001~5000:Delete
5001~7000:Delete → Insert version=3
7001~9000:连续 Update 到 version=20
9001~10000:保持不变作为对照组
测试数据必须可重复生成,且每轮使用独立 Run ID,避免上一次残留干扰结果。
六、一次故障实验的标准步骤
1. 记录基线
text
Job ID
代码提交哈希
配置版本
Source 启动模式
Checkpoint Interval/Timeout
Kafka Topic、Partition、Consumer Group
Sink Batch Size/Flush Interval
测试 Run ID
2. 等待明确前置状态
例如 F07 必须确认 JDBC Commit 已完成,但 Checkpoint 尚未完成。没有触发窗口证据,就不能声称测试到了这个边界。
3. 注入一次故障
一次实验只改变一个变量。不要同时停止 Kafka、数据库和 TaskManager,否则无法归因。
4. 保留运行证据
至少保存:
- Flink Checkpoint ID、状态和各阶段耗时;
- TaskManager/JobManager 日志时间点;
- Kafka Transaction/Offset 观察结果;
- JDBC Commit/Rollback 证据;
- Source/目标数据校验结果;
- 恢复前后 Job Attempt。
5. 等待追平再验收
任务回到 Running 不等于恢复完成。应等待 Source Lag、Kafka Lag、待处理 Buffer 和目标更新时间全部稳定。
七、怎样证明 F01:快照故障可恢复
- 使用
initial启动大表快照; - 记录已完成和剩余 Snapshot Split;
- 在某个 Chunk 扫描期间终止 Reader;
- 从最近成功 Checkpoint 恢复;
- 确认已确认 Chunk 数不会整体归零;
- 允许未确认 Chunk 重读;
- 快照完成后比较主键集合与字段摘要;
- 检查初始化期间发生的 Insert/Update/Delete。
判定标准:
text
恢复进度正确
最终主键集合一致
最终字段状态一致
目标主键无重复
Delete 无残留
八、怎样证明 F03/F04:Kafka Exactly Once 生效
前置条件:
- Flink Checkpoint 已启用;
- Kafka Sink 使用
DeliveryGuarantee.EXACTLY_ONCE; - Transactional ID Prefix 稳定且不同 Job 不冲突;
- 消费者使用
read_committed; - Kafka Transaction Timeout 覆盖最坏 Checkpoint 与恢复时间。
实验:
- 在一个 Checkpoint 周期内持续发送带唯一序号的事件;
- 在事务提交前停止 Source-to-Kafka Job;
- 使用
read_committed和read_uncommitted分别观察; - 从 Checkpoint 恢复;
- 比较业务序号缺口与重复。
需要证明的是"已提交消费者的可见结果",不是 Broker 上是否曾写过未提交记录。
九、怎样证明 F06/F07:JDBC 恢复语义准确
F06:事务中途失败
在 Batch Writer 第 N 条记录抛异常:
text
前 N-1 条已执行但未 Commit
↓
第 N 条失败
↓
Rollback
↓
Buffer 保留
↓
Checkpoint 失败
验收目标表中不能留下这批部分结果。
F07:Commit 后失败
在 Commit 成功后、Checkpoint 完成前终止任务。恢复后同一批会再次写入,这是普通 JDBC Sink 的预期 At-least-once 行为。
正确验收不是"数据库日志只有一次 Insert",而是:
text
目标业务主键唯一
字段为最后版本
Delete 后无残留
重复执行没有改变最终业务结果
如果测试结果确实出现两次物理执行,但业务结果唯一,反而证明语义描述准确。
十、怎样证明 F09:旧事件不会覆盖新事件
对同一个 SQLServer 业务 Key 构造:
text
LSN 100:version=1
LSN 102:version=3
LSN 101:version=2(延迟到达)
预期:
text
100 放行
102 放行并更新 ValueState
101 被判定为旧事件并丢弃
目标最终 version=3
然后在 102 放行后、Checkpoint 完成前终止任务,验证恢复重放后 ValueState 和目标结果不会回退。
还必须测试 Delete/Insert,而不只是 Update 版本号。
十一、数据校验不能只用 COUNT(*)
两个集合行数相同,也可能各缺一条不同记录。
至少使用五层校验:
1. 数量
sql
SELECT COUNT(*) FROM target WHERE run_id = ?;
2. 唯一性
sql
SELECT id, COUNT(*)
FROM target
WHERE run_id = ?
GROUP BY id
HAVING COUNT(*) > 1;
3. 主键集合差异
分别找 Source 有/Target 无和 Target 有/Source 无的 Key。
4. 字段摘要
按稳定字段生成 Hash,比较同一 Key 的最终业务内容。
5. 单调版本
确认目标 version 不小于本轮源端最终版本,重点检查高频更新和乱序样本。
数据量、唯一性、集合和内容必须同时通过。
十二、可靠性验收报告应该长什么样
| 项目 | 记录内容 |
|---|---|
| 实验编号 | F01~F12 |
| 语义声明 | Exactly Once / At-least-once + 幂等 / 明确降级 |
| 注入位置 | 精确到提交前后边界 |
| 注入方法 | 终止进程、断网、SQL 异常、存储不可写等 |
| Checkpoint 证据 | ID、状态、恢复来源 |
| 外部系统证据 | Kafka 可见性、JDBC Commit/Rollback |
| 数据校验 | 数量、唯一性、集合、摘要、版本 |
| 实际结果 | PASS / FAIL |
| 偏差与处理 | 与预期不同的原因和后续动作 |
一条测试只有"现象"和"结论",没有边界证据,不能进入生产验收。
十三、推荐的最小生产门槛
- Initial Snapshot 并发变更和故障恢复通过
- Source-to-Kafka 事务故障测试通过
- Kafka-to-JDBC Commit 后重放测试通过
- JDBC Batch 中途失败全量 Rollback
- SQLServer 乱序旧事件不会覆盖新值
- Delete、Delete → Insert、主键更新均有用例
- Checkpoint Storage 不可用时任务不会伪装健康
- Route/主键元数据缺失时行为明确
- Schema Add/Alter/Drop 策略分别验证
- 所有结论有日志、指标和数据查询证据
十四、这套测试不能保证什么
- 通过有限故障用例不能数学证明所有生产故障;
- 单机测试不能替代真实 Kafka、数据库和存储容量压测;
- 最终一致校验不能替代延迟、RTO、RPO 指标;
- 自动重启不能替代日志保留、Checkpoint 可用性和运维响应;
- 测试环境的低并发结果不能直接外推到生产峰值。
但它可以把"我觉得不会丢"提升为:
在明确版本、配置、故障边界和数据集下,链路表现符合已声明的恢复语义,并有可复查证据。
十五、系列总结
一条生产级 CDC 链路的正确性来自连续成立的机制:
text
增量快照用 LOW/HIGH 校正初始化并发变化
↓
Checkpoint Barrier 建立一致恢复点
↓
JDBC Flush 用事务和幂等承接外部提交
↓
LSN + Keyed State 阻止旧事件覆盖新状态
↓
Operator UID + Savepoint 保证升级时状态可解释
↓
Schema Governance 保证字段契约持续一致
↓
故障注入用证据证明每个边界真的成立
精讲的目标不是记住更多参数,而是能够准确说出:
text
这段链路保证什么?
依靠哪个提交点保证?
故障发生在提交点前后分别怎样?
恢复后用什么证据判断正确?
哪些范围仍然不保证?
