订阅更新

同一个DAU,离线1200万、实时1180万——双链路口径不一致怎么办?

指标管理手册 第 6 题 约 9 分钟读完

本文目录
  1. 先说结论
  2. 业务场景:双链路DAU差20万的归因
  3. 方法一:口径在字典层统一
  4. 方法二:实时去重State的设计
  5. 方法三:Diff监控+采样校验
  6. 方法四:实时校准+Lambda架构演进
  7. 面试高频追问
  8. 实战踩坑:State TTL设24h跨日丢了10%用户
  9. 总结

先说结论

双链路差异有4类来源:①数据源差异(实时走Kafka、离线走DB)②时间窗口差异(实时窗口 vs 离线天分区)③去重逻辑差异(实时State vs 离线DISTINCT)④口径定义差异(口径文档没统一)。要先归因再治理,不能笼统说"实时不准"。

治理核心是「以离线为基准、用实时做近实时增量」。北极星指标的最终值以离线为准,实时只用作分钟级监控和决策辅助。允许差异,但差异必须可解释、可追溯、有阈值。

健康的diff率应该<1%。我在元宇宙社交App把双链路DAU的diff率从初期的8%降到0.6%,靠的是字典统一+采样校验+异常告警三件套。

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

详细讲解

"实时和离线对不上"是Lambda架构和流批一体最经典的痛点。我在某元宇宙社交App从0搭实时数据平台时,第一个被业务追问的问题就是"为什么实时DAU比离线少20万"。这道题考的是你对实时和离线在数据采集、计算逻辑、状态管理上的差异理解——能答到流批口径治理这个层面就是Senior,只会说"实时数据少所以不准"就是初级。

业务场景:双链路DAU差20万的归因

元宇宙社交App上线初期,离线DAU是1200万,实时DAU是1180万,业务每天追着问"差的20万去哪了"。我用一周时间逐项排查,把20万拆成4个来源:

Markdown

## 双链路DAU diff归因(共差20万)
 
| 差异来源 | 数量 | 占比 | 根因 |
| --- | --- | --- | --- |
| Kafka丢失消息 | 8万 | 40% | 上游client端Kafka producer batch缓冲未发送 |
| 实时去重State过期 | 5万 | 25% | 跨日State TTL=24h,恰好过期掉一批 |
| 离线包含T-1延迟数据 | 4万 | 20% | 离线T+1跑批时纳入了T日19:00后到达T-1的迟到数据 |
| 测试账号过滤逻辑差异 | 3万 | 15% | 离线有is_test_user过滤,实时漏了 |

归因清楚之后,治理就有了方向。最关键的认知是:差异不一定是bug,但每一笔差异必须可解释。

方法一:口径在字典层统一

最低成本的治理是把口径定义抽到字典层,实时离线SQL都引用同一份口径文档:

# metric_dict/dau.yaml
metric_id: dau_north_star
metric_name: 日活跃用户数
definition:
  description: 当日登录且停留>30秒的UID数
  filter:
    - is_login = 1
    - duration_sec > 30
    - user_type != 'TEST'        # 实时离线必须都过滤
    - device_type != 'EMULATOR'   # 模拟器排除
  time_window: [00:00:00, 23:59:59] in tz=UTC
  dedup_key: user_id
data_source:
  offline: dws_user_action_1d
  realtime: kafka_user_login + Flink State

有了这份YAML后,离线SQL和实时Flink job都从这里读配置——逻辑变更只改一处。在出海硬件品牌我做过更进一步:用Jinja模板把离线SQL和Flink SQL的过滤条件统一生成,杜绝手写不一致。

方法二:实时去重State的设计

DAU这种当日UV指标实时做有挑战——必须用State存"当天已经见过的user_id"。State设计有三个关键点:

// Flink实时DAU计算关键代码
MapState<String, Long> dailyUidState;  // key=user_id, value=首次出现时间
 
@Override
public void open(Configuration parameters) {
    // 1. State TTL必须大于业务定义的窗口(24h不够,要给48h留余量)
    StateTtlConfig ttlConfig = StateTtlConfig.newBuilder(Time.hours(48))
        .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
        .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
        .cleanupFullSnapshot()
        .build();
    
    // 2. 跨日清理用Timer而不是TTL,更精确
    MapStateDescriptor<String, Long> desc = new MapStateDescriptor<>(
        "dailyUidState", String.class, Long.class);
    desc.enableTimeToLive(ttlConfig);
    dailyUidState = getRuntimeContext().getMapState(desc);
}

3个常见坑:①TTL设24h刚好够,但跨时区/DST会丢用户;②State太大OOM,1000万UV的State大概3GB,要做keyby分片;③重启丢State,必须开Checkpoint+RocksDB Backend。

方法三:Diff监控+采样校验

光统一口径不够,还要每天跑监控验证一致性。我做了两层校验:

-- 第1层:每5分钟跑一次小时diff监控
WITH offline_hourly AS (
  SELECT DATE_TRUNC('hour', event_time) AS hr,
         COUNT(DISTINCT user_id) AS dau_offline
  FROM dwd_user_login WHERE dt='${today}' GROUP BY 1
), realtime_hourly AS (
  SELECT hr, dau_realtime FROM realtime_dau_hourly WHERE dt='${today}'
)
SELECT o.hr, o.dau_offline, r.dau_realtime,
       (r.dau_realtime - o.dau_offline) * 1.0 / o.dau_offline AS diff_rate
FROM offline_hourly o JOIN realtime_hourly r USING(hr)
WHERE ABS(diff_rate) > 0.01;  -- diff>1%告警
 
-- 第2层:每周跑一次user_id级diff(采样1%)
-- 拉离线在但实时不在的UID,看是哪一类用户漏了

第二层用户级diff是定位精度最高的——能告诉你"实时漏的5万用户里3万是某渠道、2万是某地区",定位根因事半功倍。

方法四:实时校准+Lambda架构演进

健康的双链路应该是:实时给业务看分钟级趋势(秒级时延、有1%以内误差),离线给最终值(T+1时延、精确)。每天T+1时离线值发布后,实时表自动用离线值校准。

-- 实时校准任务(每日跑)
MERGE INTO ads_dau_realtime t
USING (SELECT dt, dau AS dau_offline FROM ads_dau_offline WHERE dt='${dt}') s
ON t.dt = s.dt
WHEN MATCHED THEN
  UPDATE SET t.dau_final = s.dau_offline,    -- 用离线值替换
             t.is_calibrated = TRUE,
             t.calibrated_at = CURRENT_TIMESTAMP;
 
-- BI侧只读dau_final,业务方看到的永远是最准确的值

这是Kappa/Lambda架构的折衷做法。如果走真正的流批一体(Flink Table Store或Paimon),可以做到实时离线SQL一份代码,从根上消除diff——这是更前沿的方案,但需要技术栈支持。

面试高频追问

追问一:为什么不直接用实时DAU作为最终值?

实时不可能100%精确,因为:①Kafka允许丢消息(at-least-once也可能丢,exactly-once开销大)②State有限制(不可能存无限历史)③上游业务系统有迟到数据(实时窗口关闭后到达)。这三点在分布式系统里几乎无法100%消除。所以业界标准做法是"离线为基准、实时为辅助"。

追问二:流批一体能彻底解决diff吗?

理论上能,实际上看实现。Flink Table Store/Paimon方案里同一份SQL同时跑离线和实时,逻辑层面统一。但物理层面仍有时延差(流模式秒级延迟、批模式T+1),完全消除diff只在「最终一致性」意义上成立。我在出海公司试过Paimon,diff从1%降到0.1%以下,但还是有差异。

追问三:Diff超过阈值,怎么排查?

我的SOP:①先看diff是单向(实时永远少)还是双向波动 ②单向少看Kafka消费lag和State过期 ③双向波动看时间窗口对齐 ④抽样取user_id去两边链路里查 ⑤如果用户级也对得上,就是聚合层的问题(如UNION/JOIN逻辑差异)。这套SOP在元宇宙App跑了一年多,平均定位时间从2天压到4小时。

实战踩坑:State TTL设24h跨日丢了10%用户

元宇宙App初期我把DAU去重State的TTL设成24小时,自以为够用。结果发现凌晨0点附近实时DAU会陡降——因为前一天23:59登录的用户的State恰好在24:00过期,0:01再登录被算成新用户,DAU重复计数。但更糟的是有些跨日上线作业重启后State丢失,10%用户被漏算。

教训:State TTL要≥业务窗口×2,且跨日清理要用Event-Time Timer,不能用Processing-Time TTL。后来我把TTL设成48h+用onTimer做精确跨日清理,diff率从8%降到0.6%。这是Flink State设计的硬功夫。

总结

回答这道题的高分结构:先归因(4类差异来源)→ 再治理(字典统一+State设计+Diff监控+实时校准)→ 最后给数字(diff率从8%到0.6%)。强调"差异不一定是bug,但必须可解释"——这一句话能体现你对生产级实时系统的理解深度。能聊到流批一体(Paimon/Hudi)就是Staff级别。

下一篇聊一个表面简单但水极深的指标:"次日留存率"分母到底用谁?——留存类指标的口径陷阱

你们公司的实时和离线DAU diff多少?怎么校准的?评论区聊聊~

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

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