周一早会,运营拿着两份报表问老张:“实时大屏 GMV 1.2 亿,离线报表 1.15 亿,差 500 万,信哪个?”
老张没慌。他打开对账系统,看了一眼 ODS 层差异率——0.01%,正常。再看 DWD 层——0.5%,超阈值了。他心里有数了:大概率是迟到数据导致的,流计算链路的 Watermark 拦掉了一部分,批计算全量重算没丢。“给我两小时,数据修完回复你。”
两小时后,差异率压到了 0.05%,两份报表对上了。运营那边没再追问,业务照常运转。
这就是对账体系的价值——它让你在问题发生前就发现,在被问到时能回答,在影响业务前修复。 今天就把这套体系拆开讲透。
一、架构统一了,为什么流批结果还会不一致?
湖仓一体架构升级之后,ODS 到 DWD 用的都是同一套 Flink SQL,存储也是同一张 Paimon 表。代码统一了,存储统一了,为什么流式增量计算和批量全量计算的结果还会有偏差?
答案很简单:湖仓一体解决了“两套代码、两套存储”的问题,但解决不了流计算和批计算是两种截然不同的计算模式这个事实。 它们的差异是客观存在的,不是 Bug,不是配置问题,是计算模式的天然属性。
1. 迟到数据:一个有时差,一个上帝视角
流计算是数据来一条算一条。它不知道下一条数据什么时候来,更不知道还有没有数据会来。所以它依赖 Watermark 机制,当一个窗口的 Watermark 超过窗口结束时间时,这个窗口就关闭了,之后再来属于这个窗口的数据,就是“迟到数据”。
迟到数据怎么处理?要么丢弃,要么写入旁路,但不会更新已经输出的窗口结果。
批计算不一样。T+1 全量计算时,所有数据都已经安安静静躺在 ODS 表里了,不存在“迟到”这个概念。批计算拥有上帝视角,每一笔订单都会被算进去,不管你是什么时候产生的。
差异体现:流计算的 GMV 可能略低于批计算,差额就是那些被 Watermark 挡在门外的迟到数据。这不是谁错了,是两种计算模式对“完整性”的定义不同。
2. 状态恢复:流计算为了快付出的代价
Flink 的 Checkpoint 机制为了保证高性能,采用的是异步快照。作业在运行过程中定期把状态备份到 HDFS,但这个备份不是实时的——两次 Checkpoint 之间处理的数据,状态只存在于内存里。
如果作业在这期间故障了,从最近一次 Checkpoint 恢复时,这两次 Checkpoint 之间处理的那部分数据,状态就丢了。即使配置了 Exactly-Once 语义,也只是保证状态不重复、不丢失已提交的部分,无法保证故障瞬间正在处理的那批数据不丢。
这不是 Bug,不是配置问题,是流计算为了低延迟和高吞吐所做的取舍。 差异极小,通常只有万分之几,但它确实存在。
3. 维表版本:不是做不到,是故意不这么做
流计算关联的维表是实时最新的。比如订单金额计算时,关联的商品价格是 Redis 里此时此刻的价格,没问题。
批计算呢?它处理的是昨天甚至上周的历史数据。如果每条历史订单都去跨网络查 Redis 拿当时的商品价格,性能会差到不可接受。而且批计算有一个隐含要求——结果必须是可重复的。如果你用实时维表,今天跑一遍和明天跑一遍,维表变了,结果也变了,对账就无从谈起。
所以批计算选择用 T-1 的维表快照做本地 Join。快照存在 Hive 或 Iceberg 里,批作业直接读,不需要跨网络,性能极高,而且任何时间重跑结果都一样。
代价是:同一笔订单,流计算按最新价格算,批计算按 T-1 价格算,金额对不上。这不是技术缺陷,是用维表的时效性换计算性能和结果可重复性,是有意为之的权衡。
4. 去重逻辑:流只能保证一段时间,批能看到全部
流计算做去重,只能依赖状态。但状态不能无限增长,必须设 TTL。TTL 一设,就意味着流计算只能保证“一段时间内不重复”——过了 TTL 再来一条相同主键的数据,Flink 会认为它是新数据,不去重。
批计算没有这个限制。它做全量去重时,能看到整个数据集,主键相同的记录一个都跑不掉。
差异体现:流计算可能有微量重复,批计算完全去重。
四种差异一览表
| 差异类型 |
流计算 |
批计算 |
差异体现 |
| 迟到数据 |
Watermark 超时后丢弃 |
全量计算,不丢 |
流 GMV 略低 |
| 状态恢复 |
两次 Checkpoint 间可能丢 |
无状态,不丢 |
微量丢失(万分之几) |
| 维表版本 |
实时最新 |
T-1 快照 |
价格可能不同 |
| 去重逻辑 |
TTL 内去重 |
全局去重 |
流可能有微量重复 |
二、对账的第一道防线——ODS 层对账
理解了流批差异之后,对账这事儿就有了清晰的起点:ODS 层。
在湖仓一体架构下,ODS 层就是同一张 Paimon 表。流计算往里写,批计算从里面读。对账要做的,是验证流计算视角下的“当前状态”和批计算视角下的“历史快照”,两者看到的数据是否一致。
为什么 ODS 层对账是基准?
因为它是流批两条计算链路的唯一公共源头。
- 如果 ODS 层对不齐 → 采集链路出了问题(CDC 丢了数据、Kafka 积压溢出、Flink CDC 解析错误)→ 后续 DWD、ADS 的对账都失去意义。
- 如果 ODS 层对得齐,但 DWD 层对不齐 → 问题一定出在加工逻辑上,排查范围直接缩小。
怎么对?——利用 Paimon 的时间旅行
Paimon 每次 Checkpoint 都会生成一个快照,记录表在那个时刻的完整状态。批计算每天凌晨产出 T-1 的全量数据,本质上就是读取 ODS 表在凌晨时刻的快照,然后按分区组织好。
-- 批计算视角:T日分区的数据统计
SELECT COUNT(*), SUM(amount)
FROM ods_orders_batch
WHERE dt = '2026-01-15';
-- 流计算视角:T日凌晨快照的数据统计(通过时间旅行)
SELECT COUNT(*), SUM(amount)
FROM ods_orders
/*+ OPTIONS('scan.timestamp-millis' = '1705253400000') */;
两个数往一块一放,差异率就出来了。
差异怎么处理? 当差异率 < 0.1% 时只要记录一下就行,通常是边界时间戳的微小偏差;差异率 > 0.1% 时,需要进行采集链路排查,检查 CDC Lag、Kafka 积压、Flink 作业状态。必要时从 MySQL Binlog 重新灌入受影响时段的 ODS 数据。
ODS 对账的局限
它只能发现“量级”差异——总数对不对、总金额对不对,没法发现“单条内容”的差异。比如总数都是 100 万条,但其中有几条订单的金额字段错了,ODS 对账看不出来。细粒度的对账要下沉到 DWD 层。
三、对账的第二道防线——DWD 层对账
DWD 层是业务逻辑最集中的地方。清洗、维度关联、多流 Join、去重,全在这一层完成。任何一个环节出问题,都会传导到下游的 ADS 层,最终被业务方看到。
DWD 层对账要精确到“单条数据的内容差异”。方法很直接:FULL OUTER JOIN 逐行对比。
流计算视角取 DWD 表在 T 日凌晨的快照,通过 Paimon 的时间旅行功能指定时间戳就能拿到。批计算视角取 DWD 表的 T 日全量重算结果,按分区组织好。然后按业务主键——比如订单 ID——做 FULL OUTER JOIN。
JOIN 之后会冒出三类差异。
- 第一类:流计算有,批计算没有。 这种情况通常就是迟到数据。流计算在 Watermark 超时前收到了这条订单,把它算进去了;批计算因为 T+1 统计口径的原因没包含它。这类差异是可解释的,记录下来就行,不需要修。
- 第二类:批计算有,流计算没有。 这类要重视了。可能是流计算链路丢了数据——Checkpoint 回滚导致微量丢失,或者状态 TTL 过期导致多流 Join 时没关联上。这类差异需要重点排查 Flink 作业,确认根因后评估是否要重跑修复。
- 第三类:两边都有,但字段值不同。 这是清洗逻辑或维表版本不一致导致的。比如流计算关联了实时维表,批计算关联了 T-1 维表快照,同一笔订单的金额字段对不上。这类差异要评估业务影响——如果业务方能接受这个差异,比如商品价格变化不频繁,记录下来即可;如果不能接受,就要考虑修复。
DWD 对账的自动化
每天凌晨批计算 DWD 跑完后,自动触发对账任务:
- 差异率超过阈值(如 1%)→ 自动往钉钉群发告警,附带差异明细。
- 差异明细本身就是重跑修复的输入范围——哪些订单需要重算,一目了然。
四、对账的第三道防线——ADS 层对账
ODS 对得齐,DWD 也对得齐,是不是就高枕无忧了?还没完。ADS 层是直接面向业务的指标层,这里还有最后一道坎。
ADS 层的特殊之处
流计算和批计算的聚合粒度、更新方式完全不同。流计算 ADS 是分钟级增量更新——比如 StarRocks 的物化视图每 1 分钟刷新一次。批计算 ADS 是每天凌晨全量覆盖。两者连刷新节奏都不一样,不能像 DWD 层那样逐行对比。
ADS 层对账要按“指标维度”来。
- 先定义核心指标:GMV、订单量、UV、退款额。
- 再定义对比维度:按小时、按店铺、按商品品类。
- 然后批计算 ADS 表和流计算 ADS 表按相同维度聚合后对比。
由于迟到数据和聚合粒度的天然差异,ADS 层对账允许一定的误差,通常在 0.5% 到 1% 之间。
- 小于阈值 → 正常,记录日志就行。
- 大于阈值 → 说明 DWD 层或 ODS 层的问题已经传导上来了,需要向上溯源:ADS 出问题看 DWD,DWD 出问题看 ODS,一层一层往下追。
ADS 层本身一般不直接修复,因为它是聚合结果,修复了也治标不治本。正确做法是修复 DWD 层的明细数据后,等流计算的增量更新自然传导上来,或者手动触发一次 ADS 物化视图的全量刷新。
三层对账合在一起,形成了一条漏斗式的排查链路:ADS 发现问题 → DWD 定位加工原因 → ODS 确认源头是否干净。每一层都有它的职责,缺一层都不完整。
五、如何把对账做成自动化运营?
对账不是一次性项目,是一个持续运行的自动化体系。这套体系应该长什么样?
设计一个调度触发流程。比如批计算任务每天凌晨 3 点跑完,让对账任务凌晨 4 点自动触发。ODS 对账先跑,跑完跑 DWD,最后跑 ADS,按依赖关系串行执行。并将对账的结果写入一张专门的日志表,记录每次对账的时间、差异率、差异明细,方便后续追溯。
建立阈值与告警分级。当差异率小于 0.1%,INFO 级别,记日志就行。0.1% 到 1% 之间,WARN 级别,钉钉或企业微信告警,需要人工确认。超过 1%,ERROR 级别,电话告警,触发自动修复流程。
告警触发后,第一步确认差异范围:哪张表、哪个时间段、哪些主键。第二步判断根因:ODS 层还是 DWD 层?采集链路还是加工逻辑?第三步触发修复:DWD 层问题启动重跑流程,ODS 层问题回溯采集链路。第四步修复完成后重新对账,确认差异消除。
这套体系跑起来之后,你应该能回答三个问题:平均发现时长——从口径不一致发生到对账发现,目标是 24 小时以内;平均修复时长——从发现问题到数据修复完成,目标是 2 小时以内;对账覆盖率——核心业务表 100% 覆盖,如果是非核心表可以降级。
写在最后
老张在团队复盘会上说了一段话:
“架构统一只是第一步,对账才是让你敢拍胸脯说‘数据没问题’的底气。流批差异客观存在迟到数据、状态恢复、维表快照、去重语义的问题,这四样东西你绕不开。对账把它们从黑盒变成白盒,量化它、管理它、兜底它。你知道差在哪、差多少、能不能接受,心里就有底了。”
对账不是找茬,是建立信心。它让你敢在流计算链路上快速迭代,因为你知道不管代码怎么改,第二天凌晨的对账会告诉你结果对不对。ODS、DWD、ADS 三层对账各司其职,加上自动化的调度告警和修复联动,才是实时数仓数据质量的完整闭环。