订阅更新

实时数仓也要分层吗?跟离线分层完全不一样——实时分层怎么做?

数仓项目手册 第 3 题 约 9 分钟读完

本文目录
  1. 先说结论
  2. 实时分层架构:Kafka分层 + Flink处理
  3. ODS层:Kafka原始Topic
  4. DWD层:Flink做流上清洗
  5. DWS层:Flink窗口聚合 → 写入OLAP
  6. 离线分层 vs 实时分层对比
  7. 面试高频追问
  8. 实战踩坑
  9. 总结

先说结论

实时数仓也要分层,但层数要少、链路要短。一般是ODS→DWD→DWS三层就够了,不需要ADS层——实时的ADS就是OLAP引擎上的查询。

跟离线最大的区别:离线每一层是'表'(存储在Hive/MaxCompute),实时每一层是'流'(Kafka Topic或Flink State)。

核心原则:实时分层尽量少做多级JOIN,因为流上的JOIN复杂度和状态开销远大于批处理。能在一层做完的不要拆两层。

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

详细讲解

很多人知道离线数仓要分层,但一说到实时数仓就懵了——实时也要分层?数据不是来一条处理一条吗?确实,实时数仓的分层跟离线完全不是一个思路。离线分层存的是'表',实时分层流的是'流'。我在某元宇宙社交应用从0搭建Flink+Doris实时数仓时,专门设计了实时分层架构,今天把这套方案和踩坑经验分享出来。

实时分层架构:Kafka分层 + Flink处理

我在某元宇宙社交应用搭建实时数仓的架构如下:

层级存储介质处理引擎典型内容
ODSKafka Topic: ods_xxx无(直接消费原始数据)原始埋点日志、Binlog变更流
DWDKafka Topic: dwd_xxxFlink(清洗+标准化)清洗后的明细事件流
DWSDoris / HologresFlink(窗口聚合)分钟级/小时级聚合指标

注意看存储介质的变化——ODS和DWD都在Kafka里(流),到了DWS才落到OLAP引擎(表)。这就是实时分层跟离线最大的不同。

ODS层:Kafka原始Topic

实时的ODS层就是Kafka里的原始Topic。数据来源通常有两种:

① 埋点日志:客户端/服务端埋点通过SDK打到Kafka,一个事件类型一个Topic,比如topic: ods_user_gift_event(用户送礼事件)、ods_user_login_event(登录事件)。

② Binlog变更流:MySQL的Binlog通过Canal/Debezium采集到Kafka,比如topic: ods_binlog_order_table(订单表变更)。

// ODS层Kafka消息示例:用户送礼事件
{
  "event_id": "evt_20231015_abc123",
  "user_id": "u_998877",
  "event_type": "send_gift",
  "gift_id": "gift_rose",
  "gift_amount": 10,
  "receiver_id": "u_112233",
  "timestamp": 1697356800000,
  "device_os": "iOS",
  "app_version": "3.2.1"
}

DWD层:Flink做流上清洗

实时DWD层是一个Flink作业,消费ODS的Kafka Topic,做清洗和标准化后写入新的Kafka Topic。跟离线DWD做的事情类似,但有几个关键区别:

-- Flink SQL实现实时DWD层:用户送礼事件清洗
INSERT INTO dwd_user_gift_event  -- 写入DWD层的Kafka Topic
SELECT
    event_id,
    user_id,
    gift_id,
    gift_amount,
    receiver_id,
    -- 时间戳转为标准格式
    TO_TIMESTAMP(FROM_UNIXTIME(`timestamp`/1000)) AS event_time,
    device_os,
    app_version,
    -- 维度关联:通过异步IO查Redis获取礼物名称和价格
    -- 这里用Flink的Temporal Join关联维度表
    dim_gift.gift_name,
    dim_gift.gift_price_coins
FROM ods_user_gift_event
LEFT JOIN dim_gift FOR SYSTEM_TIME AS OF ods_user_gift_event.proc_time
    ON ods_user_gift_event.gift_id = dim_gift.gift_id
WHERE event_id IS NOT NULL
  AND user_id IS NOT NULL;

注意这里的维度关联方式:离线DWD用的是普通的LEFT JOIN(维度表是Hive表),实时DWD用的是Flink的Temporal Join(维度表在MySQL/Redis中)。这是面试经常被追问的一个点——流上怎么做维度关联。

DWS层:Flink窗口聚合 → 写入OLAP

实时DWS层是另一个Flink作业,消费DWD层的Kafka Topic,做窗口聚合后写入Doris或Hologres:

-- Flink SQL实现实时DWS层:每分钟送礼汇总
INSERT INTO dws_gift_summary_1min  -- 写入Doris/Hologres
SELECT
    TUMBLE_START(event_time, INTERVAL '1' MINUTE) AS window_start,
    gift_id,
    gift_name,
    COUNT(*) AS gift_cnt,
    COUNT(DISTINCT user_id) AS send_uv,
    COUNT(DISTINCT receiver_id) AS receive_uv,
    SUM(gift_price_coins) AS total_coins
FROM dwd_user_gift_event
GROUP BY
    TUMBLE(event_time, INTERVAL '1' MINUTE),
    gift_id,
    gift_name;

聚合结果直接写入Doris,前端大屏查Doris就行。不需要再建ADS层——Doris上的SQL查询就是'实时ADS'。

离线分层 vs 实时分层对比

维度离线分层实时分层
层数4层(ODS/DWD/DWS/ADS)3层(ODS/DWD/DWS),不需要ADS
存储介质每层都是表(Hive/MaxCompute)ODS/DWD是Kafka Topic,DWS是OLAP表
处理引擎Spark SQL / HiveFlink SQL / Flink DataStream
维度关联普通LEFT JOINTemporal Join / 异步IO + Redis
更新频率T+1 / 小时级秒级 / 分钟级
数据修正重跑即可需要额外的修正机制(离线兜底)
调度方式DAG调度(DataWorks/Airflow)常驻作业,7×24小时运行
开发复杂度中等高(状态管理、Exactly-Once、反压)

面试高频追问

追问一:实时DWD写回Kafka,那Kafka岂不是存了两份数据?成本怎么控制?

没错,Kafka分层确实会增加存储成本。但Kafka的存储成本远低于Hive/HDFS,而且Kafka Topic可以设置较短的保留时间(比如DWD只保留3天)。权衡下来,存储成本增加不大,但获得的好处是DWD可以被多个下游Flink作业复用——比如DWS_gift和DWS_revenue两个聚合作业都可以消费同一个DWD Topic,避免重复清洗。如果不分层,每个下游作业都要各自清洗一遍,计算资源浪费更大。

追问二:实时分层链路挂了怎么办?数据丢了怎么修?

这是实时数仓最大的痛点——数据修正。离线数仓重跑一遍就行,实时数仓的Kafka数据过了保留期就没了。我们的做法是'实时出数 + 离线兜底':实时链路负责快速出数,离线链路每天凌晨跑一遍相同口径的汇总,覆盖写入同一张OLAP表。这样即使实时链路有数据质量问题,第二天离线也会修正过来。这就是Lambda架构的核心价值——实时和离线互相兜底。

追问三:为什么不把所有指标都做成实时?

成本和必要性。实时链路的开发成本是离线的3-5倍(要处理乱序、状态、Checkpoint、反压),运维成本更高(7×24小时常驻作业)。大部分业务指标T+1就够了,只有核心指标(比如广告ROI、实时大屏、风控告警)才需要实时。我们的原则是:只有'业务决策时效性 < 1小时'的指标才走实时链路。

实战踩坑

我在某元宇宙社交应用搭建实时数仓时,一开始没有做Kafka分层——所有Flink作业都直接消费ODS层的原始Topic。一开始只有3个实时作业,问题不大。后来增长到10+个,每个作业都在各自清洗同样的原始数据,不仅浪费了大量Flink资源,而且不同作业的清洗逻辑开始出现不一致——同一个'活跃用户'的定义,有的作业过滤了机器人账号,有的没过滤。

后来我们花了两周时间,把所有原始Topic的清洗逻辑抽出来,建了一组DWD层的Kafka Topic,统一定义了清洗标准。所有下游作业都消费DWD Topic。清洗逻辑只维护一份,口径一致性问题彻底解决了。这也是实时分层最核心的价值——不是为了'规范',而是为了'统一'和'复用'。

总结

实时数仓分层的核心是:ODS/DWD在Kafka里流转,DWS落地到OLAP引擎。跟离线分层最大的区别是存储介质不同、维度关联方式不同、数据修正机制不同。面试时讲实时分层,要重点说清楚'为什么要分层'(复用+口径统一)、'Kafka怎么分层'、'维度关联怎么做'三个点。如果能再说出'实时出数+离线兜底'的修正机制,那就很有深度了。

离线分层和实时分层都讲清楚了,那两个链路怎么统一?下一篇聊Lambda架构和Kappa架构的选型——我为什么最终选了Lambda。

你们的实时数仓分了几层?DWD是存在Kafka里还是直接写OLAP?评论区聊聊~

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

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