Skip to content

端到端 Exactly-Once 边界怎么验收 ​

难度与前置

难度:架构
前置:理解「组件 EOS ≠ 业务不重不丢」;接触过 Kafka 事务 / EOS、Checkpoint。
读完能做什么:画出链路边界、标明每段语义,设计故障注入与对账验收,避免面试背名词、上线却对不齐。

适用场景 ​

  • 实时入仓 / 入湖要宣称「不丢不重」,需要可演示的验收
  • 多跳:采集 → Kafka → Flink → Kafka/仓 → ADS
  • 评审质疑「开了 exactly-once 为什么还有重复」

不适用:

  • 只想了解 Kafka 事务原理细节(见 Kafka EOS)
  • 纯离线分区重跑可覆盖(优先 幂等回刷)
  • 下游是不可控第三方 API 且无法幂等(只能 at-least-once + 对账补偿)

真实场景(一条) ​

链路:客户端 → Kafka raw → Flink 清洗 → Kafka dwd → Flink 入仓 → Paimon/Hive 分区表。曾宣称「Flink EOS」,failover 后 ADS UV 仍冲高。排查:入仓 Sink 在 CK 之外二次重放且表无主键去重;另一消费者手工提交位移。改造:明确边界图;入仓按 event_id 幂等;用「杀 TaskManager + 重复生产」演练;对账 COUNT(*) vs COUNT(DISTINCT event_id) 与离线一致。重复演练偏差 < 0.05%。

怎么做(步骤清单) ​

1. 先画边界图(一页纸) ​

把链路拆成段,每段只标一种语义:

text
[采集] at-least-once
   → [Kafka raw] 持久化(本身不消除业务重复)
   → [Flink A] EOS(source-sink 对齐) 边界:raw → dwd topic
   → [Kafka dwd]
   → [Flink B] EOS 边界:dwd → 仓表(Sink 需两阶段/幂等)
   → [仓表] 主键/分区幂等(业务 Exactly-Once 最后一道闸)

口诀:组件保证写在段上;业务不重写在出口幂等与对账上。概念细节不在此文展开,见 Kafka EOS。

2. 声明「保证什么 / 不保证什么」 ​

声明可验收含义常见误读
段内 EOS故障恢复后该段输出在语义上不重复不丢不等于全链路
端到端业务不重同一 event_id 在出口表只生效一次不等于中间 topic 无重复行
端到端不丢源成功采集的事件最终可查不等于延迟为 0
恰好一次可见外部系统与 CK 原子未参与协议的 API 除外

把该表贴进架构文档,面试与值班同款。

3. 出口幂等模式(选一种写清) ​

模式做法验收点
主键 / Upsert主键 = event_id重复写入行数不增
分区覆盖按 dt/hour 可重跑重跑结果稳定
去重状态Flink 状态 TTL 去重状态与 TTL 策略可解释
仓外表事务Iceberg/Paimon 等提交对齐快照可见性与 CK 一致
sql
-- 出口表幂等示例(Upsert 引擎示意)
-- PRIMARY KEY (event_id)
-- 重复消费后:
SELECT COUNT(*) AS cnt, COUNT(DISTINCT event_id) AS dc
FROM dwd_click_rt
WHERE dt = '${biz_date}';
-- 期望 cnt = dc(在去重表语义下)

4. 端到端验收四件套(必须可重复) ​

  1. 重复注入:对同一批 event_id 再写 Kafka 一次
  2. 失败注入:杀 TM / 重启 Job / 制造 CK 失败后恢复
  3. 延迟注入:暂停 Sink 再恢复,观察是否重复提交
  4. 对账:与源计数 / 离线链路比
bash
# 1) 重复注入示意
# cat sample.jsonl | kafka-console-producer ... --topic click_raw
# 再执行一次相同文件

# 2) 失败注入
# kubectl delete pod flink-tm-xxx   # 或 yarn kill container

# 3) 等待 job running + lag 回落
# kafka-consumer-groups.sh --describe --group flink_click

5. 对账 SQL / 指标(链路级) ​

sql
-- A. 源与仓(按 event_id)
SELECT
  s.cnt AS src_cnt,
  d.cnt AS dwd_cnt,
  s.cnt - d.cnt AS diff
FROM (
  SELECT COUNT(DISTINCT event_id) AS cnt FROM ods_click_raw WHERE dt='${biz_date}'
) s,
(
  SELECT COUNT(DISTINCT event_id) AS cnt FROM dwd_click_rt WHERE dt='${biz_date}'
) d;

-- B. 重复率
SELECT
  1 - COUNT(DISTINCT event_id) / COUNT(*) AS dup_rate
FROM kafka_dwd_snapshot_or_stage
WHERE dt = '${biz_date}';

-- C. 与离线
SELECT
  SUM(CASE WHEN r.event_id IS NULL THEN 1 ELSE 0 END) AS missing_in_rt,
  SUM(CASE WHEN o.event_id IS NULL THEN 1 ELSE 0 END) AS extra_in_rt
FROM offline_click o
FULL OUTER JOIN dwd_click_rt r ON o.event_id = r.event_id
WHERE COALESCE(o.dt, r.dt) = '${biz_date}';

阈值示例:金额类 diff_rate < 0.01%;UV 类按业务签收。

6. 「假 EOS」排查清单 ​

现象可能断点
中间 topic 有重复正常(段内可能 at-least-once 可见);看出口
出口 cnt > dcSink 未幂等或去重键错
failover 后暴增两阶段提交未对齐 / 手工提交位移
多消费者一组乱另一组用不同语义写同一表
仅高峰重复超时重试无幂等键

结合 乱序与重复、链路延迟排查。

7. 发布与值班要求 ​

  • 架构图带语义标注,否则不准说「我们是 exactly-once」
  • 演练每年/每重大版本至少一次,结果贴到变更单
  • 告警:对账 diff、Sink 提交失败、CK 连续失败
  • 回滚:停写 → 修幂等 → 补数(补数也要幂等)

8. 与组件文如何分工 ​

问题去哪篇
Kafka 事务参数、幂等 ProducerKafka EOS
CK 对齐、FailoverCheckpoint
本篇边界声明 + 注入演练 + 对账验收

常见坑(至少牢记这些) ​

  1. 把「Flink 开了 EOS」当成业务不重
    出口表无主键时,中间再正确也会重。

  2. 只验 happy path
    不杀进程、不重复注入,上线首轮故障必翻车。

  3. 对账用 COUNT(*) 对 Kafka 消息数
    未去重的中间层与业务计数不可比;应对 event_id。

  4. 多作业写同一张表各说各的 EOS
    无全局互斥与统一幂等键,结果不可解释。

  5. 忽略时间边界
    「不丢」是最终一致;SLA 内延迟要用 新鲜度 另验。

  6. 演练数据污染生产口径
    应用隔离前缀 / 专用表,或只在预发做注入。

如何验收(可量化) ​

  • [ ] 边界图已评审,每段语义有主责人
  • [ ] 重复注入后出口 cnt = count(distinct business_key)
  • [ ] 杀 TM / 重启后对账 diff 在阈值内
  • [ ] 与离线/源的 diff 连续 N 天达标
  • [ ] 负向:关闭幂等时能复现重复(证明测到了真问题)
  • [ ] 文档含回滚与补数步骤
bash
echo "run_id,inject,dup_rate,src_vs_dwd_diff,ck_fail_recover"
# 20260920-1,replay_same_ids,0.0000,0,ok
# 20260920-2,kill_tm,0.0001,3,ok
sql
-- 最终签字查询(示例)
SELECT COUNT(*) AS cnt, COUNT(DISTINCT event_id) AS dc
FROM dwd_click_rt
WHERE dt = '${biz_date}';

相关面试题 ​

题库小程序链接待定;先用站内锚点占位。

  • [ ] 题:端到端正好一次验收前,边界图要评审什么?(考察点:边界图已评审,每段语义有主责人)→ 占位:#todo-interview-e2e-eos-boundary-map
  • [ ] 题:重复注入后出口怎样证明无业务键翻倍?(考察点:cnt = count(distinct business_key))→ 占位:#todo-interview-e2e-eos-inject-dedup
  • [ ] 题:杀 TM / 重启后对账怎样算过?(考察点:diff 在阈值内;与离线/源连续 N 天达标)→ 占位:#todo-interview-e2e-eos-failover-recon
  • [ ] 题:为什么要做「关闭幂等能复现重复」的负向实验?(考察点:证明测到了真问题,非整条链路虚假绿)→ 占位:#todo-interview-e2e-eos-negative-test

改写说明:结构与判断标准参考 Wood 原创数仓/面试文,已改写成可执行清单;侧重链路验收,不与 kafka/exactly-once-eos 重复堆概念;未包含未公开公司数据与内部表名。