最近帮一个电商团队把实时报表链路从 Lambda 架构切到了 Kappa 架构,改造完之后,整个主链路从原来的 4 套系统缩到 2 套,凌晨的调度依赖全部消失,关键是实时看板和离线报表数字终于能对上了。这篇文章就是这次改造的完整复盘:Kappa 架构的选型依据、实时数仓分层怎么设计、组件版本怎么配、Flink SQL 全链路代码怎么跑通、压测和调优怎么做,以及踩过的几个比较隐蔽的坑。适合正在做实时数据仓库、或者准备从 Lambda 往 Kappa 迁移的读者参考,内容偏实战,可以直接照着落地。
1. 为什么从 Lambda 切到 Kappa,你的业务到底适不适合
1.1 Lambda 架构的痛:两套代码和永远对不齐的口径
先花点时间说说我们为什么要做这次切换。原系统是典型的 Lambda 架构:实时链路用 Flink 算当天累计,凌晨再用 Spark 批任务算 T-1 汇总。看起来是“双保险”,实际上痛苦全在运维和口径上。
最大的问题是同一套业务逻辑要维护两遍。就拿订单金额来说,批处理会过滤掉测试订单、退款单、部分内部单,而实时 SQL 为了“先跑起来”,当时加过滤条件的时候并没有和批任务完全对齐。结果就是每天早上对账,实时数字和离线数字差个几万块,每次都要靠人工去排查差在哪。改一个指标要同时改 Spark 批任务和 Flink 实时任务,两边上线节奏还不一致,经常出现“批任务先上了,实时任务忘了同步”的情况。时间一长,谁都不敢动那套实时 SQL,因为改完你根本不知道会影响哪个下游报表。
这不是代码质量的问题,是结构问题。Lambda 结构上就要求批流两套物理链路并存,那么“两套代码对不齐”就是一个必然结果,而不是偶发 bug。
1.2 Kappa 的解法:一条流,靠重放解决回溯
Kappa 架构的思路很直接:只保留实时一条链路,所有数据都走流式引擎处理,如果需要修正历史数据,就重放 Kafka 里保留的消息,让流式计算重新跑一遍。这样数据从 ODS 到 DWS,永远是同一套 Flink SQL,不存在两套代码分叉的问题。
很多人对 Kappa 有个误解,以为它是“不要批处理、全部实时”,其实它只是把“批”这件事从架构里拿掉了,但保留了一个核心能力:Kafka 的消息重放。当业务逻辑变化需要回算历史数据时,你不需要写一套批任务去跑 Hive,而是把 Flink 的状态清掉,把消费位点重置到需要回放的 offset,重新消费、重新计算、重新写出。只要 Kafka 的保留时间足够覆盖你需要回放的时间窗口,这个操作就能完成。
Kafka 在这里的定位就很重要了——它不只是实时链路的缓冲层,更是整个数据仓库的“可回放存储”。
1.3 什么样的业务适合 Kappa
Kappa 确实好,但不是所有业务都适合。我自己的判断依据是这样的:
| 场景 | 适合程度 | 原因 |
|---|---|---|
| 分钟级甚至秒级可见性需求 | 适合 | 只有一套实时链路,天然能满足 |
| 指标口径相对稳定,不频繁改口径 | 适合 | 改一次口径 = 重放一次,重放有成本 |
| Kafka 保留期内能完成全量重放 | 适合 | 数据量太大、重放时间超过保留期就会断档 |
| 需要 T-1 精确对账、对历史数据精确到天的强校验 | 不太适合 | 需要 OLAP 存储配合主键模型才能强一致 |
| 指标经常变,每次变更都要重算历史 N 天 | 不太适合 | 重放预算会非常吃紧,状态回收也麻烦 |
我们最终的选择是“Kappa 为主,离线批做补充”,但不是让两套口径打架,而是明确分工:实时链路的 Kappa 出口是业务方每天看的实时看板、大屏和告警;离线 Hive 数仓只承担长周期的历史分析和报表归档。Kappa 输出的结果会持久化到 StarRocks,离线数仓也不再去覆盖实时口径,两边各管一段。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实时数仓分层设计:每一层在 Kappa 里用什么承载
Kappa 一样有分层,只是物理载体变了。原来 ODS 是 Hive 表,DWD 是 Hive 表,DWS 是宽表,ADS 是导出的 MySQL/ES;在 Kappa 里,中间层变成了 Kafka Topic + Flink 状态,最终结果落到 OLAP 存储。每一层的作用没有变,只是“查到哪一层的数据”这个逻辑变了。
2.1 ODS 层:Kafka 就是你的原始事件总线
ODS 层在 Kappa 里的角色就是 Kafka 的原始业务 Topic。订单、支付、退款、库存变动这些事件按业务域拆分 Topic,分区键按 user_id 或者 order_id 做 hash,保证同一个业务实体的消息有序进入同一个分区,这是后面做状态聚合的前提。
ODS 的保留时间不建议设太短,至少 72 小时,如果磁盘够的话放到 7 天。这一层就是整个数仓的“原始底片”,任何下游指标计算错误,最终都要通过重放这一层来找回和新算。我们当时把 log.retention.hours 设成了 72,峰值流量下 Kafka 单 Topic 日增约 70GB,3 个副本下来整个集群一天新增差不多 600GB,磁盘规划要按这个量级去评估。
2.2 DWD 层:清洗、维表补齐、以及那个关键的中间 Topic
DWD 层在 Kappa 里做的事情和离线数仓一样:去重、过滤无效数据、补维度字段、统一字段格式,然后输出成一行行的明细宽表。
区别在于,离线 DWD 是一张 Hive 表,等着批任务去扫描;Kappa 的 DWD 是一条 Kafka Topic,Flink 实时写入,下游所有消费者实时订阅。我强烈建议 DWD 不要直接写到 StarRocks 明细表,而是先落一个 Kafka Topic,再让 ADS 层消费。为什么呢?因为数仓不只是服务一张报表,BI、算法、消息推送都可能需要明细数据;走 Kafka Topic 可以让任意下游独立消费、独立回放,不用都挤到 StarRocks 上跑查询。
2.3 DWS 层:预聚合和 Build 的粒度选择
DWS 是做预聚合的地方。在 Lambda 里,这一层经常用 Kylin 或者 Spark 把明细汇总成宽表;Kappa 里这一层是 Flink SQL 的窗口聚合,按 1 分钟、5 分钟、1 小时粒度汇总订单量、GMV、UV 这类指标,结果写到一个新的 Kafka Topic,同时物化一份到 StarRocks 汇总表。
这里有一点比较关键:聚合粒度怎么选。粒度越细,下游越灵活,但 Flink 的状态和计算压力也越大。我们当时定的是 1 分钟粒度和小时粒度各一份。1 分钟的给实时大屏用,小时的给运营报表用。如果你一开始不确定要什么粒度,至少先做分钟的,因为后续可以用分钟结果再自动汇总到小时,反过来就没法补。
2.4 ADS 层:StarRocks 物化与即席查询
ADS 层是真正给业务方查数的地方,我们用 StarRocks 承载。明细表、汇总表、一级宽表全部建在 StarRocks 上,Flink 通过 StarRocks Connector 写入。
选 StarRocks 而不是 ClickHouse,主要原因是实时数仓里大量操作是“更新已有行”而不是纯追加:订单状态流转、用户信息变更、退款单状态变化,这些都是 UPDATE。StarRocks 的 Primary Key 模型对高频更新支持得比较好,配合 Flink 的 changelog 流可以做 upsert 写入,ClickHouse 在更新场景相对要别扭一些。
