外观
端到端 Exactly-Once 边界怎么验收
难度与前置
难度:架构
前置:理解「组件 EOS ≠ 业务不重不丢」;接触过 Kafka 事务 / EOS、Checkpoint。
读完能做什么:画出链路边界、标明每段语义,设计故障注入与对账验收,避免面试背名词、上线却对不齐。
适用场景
- 实时入仓 / 入湖要宣称「不丢不重」,需要可演示的验收
- 多跳:采集 → Kafka → Flink → Kafka/仓 → ADS
- 评审质疑「开了 exactly-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. 端到端验收四件套(必须可重复)
- 重复注入:对同一批
event_id再写 Kafka 一次 - 失败注入:杀 TM / 重启 Job / 制造 CK 失败后恢复
- 延迟注入:暂停 Sink 再恢复,观察是否重复提交
- 对账:与源计数 / 离线链路比
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_click5. 对账 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 > dc | Sink 未幂等或去重键错 |
| failover 后暴增 | 两阶段提交未对齐 / 手工提交位移 |
| 多消费者一组乱 | 另一组用不同语义写同一表 |
| 仅高峰重复 | 超时重试无幂等键 |
7. 发布与值班要求
- 架构图带语义标注,否则不准说「我们是 exactly-once」
- 演练每年/每重大版本至少一次,结果贴到变更单
- 告警:对账 diff、Sink 提交失败、CK 连续失败
- 回滚:停写 → 修幂等 → 补数(补数也要幂等)
8. 与组件文如何分工
| 问题 | 去哪篇 |
|---|---|
| Kafka 事务参数、幂等 Producer | Kafka EOS |
| CK 对齐、Failover | Checkpoint |
| 本篇 | 边界声明 + 注入演练 + 对账验收 |
常见坑(至少牢记这些)
把「Flink 开了 EOS」当成业务不重
出口表无主键时,中间再正确也会重。只验 happy path
不杀进程、不重复注入,上线首轮故障必翻车。对账用 COUNT(*) 对 Kafka 消息数
未去重的中间层与业务计数不可比;应对event_id。多作业写同一张表各说各的 EOS
无全局互斥与统一幂等键,结果不可解释。忽略时间边界
「不丢」是最终一致;SLA 内延迟要用 新鲜度 另验。演练数据污染生产口径
应用隔离前缀 / 专用表,或只在预发做注入。
如何验收(可量化)
- [ ] 边界图已评审,每段语义有主责人
- [ ] 重复注入后出口
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,oksql
-- 最终签字查询(示例)
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 重复堆概念;未包含未公开公司数据与内部表名。