一、Kafka 层承担四种职责
在当前项目中,Kafka 不只是消息队列,而是:
- 事件缓冲层:吸收下游短时故障和速度波动;
- 数据契约层:统一 MySQL、SQLServer CDC 事件;
- 分发层:让 Postgres、Doris 独立消费;
- 重放边界:在保留期内按 Topic、Partition、Offset 重新消费。
这四种职责决定了 Kafka 层不能只写一句 producer.send(json)。
二、KafkaRecord 应包含什么
项目把 Kafka 消息封装为统一记录,核心概念包括:
text
key:稳定业务键
value:CDC 事件 JSON
topic:事件所属数据流
partition:消费后可追踪分区
offset:消费后可追踪位置
Key 的重要性远高于普通日志消息。它直接影响:
- 同一业务对象是否进入同一 Partition;
- SQLServer 消费端能否按 Key 保存顺序状态;
- 下游是否能建立幂等主键;
- Delete 是否能与之前的 Insert / Update 对应。
三、进入 Kafka 前的 Enrichment
Source 输出的原始 JSON 先经过 CdcKafkaRecordEnrichmentFunction:
java
SingleOutputStreamOperator<KafkaRecord> kafkaRecords = stream
.flatMap(new CdcKafkaRecordEnrichmentFunction(
runtime.systemName,
runtime.redisName,
runtime.redisHost,
runtime.redisPort,
runtime.redisDatabase,
runtime.redisPassword))
.name("cdc-kafka-record-enrichment-" + suffix)
.uid("cdc-kafka-record-enrichment-"
+ runtime.systemName + "-" + suffix);
这一步通常需要完成:
- 识别或补齐
op; - 从
before/after提取主键; - 查询 Redis 主键元数据;
- 构建稳定 Kafka Key;
- 保留源系统、源表和顺序字段。
如果 Enrichment 无法获得关键主键,不应随便退化成随机 Key。那会把"启动失败"变成更难发现的数据乱序和脏数据。
四、Exactly-Once Kafka Sink
当前 Kafka Sink 使用 Flink 新 Sink API:
java
KafkaSink<KafkaRecord> sink = KafkaSink.<KafkaRecord>builder()
.setBootstrapServers(bootstrapServers)
.setKafkaProducerConfig(producerProps)
.setRecordSerializer(
new CdcKafkaRecordSerializationSchema(topic))
.setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)
.setTransactionalIdPrefix(transactionalIdPrefix)
.build();
配套 Producer 参数包括:
text
acks=all
enable.idempotence=true
retries>=3
max.in.flight.requests.per.connection=5
transaction.timeout.ms=...
Flink 在 Checkpoint 成功时提交 Kafka 事务,使 Source 状态和本次写入 Kafka 的消息保持一致。
五、Transactional ID 为什么必须稳定且唯一
项目为不同 Source 类型和系统生成独立前缀,例如:
text
flink-cdc-mysql-{system}-{topic}-{operator}-
flink-cdc-{sqlserver-system}-{topic}-{operator}-
前缀需要满足两点:
- 稳定:同一逻辑算子重启后仍能识别和清理遗留事务;
- 唯一:不同 Job、不同 Source 组不能使用相同前缀互相 Fence。
常见错误包括:
- 使用随机 UUID,导致重启后事务身份变化;
- 多个环境共用同一前缀;
- 复制 Job 后忘记修改系统或 Topic 部分;
- 前缀含 Kafka 不接受的特殊字符。
六、Topic 自动创建后的元数据延迟
项目启动时会检查 Topic,不存在则自动创建。但 Admin Client 返回创建成功,不代表所有 Broker 已立即完成元数据传播。
典型时间线:
text
CreateTopics 返回成功
↓
立刻 DescribeTopics
↓
UnknownTopicOrPartitionException
正确处理是:
- 对元数据未就绪异常做有限次数重试;
- 每次重试打印 Topic 和次数;
- 认证、网络、权限等其他错误不应被误判为传播延迟;
- 最终失败时保留原始异常链。
七、Kafka Source 的 Offset 语义
Kafka 消费端关闭自动提交:
java
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
props.put("commit.offsets.on.checkpoint", "true");
Source 支持三种启动模式:
java
switch (startupMode) {
case LATEST:
return OffsetsInitializer.latest();
case COMMITTED_OFFSETS:
return OffsetsInitializer.committedOffsets(
OffsetResetStrategy.EARLIEST);
case EARLIEST:
default:
return OffsetsInitializer.earliest();
}
选择含义:
| 模式 | 适合场景 | 风险 |
|---|---|---|
| Earliest | 新目标端需要消费保留期内全部数据 | 数据量大,可能触发长时间追赶 |
| Latest | 只关心启动后的新事件 | 会跳过已有消息 |
| Committed Offsets | 稳定生产任务重启 | 首次没有 Offset 时需要明确回退策略 |
八、Consumer Group 为什么按目标端拆分
假设 Postgres 和 Doris 都使用:
text
group.id=cdc-erp
Kafka 会把它们当作同一消费组中的两个消费者,共同分摊 Partition。结果不是"两边各一份",而是"两边合起来一份"。
正确设计:
text
cdc-erp-postgres
cdc-erp-doris
Consumer Group 应由:
text
environment + system + target type + target instance
共同构成,既避免冲突,也方便从名称识别用途。
九、Kafka 只能保证什么顺序
Kafka 能保证单 Partition 内的记录顺序,不能保证:
- 不同 Partition 全局有序;
- Key 不稳定时同一业务对象有序;
- 数据库事务顺序自动等同于 Kafka Offset;
- 下游批量执行不会重排操作。
因此顺序需要端到端共同保证:
text
稳定业务 Key
↓
同 Key 进入同一 Partition
↓
消费端按 Key 分组
↓
结合源端顺序字段过滤旧事件
↓
Sink 批量不改变操作顺序
十、SQLServer 消费端保序
当前项目只对启用配置的 SQLServer 链路增加保序算子:
java
DataStream<KafkaRecord> ordered = stream
.keyBy(new CdcKafkaRecordKeySelector())
.process(new SqlServerKafkaRecordOrderProcessFunction())
.uid(CdcJobSupport.operatorUid(
systemName,
"sqlserver-cdc-order-" + downstream,
Collections.singletonList(topic)));
顺序状态会记录:
text
startLsn
seqval
commandId
eventTs
kafkaOffset
新事件到达时与已保存状态比较,旧事件可以被识别并过滤。这里的状态也必须随 Checkpoint 保存,否则重启后保序逻辑会失忆。
十一、Source-to-Kafka 与 Kafka-to-Sink 的语义不要混淆
text
Source → Kafka
Exactly Once(在配置与 Checkpoint 正确的条件下)
Kafka → JDBC Target
At-least-once + target-side idempotence
Kafka 中没有重复,不代表 JDBC 目标端不会因为 Checkpoint 恢复而重复执行。每一段都要单独分析提交点。
十二、Kafka 模块验收清单
- Topic 创建和元数据重试经过验证
- Partition 数与 Source / Consumer 并行度匹配
- Kafka Key 对同一业务主键保持稳定
- Delete 事件可以生成正确 Key
- Transactional ID 前缀稳定且跨任务唯一
- Kafka 事务超时覆盖最坏 Checkpoint 时长
- 每个目标端使用独立 Consumer Group
- Startup Mode 和首次启动策略明确
- SQLServer 顺序状态可随 Checkpoint 恢复
- Lag、失败事务和消息重试可观测
十三、小结
Kafka 层真正增加的不是一个 Topic,而是一套必须长期稳定的数据契约和状态边界。
下一篇将转向数据之外的控制面:Pipeline YAML、资源配置、Postgres 映射表和 Redis 元数据如何共同驱动一条链路,以及配置错误为什么比代码异常更容易造成静默丢数。