订阅更新

实时大屏和离线日报数对不上,该信谁——实时报表怎么做才靠谱?

数据报表手册 第 11 题 约 7 分钟读完

本文目录
  1. 先说结论
  2. 差异的四个来源
  3. 实时链路的正确姿势:以投放实时监控为例
  4. 双链路怎么“和平共处”
  5. 面试高频追问
  6. 实战踩坑:状态TTL把UV“去重”去没了
  7. 总结

先说结论

实时和离线有差异是常态,不是事故;目标不是“完全一致”,而是“差异可解释、有唯一裁决标准”。行业通行做法:实时数据用于盯趋势、做告警,离线数据是结算和汇报的最终口径,T+1由离线覆盖修正。

要缩小差异,先搞清楚差异的四个来源:统计时点不同、迟到数据处理不同、去重精度不同、两套代码口径漂移。每一项都有对应的收敛手段。

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

详细讲解

上了实时报表的团队都会在某一天遇到灵魂拷问:昨晚大屏上显示GMV是102万,今早离线日报说是98万,老板问“到底哪个是真的”。这道题面试官考两层:浅层是你懂不懂实时链路的技术,深层是你能不能设计一套让两条链路“和平共处”的机制。我在某元宇宙社交应用从0搭过Flink+Doris的实时平台,在某短剧出海公司用Flink+Hologres做投放实时监控,这个问题两边都正面撞上过。

差异的四个来源

来源具体表现收敛手段
统计时点实时是“截至此刻”,离线是全天终值实时大屏标注数据时点
迟到数据实时按事件时间开窗有watermark截断,晚到被丢或归错允许更新的sink+晚到侧输出
去重精度实时去重受状态TTL/近似算法限制关键UV用精确去重(Bitmap)
口径漂移实时Flink SQL和离线SQL两套代码各自演进口径变更双链路同步上线

四个来源里最被低估的是第四个。上线时两边明明对齐了,三个月后差异越来越大——多半是离线侧改了过滤条件(比如新增了剔除某类测试流量),实时侧没人同步改。我们后来立了规矩:任何指标口径变更,实时离线两个任务必须在同一个发布单里一起改,缺一个不准上线。

实时链路的正确姿势:以投放实时监控为例

在短剧出海公司,投放同学需要盯着“今日各渠道消耗和安装”的准实时曲线,用来当天砍掉劣质计划。链路是Kafka→Flink→Hologres→BI:

-- Flink SQL:按事件时间开窗聚合,写入Hologres
INSERT INTO holo_ad_realtime_stat
SELECT
  DATE_FORMAT(window_start, 'yyyy-MM-dd HH:mm') AS stat_minute,
  channel,
  SUM(cost)                  AS cost,
  COUNT(DISTINCT device_id)  AS install_cnt
FROM TABLE(TUMBLE(TABLE dwd_ad_event,
     DESCRIPTOR(event_time), INTERVAL '1' MINUTE))
GROUP BY window_start, channel;
 
-- 两个关键设计:
-- 1. sink表主键=分钟+渠道,Flink写入是UPSERT:
--    failover重放时覆盖而不是累加,天然幂等
-- 2. watermark设5分钟:迟到超5分钟的数据进侧输出流
--    单独落表,离线链路会包含它们(这就是差异的一部分)

UPSERT幂等这个设计怎么强调都不过分:实时链路一定会failover,重放时如果sink是append-only的累加逻辑,数字就会虚增,而且是悄悄虚增。用“主键覆盖”代替“来一条加一条”,是实时报表稳定性的第一原则。

双链路怎么“和平共处”

① 大屏标注时点和口径。“数据截至14:32,实时口径与日报差异说明见帮助”——一行小字省一半解释成本。

② T+1离线覆盖。实时表只保留当天和昨天,前天以前的数据由离线任务回填覆盖,历史查询自动走的是离线口径,两套数字不会长期并存。

③ 每日自动对账。每天凌晨离线跑完后,自动比对昨天全天的实时汇总和离线终值,差异率写进监控:

-- 实时vs离线每日对账任务
SELECT r.stat_date, r.channel,
       r.install_cnt  AS rt_cnt,
       o.install_cnt  AS offline_cnt,
       ROUND(ABS(r.install_cnt - o.install_cnt)
             * 1.0 / o.install_cnt, 4) AS diff_ratio
FROM dws_ad_rt_day_agg r
JOIN ads_ad_stat_1d    o
  ON r.stat_date = o.stat_date AND r.channel = o.channel
WHERE r.stat_date = '${bizdate}'
  AND ABS(r.install_cnt - o.install_cnt) * 1.0 / o.install_cnt > 0.01;
-- 差异率>1%告警。差异突然放大=某条链路出问题的领先指标

这个对账任务的价值远超“发现差异”本身:差异率长期稳定在0.5%,突然有一天跳到5%,不用等业务方发现,你就知道某条链路出问题了——它是两条链路互为对方的监控。

面试高频追问

追问一:业务方要求实时和离线必须完全一致,怎么回应?

先讲清楚代价再给方案。完全一致意味着:实时侧做精确去重(大状态)、无限等迟到数据(高延迟)、两套代码逐字对齐(高维护成本),本质是把实时做成一个跑得快的离线,成本翻几倍收益趋近于零。我会反问业务场景:你用实时数据做什么决策?盯趋势砍计划,1%的差异完全不影响判断。真正需要精确的场景(结算、对外披露)本来就该用离线终值。把“差异率SLA”明确下来(比如承诺日终差异<1%),比承诺“完全一致”专业得多。如果团队技术栈允许,流批一体(一套代码两种执行模式)是从根上消除口径漂移的方向,但那是架构演进,不是当下承诺。

追问二:Flink作业重启后,大屏数字会不会丢或者重复?

取决于三件事的组合:Checkpoint、Source可回放、Sink幂等或事务。标准配置是:

# 实时报表作业的容错三件套配置
execution.checkpointing.interval: 60s        # 1分钟一次Checkpoint
execution.checkpointing.mode: EXACTLY_ONCE   # 状态一致性语义
state.backend: rocksdb                        # 大状态用RocksDB
 
# Source:Kafka位点存进Checkpoint,重启从位点重放
# Sink:Hologres/Doris用主键UPSERT,重放覆盖不累加
# 组合效果:重启表现为数字停滞1-2分钟后追上,不丢不重

如果面试官继续往下钻,就聊端到端Exactly-Once的边界:写下游再发通知这类副作用是没法回滚的,所以设计上尽量让“可重放+幂等”兜底,而不是迷信事务。

追问三:实时报表的成本比离线高很多,怎么控制?

三个点。第一,准入评估:每个实时需求先问“晚一小时会损失什么”,答不上来的降级成小时级微批——微批的成本可能只有纯实时的几分之一,而大多数“实时需求”实际是“别让我等到明天”的需求。第二,粒度控制:实时聚合到分钟×核心维度就够了,不要在实时链路里保留明细级下钻,明细分析引到离线。第三,资源复用:多个实时指标合并进同一个Flink作业共享状态和资源,而不是一个指标一个作业。

实战踩坑:状态TTL把UV“去重”去没了

在元宇宙社交应用踩过一个很典型的坑。大屏有个“今日活跃用户数”,Flink里用状态做精确去重。上线初期没问题,随着用户量涨,状态越来越大,有人给作业加了状态TTL=2小时来控制内存。结果是:用户早上活跃过、中午再来,状态已过期,被当成新用户再计一次——当日UV虚高,而且活跃越分散虚得越多,离线对账每天差8%-10%。定位花了不少功夫,因为TTL是别人加的,变更记录里只写了“优化内存占用”。修复方案:改用Doris的Bitmap聚合表做去重(Flink只写明细,去重下沉到OLAP层),Flink状态只保留轻量聚合。这也成了我后来的一个架构习惯:精确去重这种大状态计算,能下沉到存储引擎就不要扛在流计算的状态里。

教训:实时链路的任何“性能优化”都可能是口径变更——TTL、近似算法、提前触发,动之前必须评估对数字准确性的影响,并且让对账任务替你盯着。

总结

实时报表的靠谱=机制靠谱:UPSERT幂等写入防重放虚增,watermark和迟到策略明确,T+1离线覆盖统一历史口径,每日自动对账让差异可见可解释,大屏标注时点管理预期。技术上记住两条铁律:sink幂等是第一原则,大状态去重下沉到OLAP。面试时先说“差异是常态、离线是终审”这个机制设计,再讲技术细节,层次立刻就出来了。

下一篇聊自助分析:业务方说“我不想每次都找你,我要自己拖着看”——千万级数据的自助多维分析怎么支撑?

你们的实时和离线每天差多少?有对账任务吗?评论区聊聊~

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

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