订阅更新

实时数仓和离线数仓数据对不上——流批对账(Reconciliation)框架怎么搭?

数据质量手册 第 7 题 约 9 分钟读完

本文目录
  1. 先说结论
  2. 根因一:时间口径差异(最常见)
  3. 根因二:Late Data(迟到数据)处理不一致
  4. 根因三:去重策略不同
  5. 对账框架的核心架构
  6. 差异定位下钻
  7. 面试高频追问
  8. 实战踩坑:一次「永远对不齐」的对账
  9. 总结

先说结论

流批对不上的5大根因:时间口径差异、Late Data处理、去重策略不同、数据源不一致、状态TTL截断。每一类都需要专门的对账机制。

对账框架的核心组件三件套:①时间对齐机制 ②差异检测SQL ③差异定位与修复SOP。三件套缺一不可,否则对账就是「报告差异但不知道怎么解决」。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

Lambda架构(流+批)几乎是大厂标配,但「流和批数据对不上」也几乎是必踩的坑。同一个指标,离线T+1算出来1000万,实时算出来980万,业务方一问哑口无言。这道题在中高级面试里非常加分,因为它考的是你对Exactly-Once、时间窗口、幂等性的理解。我在某元宇宙社交应用搭过一套流批对账框架,今天讲清楚怎么做。

根因一:时间口径差异(最常见)

流式按Event Time(事件时间)聚合,离线按Process Time(处理时间)聚合,必然对不上。举个例子:

-- 离线:按dt(处理日期)聚合
INSERT OVERWRITE TABLE ads_dau PARTITION(dt='${bizdate}')
SELECT COUNT(DISTINCT user_id) AS dau
FROM dwd_user_login
WHERE dt = '${bizdate}';  -- 这里dt是「数据落库当天」
 
-- 实时:按event_time聚合
SELECT TUMBLE_START(event_time, INTERVAL '1' DAY) AS day,
       COUNT(DISTINCT user_id) AS dau
FROM kafka_user_login
GROUP BY TUMBLE(event_time, INTERVAL '1' DAY);  -- 这里是事件时间
 
-- 不一致来源:12月1号23:59发生的登录、12月2号才入库
-- 离线算到12月2号,实时算到12月1号

解决方案:流批必须统一用Event Time。离线SQL改成`WHERE event_time BETWEEN ...`而不是`WHERE dt = ...`。这是治本的方法。

根因二:Late Data(迟到数据)处理不一致

12月1号晚上发生的事件,由于网络延迟2号才到。流式有Watermark机制,超过阈值就丢弃;离线T+1全量重算,自然包含。结果就是:

-- 流式(Flink)
SELECT TUMBLE_START(event_time, INTERVAL '1' HOUR) AS hr,
       COUNT(*) AS cnt
FROM source
GROUP BY TUMBLE(event_time, INTERVAL '1' HOUR);
-- Watermark设为延迟5分钟,5分钟后才到的数据被丢弃
 
-- 离线(Hive/Spark)
-- T+1全量算,所有迟到数据都包含进来
SELECT DATE_TRUNC('hour', event_time), COUNT(*)
FROM ods_event WHERE dt = '20241201'
GROUP BY DATE_TRUNC('hour', event_time);

两种应对方案:①接受差异,给业务方明确说明流式有X%截断率;②流式用Allow Lateness + 侧输出流(Side Output)补迟到数据回写下游。我倾向于第一种——把误差范围明确告诉业务,比追求100%一致但代价巨大要务实。

根因三:去重策略不同

流式经常用Flink的Dedup算子+TTL(比如保留24小时);离线一般用ROW_NUMBER全量去重。同样的输入数据,去重结果可能差很多。

-- 流式去重:Flink Dedup + 24小时TTL
-- 第25小时来的重复数据 → 状态已过期,会被认为是「新数据」
 
-- 离线去重:ROW_NUMBER全量
SELECT * FROM (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY id ORDER BY ts DESC) rn
  FROM source
) WHERE rn = 1;  -- 全表去重,不受时间窗口限制
 
-- 结果:流式认为「重复」的,离线可能认为是新数据,反之亦然

对账框架的核心架构

组件职责技术实现
指标产出层流批分别计算同指标Flink (实时) + MaxCompute (离线)
对账存储层存储两侧产出指标值对账明细表 + 元数据表
对账执行层T+1调度对账SQLDataWorks调度 / Airflow
差异检测层判断超阈值差异DQC规则 + 自定义SQL
告警通知层推送差异报告钉钉/邮件/对账Dashboard
定位修复层差异下钻和修复SOP明细对账 + 修复脚本
-- 对账明细表设计
CREATE TABLE dq_reconciliation (
  dt STRING,                  -- 业务日期
  metric_name STRING,         -- 指标名
  online_value DOUBLE,        -- 实时值
  offline_value DOUBLE,       -- 离线值
  diff_abs DOUBLE,            -- 绝对差异
  diff_pct DOUBLE,            -- 相对差异 = abs(o-f)/f
  threshold DOUBLE,           -- 容忍阈值(如0.5%)
  status STRING,              -- PASS/WARN/FAIL
  detect_time TIMESTAMP
) PARTITIONED BY (dt);
 
-- T+1对账SQL
INSERT INTO dq_reconciliation
SELECT '${bizdate}', 'dau',
       o.dau AS online_value, f.dau AS offline_value,
       ABS(o.dau - f.dau), ABS(o.dau-f.dau)*1.0/f.dau,
       0.005,
       CASE WHEN ABS(o.dau-f.dau)*1.0/f.dau < 0.005 THEN 'PASS'
            WHEN ABS(o.dau-f.dau)*1.0/f.dau < 0.02 THEN 'WARN'
            ELSE 'FAIL' END,
       CURRENT_TIMESTAMP
FROM ads_dau_online o JOIN ads_dau_offline f ON o.dt = f.dt
WHERE o.dt = '${bizdate}';

差异定位下钻

对账报FAIL之后,关键是怎么定位差异在哪。我设计了一套「逐层下钻」的SQL模板:

-- Step 1: 按维度下钻,找差异最大的维度
SELECT country, 
       o.cnt AS online, f.cnt AS offline,
       o.cnt - f.cnt AS diff
FROM (SELECT country, COUNT(DISTINCT uid) cnt FROM ads_dau_online_detail GROUP BY country) o
JOIN (SELECT country, COUNT(DISTINCT uid) cnt FROM ads_dau_offline_detail GROUP BY country) f
  USING (country)
ORDER BY ABS(o.cnt-f.cnt) DESC LIMIT 10;
 
-- Step 2: 找出在线有但离线没有的用户(或反之)
SELECT uid, 'only_in_online' AS source
FROM ads_dau_online_detail o
LEFT JOIN ads_dau_offline_detail f USING (uid)
WHERE f.uid IS NULL
UNION ALL
SELECT uid, 'only_in_offline' FROM ads_dau_offline_detail f
LEFT JOIN ads_dau_online_detail o USING (uid)
WHERE o.uid IS NULL;

面试高频追问

追问一:对账阈值怎么定?

分级定阈值:核心财务指标(GMV、收入)<=0.1%;用户量级指标(DAU、UV)<=0.5%;行为类指标(点击、曝光)<=2%。阈值不是绝对值,而是基于业务可接受偏差。和业务方达成一致后写进数据契约里。

追问二:实时和离线必须100%一致吗?

不必要也不可能。Lambda架构本身就是「实时给近似值、离线给最终一致值」。能做到的是把差异控制在已知范围内。我在某元宇宙社交应用做的策略是:BI看板默认展示离线值(精确),实时大屏展示实时值(带「数据同步中」标注)。让用户知道哪个值是权威的,是设计上的关键。

追问三:Kappa架构能彻底解决流批不一致吗?

理论上能,因为Kappa只用流处理引擎,统一的代码、统一的状态、统一的口径。但代价是:①回溯历史数据成本高(要重放Kafka);②引擎对复杂SQL支持有限(窗口Join、CTE等);③对存储和算力要求高。所以现实里大厂大多还是Lambda+对账,纯Kappa很少见。Kappa更适合中小规模、SQL逻辑简单的场景。

实战踩坑:一次「永远对不齐」的对账

某元宇宙社交应用搭对账时,发现「虚拟礼物送出数」流式比离线少0.3%-0.5%,怎么调都对不齐。查了一周才找到根因:流式用的是Kafka消费的事件,离线用的是从业务库MySQL Binlog同步的订单表,两个数据源本身就不一致——有些礼物事件在Kafka被丢失(Producer配置问题),但业务库正常入库。

教训:对账之前必须先对齐数据源。如果两侧数据源根本不是一份数据的两个视角,对账永远不会准。后来我们改成:流式订阅业务库Binlog(CDC)作为唯一数据源,下游再分流到流批两个链路,差异立刻降到0.05%以下。

# 数据源对齐前后对比
# Before:
#   流式 ← Kafka业务事件流(前端SDK上报)
#   离线 ← MySQL Binlog(业务库)
#   差异:0.3%-0.5%(Kafka丢失部分事件)
# After:
#   流式 ← MySQL Binlog (CDC)
#   离线 ← MySQL Binlog (CDC)(同一份数据)
#   差异:<0.05%(仅剩Late Data差异)

总结

流批对不上的5大根因:时间口径、Late Data、去重策略、数据源、TTL。对账框架三件套:时间对齐+差异检测+定位修复。永远不要追求100%一致,要把差异控制在业务可接受范围内并明确说明。面试时按根因分类讲,再用一个数据源对齐的故事收尾,深度直接拉满。

下一篇聊一个跨系统对账的话题:业务库 ↔ 数仓 ↔ BI,三方数据对齐怎么做?

你们生产环境的流批差异控制在多少?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器