FlinkCDC → Kafka → Flink → Doris,实时数仓这条链路怎么设计最稳?
本文目录
先说结论
Kafka在实时数仓里的角色是'数据总线':所有数据源(埋点SDK、FlinkCDC、第三方回传)统一写入Kafka,下游Flink按需消费处理,写入OLAP引擎(Doris/Hologres)。Kafka解耦了上下游,让数据源和消费者可以独立扩缩容。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
这道题不是单纯考Kafka,而是考你对'Kafka在实时数仓中扮演什么角色'的理解。面试官想看的是你能不能把Kafka和Flink的配合讲清楚。我在某元宇宙社交公司从0搭建了Flink+Kafka+Doris的实时数仓,在某短剧出海公司也是类似的实时链路(Kafka+Flink+Hologres),今天把这套架构的设计思路和踩坑经验分享出来。
实时数仓的完整架构
# 我在某元宇宙社交公司搭建的实时数仓架构
#
# ┌──────────────┐
# │ 数据源层 │
# │ 埋点SDK │──→ Kafka topic: user_event
# │ FlinkCDC │──→ Kafka topic: user_sync / gift_txn
# │ 第三方API │──→ Kafka topic: third_party
# └──────────────┘
# ↓
# ┌──────────────┐
# │ Kafka集群 │ 3 Broker, 多Topic, 数据缓冲+解耦
# └──────────────┘
# ↓
# ┌──────────────┐
# │ Flink计算层 │ 实时ETL / 聚合 / 维表Join
# └──────────────┘
# ↓
# ┌──────────────┐
# │ 存储层 │ Doris(实时查询)+ S3(离线备份)
# └──────────────┘
# ↓
# ┌──────────────┐
# │ 应用层 │ Metabase大盘 / 接口查询 / 报警
# └──────────────┘
FlinkCDC → Kafka这段怎么设计?
FlinkCDC负责实时捕获MySQL的binlog变更,写入Kafka。这段链路有几个关键设计点:
// FlinkCDC配置(以MySQL为例)
MySqlSource<String> source = MySqlSource.<String>builder()
.hostname("mysql-host")
.port(3306)
.databaseList("app_db")
.tableList("app_db.users", "app_db.gift_transactions")
.username("cdc_reader")
.password("xxx")
.deserializer(new JsonDebeziumDeserializationSchema())
.build();
// 写入Kafka时的关键配置
// ① 用表的主键作为Kafka的Key → 同一条记录的变更进同一个Partition
// ② 保证同一主键的INSERT/UPDATE/DELETE有序
// ③ Kafka Sink开启Exactly-Once(两阶段提交)
为什么要把CDC数据写入Kafka而不是直接写Doris?三个原因:① 解耦——CDC数据可以被多个下游消费(实时大盘、风控、推荐算法各取所需);② 缓冲——上游MySQL变更速率波动大,Kafka可以削峰填谷;③ 回溯——Kafka保留7天数据,出问题可以重新消费。
Kafka → Flink → Doris这段怎么设计?
Flink消费Kafka数据做实时ETL,然后写入Doris。这段链路的关键:
| 设计点 | 方案 | 原因 |
|---|---|---|
| Flink消费模式 | KafkaSource + Checkpoint | 保证Exactly-Once语义 |
| 数据清洗 | Flink DataStream中过滤+转换 | 脏数据不写入Doris |
| 维表Join | Flink Async IO查询维度表 | 实时关联用户/设备维度 |
| 写入Doris | 批量Stream Load(攒批5s/1000条) | 避免逐条INSERT的低吞吐 |
| Checkpoint间隔 | 60秒 | 平衡延迟和恢复开销 |
// Flink消费Kafka + 写入Doris的核心代码框架
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(60000); // 60秒Checkpoint
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// 从Kafka消费
KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
.setBootstrapServers("broker1:9092,broker2:9092")
.setTopics("gift_txn")
.setGroupId("flink-gift-etl")
.setStartingOffsets(OffsetsInitializer.committedOffsets())
.build();
DataStream<String> stream = env.fromSource(kafkaSource, ...);
// ETL处理 → 攒批 → Stream Load写入Doris
实战踩坑:CDC数据乱序导致的幽灵数据
有一次业务反馈:某个用户明明修改了昵称,但Doris里还是老昵称。排查发现是FlinkCDC写Kafka时没有用主键做Key,导致同一个用户的UPDATE和之前的INSERT进了不同Partition。Flink并行消费时,UPDATE先于INSERT被处理了——UPDATE找不到对应记录,被当作无效数据丢弃了。
解决方案:FlinkCDC写Kafka时,用表的主键(user_id)作为Kafka的Message Key。同一个user_id的所有变更都进同一个Partition,保证时序正确。这个改动之后幽灵数据问题彻底解决了。
两家公司架构对比
| 维度 | 元宇宙社交公司 | 短剧出海公司 |
|---|---|---|
| 数据源 | 埋点SDK + FlinkCDC | 广告平台API回传 + 埋点 |
| Kafka管理 | 自建(Ambari+Know Streaming) | 云厂商托管 |
| 计算引擎 | Flink DataStream | Flink + 云厂商调度 |
| OLAP存储 | Doris(自建) | Hologres(云服务) |
| BI展示 | Metabase | 云厂商BI工具 |
| 数据时效 | 秒级(实时大盘) | 5分钟(投放数据) |
两套架构的核心思路一样:数据源 → Kafka(缓冲解耦)→ Flink(实时ETL)→ OLAP引擎(查询服务)。区别在于自建 vs 云服务。小公司建议优先用云服务,节省运维成本;数据量大或定制需求多再考虑自建。
面试高频追问
追问一:为什么用Kafka而不是直接Flink读MySQL?
三个原因:解耦(多下游共享)、缓冲(削峰填谷)、回溯(重新消费历史数据)。如果只有一个下游可以考虑直连,但生产环境很少只有一个消费者。
追问二:Flink Checkpoint失败了怎么办?
Checkpoint失败Flink会从上一次成功的Checkpoint恢复。Kafka的offset也回到上次Checkpoint的值,意味着会重复消费一部分数据。这就是为什么Consumer端要做幂等写入——Doris的UNIQUE KEY模型天然支持幂等。
总结
Kafka+Flink实时数仓面试,关键是讲清楚Kafka在架构中的角色(数据总线、解耦缓冲)和与Flink的配合方式(KafkaSource+Checkpoint+Exactly-Once)。CDC数据要用主键做Kafka Key保证顺序,写Doris要用Stream Load批量写入。两个踩坑点:CDC乱序和Doris写入瓶颈,都是实际生产会遇到的高频问题。
下一篇聊一个细节但很重要的问题:Kafka能保证消息有序吗?什么时候有序什么时候无序?
你们的实时数仓架构是什么样的?用了Kafka+Flink吗?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长