1. 实时数仓的选型与整体设计
做实时数仓之前,先想清楚一个问题:你到底需要多“实时”?
我见过不少团队,业务方张口就要“秒级实时”,最后拉出来一看,其实 T+1 的离线报表完全够用。实时数仓最大的成本不是 Flink 集群那几台机器,而是它把“事后补救”变成了“事中处理”——数据质量、口径管理、运维复杂度全部前置,这套体系一旦搭起来,每天都需要人盯。所以先别急着写代码,把需求分级:哪些场景真的需要分钟级甚至秒级响应(比如大促实时大屏、风控异常检测、实时个性化推荐特征),哪些场景其实跑个小时级调度就能交差。实时数仓的价值是解决“等不起”的那部分问题,不是替代离线数仓。
确定要做之后,整体架构基本是这一套组合拳:
code复制业务库/日志 -> Kafka -> Flink SQL(清洗/关联/聚合) -> 下游存储
-> ClickHouse/HBase/MySQL -> 应用/大屏
这套链路里,Flink 的角色不是“一个计算引擎”那么简单,它同时承担了三件事:实时 ETL、流式关联、增量聚合。很多团队一开始把 Flink 只当成“从 Kafka 读数据然后写进 ClickHouse 的管道”,这就浪费了它最核心的价值——状态管理和事件时间处理。状态管理让 Flink 能在内存里记住跨事件、跨窗口的中间结果,相当于给流处理装上了“记忆”;事件时间处理让乱序到达的数据能按照业务真正发生的时间进行聚合,而不是按照数据到达系统的时间。这两个能力是做实时数仓跟做“实时搬运”的分水岭。
另一点要提前定的技术选型是:用 DataStream API 还是 Flink SQL?
我的建议非常直接:默认用 Flink SQL,除非你遇到 SQL 表达不了的需求。理由有三条:
第一,Flink SQL 是声明式的,写什么不写怎么做,引擎自动优化执行计划,同样一个双流 join,手写 DataStream 的 connect + keyBy + state 大概要上百行业务代码,SQL 三行搞定。
第二,实时数仓的消费方不只有你一个开发,SQL 的维护成本低,产品经理都能看懂口径。
第三,Flink SQL 的生态已经很成熟了,CDC、维表 join、窗口聚合全是原生支持,没必要自己造轮子。
我见过太多“为了炫技而炫技”的团队,把一切逻辑用 DataStream 手写,最后代码烂到只有自己能维护。做数仓,核心是口径清晰、链路稳定,不是展示你多会写 Java。
但也不是完全不用 DataStream。如果你要做的是超高吞吐的定制化处理(比如自定义窗口触发器、复杂的旁路输出逻辑),或者需要依赖一些 SQL 不支持的第三方库做实时特征加工,那还是要用 DataStream。实际项目中,比较稳妥的做法是:以 Flink SQL 为底座搭建主链路,用 DataStream 写一个单独的 jar 处理 SQL 搞不定的边缘场景,两边都跑在同一套 Flink 集群上,通过 JobManager 统一管理。
一个容易犯的错误是把实时数仓做成“离线数仓的加速版”,在实时链路上也搞出五六层明细表。完全没有必要。实时场景下,我一般只保留三级:ODS(原始数据)、DWD(清洗关联后的明细事实)、DWS(汇总指标)。ADS 层如果需要,DWS 直接出指标给应用就行,没必要再造一层。原因很简单:实时链路每多一段,延迟和故障风险就多一截。离线可以容忍两小时跑完一个全链路调度,实时不行,多一层就要多付一次 Kafka topic 的存储成本和序列化/反序列化的延迟开销。举一个实际数字:我经手的一个项目里,Kafka 单 topic 的每日数据量在 5 亿条左右,每条消息按 1KB 算,一天就是 500GB。如果每条消息在链路中多落一个 topic,一天就多烧 500GB 的磁盘空间和对应的 Kafka 带宽成本。所以能一层的绝不两层,这既是性能考量,也是成本考量。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心机制与实战要点:Flink 到底强在哪
2.1 状态管理、精确一次语义与 checkpoints 剖析
学 Flink 的人一定听过 state、checkpoint、exactly-once,但很多人理解得很浅。只知道“Flink 很牛,能做到精确一次”,却说不出它为什么能做到。你用的时候如果不懂原理,出了问题只能干瞪眼。
Flink 的 checkpoint 机制,核心来自 Chandy-Lamport 分布式快照算法。它把整个计算任务里每个算子的状态定期做一次全局快照,包括两个关键部分:一是 Keyed State 里的业务数据,二是 Kafka 分区消费的位点(offset)。这里有个很容易被忽略的细节:不是只有状态数据需要快照,数据源端的 offset 也必须一起记录。为什么?因为一旦发生故障要恢复,如果你的状态恢复到了 10:00:00 的快照,但 Kafka offset 却还在 10:00:00 之后的位置,那中间这一段数据就永久丢失了;反过来,如果 offset 比状态更早,数据就会重复消费。Flink 把这两者封装在一个 checkpoint 里做原子持久化,才能保证“要么都成功、要么都重来”,这就是精确一次语义的开端。
实操时,我建议把这个做“端到端精确一次”的关键参数背下来:
| 配置参数 | 推荐值 | 作用说明 |
|---|---|---|
execution.checkpointing.interval |
60s 左右 | checkpoint 周期,太短会严重影响吞吐,太长则故障恢复损失大 |
execution.checkpointing.mode |
EXACTLY_ONCE | 精确一次模式 |
execution.checkpointing.min-pause |
30s | 两次 checkpoint 之间的最小间隔,防止反压时 checkpoint 连续挤压 |
execution.checkpointing.timeout |
10min | 超时自动丢弃,避免失败 checkpoint 占住资源 |
execution.checkpointing.max-concurrent |
1 | 并发 checkpoint 数,生产环境一般保持 1,太多会互相干扰 |
state.backend.type |
rocksdb | 大状态用 RocksDB,小状态可用 heap |
state.checkpoints.num-retained |
3~5 | 保留历史 checkpoint 数量,用于回溯和修复 |
这里单独说一下 RocksDB 与 heap state 的取舍。如果任务的状态不大(几个 GB 以内),用 heap state 性能最好,读写都在堆内完成,GC 压力也不大。但如果状态到了几十 GB,heap state 会导致 JVM 频繁 Full GC,甚至直接 OOM,这时候必须切 RocksDB——它把状态存储在本地磁盘,利用内存做 block cache,虽然单次读写比 heap 慢一个数量级,但它能撑住远超内存的状态规模。这个选择会在第 5 章“资源消耗最小化”里继续展开。
另外一个实战中很容易踩的坑是:checkpoint 频繁失败往往不是 Flink 自身的问题,而是下游写入组件不够“配合”。你开启 EXACTLY_ONCE,Flink 只保证自己内部的状态一致性,如果 Sink 是普通的 Kafka Producer 或者 JDBC 写入,那 Flink 任务挂了恢复时会重复写入,下游就会产生重复数据——这不叫端到端精确一次。要做到真正的端到端精确一次,Kafka Sink 要用两阶段提交协议,JDBC 连接器则要开启 XA 事务机制,否则就只能在 Sink 层做幂等设计。很多业务不用严格精确一次,用“至少一次 + 下游幂等”来兜底,如果你们对重复数据容忍度高,这也是一条省心省力的路。比如写入 MySQL 时,只要目标表有唯一键,用 INSERT ON DUPLICATE KEY UPDATE 就行;写入 Redis 时用 SET 天然幂等。关键是你要在架构设计阶段就把这个问题想清楚,而不是等数据重复了再打补丁。
2.2 事件时间、水位线与窗口计算的配合
实时数仓做窗口聚合天经地义,但“窗口怎么开”学问很大。Flink 窗口聚合有个关键配置叫水位线,它解决的是乱序数据的问题。现实世界中,业务日志产生的时间和进入 Kafka 的时间经常不匹配:比如用户 App 在弱网环境下产生了一条点击,等网络恢复后这条日志才发出来,它整整晚了 10 秒甚至 1 分钟才被服务端收到。如果不处理乱序,你用处理时间做窗口,聚合结果会被晚到的数据污染——前一个窗口少
