基于 FlinkSQL + Paimon + Doris 的车联网流批一体湖仓实践 ------ 第 1 章 业务背景与传统 Lambda 架构痛点
系列定位 :本系列以车联网 TSP(Telematics Service Provider)行程主题数仓为真实生产案例 ,完整复盘一套 Flink 1.20 + Paimon 1.4 + Doris 4.1 流批一体湖仓的架构设计、建设过程、上线部署、性能优化与踩坑总结。文中所有"踩坑"均为生产环境实测,非理论推演。
版本基线:Flink 1.20.3(JDK 17)+ Paimon 1.4.2(paimon-flink-1.20-1.4.2)+ Doris 4.1.3-rc02 + Flink CDC 3.x + Kafka 3.x + MySQL 8.0 + DolphinScheduler 3.x
与基础教程的关系 :本系列不重复讲组件原理------Paimon 怎么建表、Doris 怎么调优、Flink CDC 怎么配置,请参阅已完结的三套基础教程(《Apache Paimon 1.4.2 从理论到实践》《Apache Doris 4.1 从理论到实践》《Flink CDC 3.5.0 从理论到实践》)。本系列回答的是另一个问题:如何把这些组件拼成一套"实时与离线不打架"的流批一体湖仓,并安全送上生产。
章节导读
- [1.1 车联网 TSP 平台业务全景](#1.1 车联网 TSP 平台业务全景)
- [1.2 三大核心需求](#1.2 三大核心需求)
- [1.3 传统 Lambda 架构的三大痛点](#1.3 传统 Lambda 架构的三大痛点)
- [1.4 为什么选 Paimon 做湖仓底座](#1.4 为什么选 Paimon 做湖仓底座)
- [1.5 本章小结与下章预告](#1.5 本章小结与下章预告)
1.1 车联网 TSP 平台业务全景
1.1.1 什么是 TSP 平台
TSP(Telematics Service Provider,车联网服务平台)是连接"车"与"服务"的中间层:车辆上的 TBox 终端把车速、电量、位置、里程等状态数据持续上报到平台,平台在此基础上提供行程查询、能耗分析、围栏告警、车主 App 等服务。
我们案例中的 TSP 平台,每秒产生海量车辆报文(企标数据)。所谓"企标",是企业自定的车载数据上报标准------每条报文是一段 JSON,包含车架号(vin)、采集时间、车速、SOC(电池荷电状态)、经纬度等几十个字段,经 IoT 网关汇入 Kafka。
┌──────────┐ MQTT ┌──────────┐ ┌──────────────────────┐
│ 车辆 TBox │ ────────► │ IoT 网关 │ ─────────► │ Kafka │
│ (百万级) │ 上报 │ (协议转换) │ │ cnx_vehicle_data_qb │
└──────────┘ └──────────┘ │ (企标报文 topic) │
└──────────┬───────────┘
│
┌──────────────────┐ │
│ MySQL 业务库 │ ▼
│ 车型/组织/用户维表 │ (数据入湖,见第 4 章)
└──────────────────┘
1.1.2 数据的三个特点
在动手设计之前,先认清这份数据的"脾气"------后面所有架构决策都源于这三个特点:
| 特点 | 具体表现 | 对架构的影响 |
|---|---|---|
| 报文级高频 | 每秒全量车辆持续上报,单日原始报文量以亿计 | ODS 层必须高吞吐写入,且写入要幂等(重放不翻倍) |
| 字段频繁增补 | 企标报文字段随车型迭代、业务需求不断新增 | ODS 层不能做 schema 展开,必须存原文,否则每加一个字段就要改链路 |
| 维表在 MySQL | 车型、组织、用户等维表由业务系统维护在 MySQL,频繁变更 | 维表需 CDC 实时同步入湖,且要同时服务实时与离线两条链路 |
1.1.3 本案例的范围界定:行程主题数仓
车联网平台要建的主题有很多(告警、充电、驾驶行为......),本系列聚焦其中一个完整落地的主题------行程主题数仓:
┌────────────────────────────────────────────────────────────────┐
│ 行程主题数仓的范围 │
└────────────────────────────────────────────────────────────────┘
输入:企标报文(vin / collect_time / json_data 原文)
+ MySQL 维表(车型 / 组织 / 用户)
核心加工:把"一条条报文"加工成"一段段行程"
· SESSION 窗口切分:连续报文聚合成一次出行
· 行程内编号与分段:报文序号 rn、行程分段
· 行程级汇总:单次行程的里程、能耗、时长、起止点
· 日 / 月聚合与全历史快照:每人每天的行程数、累计能耗......
输出:
· 实时看板(当天行程、当日能耗/里程累计)
· T+1 权威报表(任意历史区间回算)
· 车主 App 行程详情查询
选择行程主题作为案例,是因为它同时踩中了流批一体的所有难点:既要秒级出当天数据(实时),又要 T+1 权威口径(离线),还有全历史递推指标(日累计、滚动平均 SOH)这个"流式最难啃的骨头"。行程主题打通了,其他主题就是复制粘贴。
1.2 三大核心需求
业务方最初给的需求只有三句话,但每句话背后都是硬指标:
| # | 需求 | 具体指标 | 难点 |
|---|---|---|---|
| ① | 实时看板 | 车辆当天行程、当日能耗/里程累计,秒级~分钟级可见 | 数据量大、时效要求高 |
| ② | 离线报表 | T+1 的历史权威统计 ,支持任意历史区间回算与口径修正 | 权威性、可重算性 |
| ③ | 统一口径 | 实时与离线最终数据一致 ,且互不踩踏 | 一致性、写冲突隔离 |
1.2.1 需求①:实时看板------"秒级~分钟级"
运营大屏要展示"今日全平台行程数""当日总能耗",车主 App 要看"我今天开了多远"。这两个场景的时效底线不同:
- 运营大屏:分钟级可接受;
- 车主 App 当天行程:秒级体验才合格。
注意措辞是"秒级~分钟级 "------这意味着当天数据不要求严格秒级一致,允许一个短暂的校准窗口。这个细节在第 10 章"实时先行、离线校准"设计中被充分利用,是整套方案能落地的关键妥协点。
1.2.2 需求②:离线报表------"权威"与"可回算"
财务对账、运营月报、政府上报用的数据,必须以 T+1 离线口径为权威值 。更重要的是"任意历史区间回算与口径修正":
- 业务口径变了(比如"行程"的定义从 30 分钟隔断改为 20 分钟)→ 历史数据要能按新口径重算;
- 上游补传了某天的报文 → 那一天的数据要能重跑。
这就要求:ODS 原始数据必须长期在手,且下游全链路可幂等重算。"ODS 在,一切可以重算"是本系列反复出现的设计哲学。
1.2.3 需求③:统一口径------重点是"互不踩踏"
"实时与离线最终一致"好理解:车主上午 10 点看到的当日里程,与第二天报表里的昨日里程,误差必须收敛到可接受范围。
但真正决定方案成败的是后半句------"互不踩踏":
实时链路写它的表、离线链路写它的表,谁也不能覆盖、污染、干扰对方的输出。哪怕两边跑在同一张物理表的不同分区上,也必须保证"每个分区只有一个写入者"。
这条需求在传统 Lambda 架构里靠"物理隔离"天然满足(两套存储嘛),但也正是物理隔离带来了口径对不齐的死结(下一节详述)。我们最终方案的核心,就是找到一种办法:让两条链路共享存储(口径天然可对齐),同时通过契约设计做到互不踩踏。
1.3 传统 Lambda 架构的三大痛点
1.3.1 改造前的架构
平台原有的数仓是典型 Lambda 架构:实时链路与离线链路各自独立,各存一份数据、各写一套逻辑。
┌────────────────────────────────────────────────────────────────────┐
│ 改造前:传统 Lambda 架构 │
└────────────────────────────────────────────────────────────────────┘
┌──────────┐
│ Kafka │ 企标报文
└─────┬────┘
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────────┐
│ 实时链路 │ │ 离线链路 │
│ │ │ │
│ Flink 作业 │ │ Sqoop 导数 + Spark ETL │
│ ▼ │ │ ▼ │
│ Kafka 中间结果 │ │ Hive ODS / DWD / DWS │
│ ▼ │ │ ▼ │
│ Redis / MySQL │ │ Impala 查询 │
│ (当天指标) │ │ (T+1 报表) │
└──────────┬──────────┘ └────────────┬────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────┐
│ 应用层:自己判断"什么时候读实时、什么时候读离线" │
└─────────────────────────────────────────┘
1.3.2 痛点一:两套存储,链路长且异构
实时侧是 Kafka + Redis(当天指标缓存),离线侧是 Hive + Impala(T+1 报表)。两套存储意味着:
- 数据物理上存了两份,一份在 Kafka/Redis,一份在 Hive;
- 中间还要 Sqoop、Spark 等多个搬运环节,每一环都是故障点;
- 想查"某车某天的完整行程明细"?实时侧 Redis 只存聚合值,离线侧 Hive 要等 T+1------当天明细两头都查不到。
1.3.3 痛点二:两套逻辑,口径对不齐
这是最致命的痛点。同一条"行程数"指标,实时链路用 Flink 代码算,离线链路用 Spark SQL 算------两套引擎、两套代码、两个团队维护,口径必然漂移。
一个真实场景:运营发现大屏上"今日行程数 1023",而第二天 T+1 报表显示"昨日行程数 1018",差 5 条。排查结果:实时链路的 SESSION 窗口超时配的是 25 分钟,离线链路是 30 分钟------两套代码里同一个业务概念的参数悄悄分叉了。更糟的是,这种分叉不会报错,只会让数字慢慢偏。
1.3.4 痛点三:运维成本高
| 环节 | 问题 |
|---|---|
| Sqoop 全量导数 | 每日全量跑,源库压力大;增量方案又依赖脆弱的时间戳字段 |
| Hive 小文件 | 高频写入导致小文件爆炸,Impala 查询越来越慢 |
| 回算困难 | 口径修正要重跑历史?Sqoop 重导 + Spark 全量重算,一个主题动辄半天 |
| 两套告警 | 实时 Flink 作业与离线 Spark 作业的监控、告警、重启各自一套 |
| 人员割裂 | 实时逻辑一个团队、离线逻辑一个团队,改口径要两边协同发版 |
1.3.5 小结:Lambda 的本质矛盾
把三个痛点归拢一下,Lambda 架构的根本矛盾是:
为了"实时与离线互不干扰",付出了"数据存两份、逻辑写两套"的代价;而逻辑写两套,就永远无法保证口径一致。
有没有办法只存一份数据、各写各的表(物理上仍隔离),但共享同一套存储与元数据?这就是我们转向流批一体湖仓的动机。
1.4 为什么选 Paimon 做湖仓底座
1.4.1 目标架构
┌────────────────────────────────────────────────────────────────────┐
│ 改造后:流批一体湖仓(本系列目标架构) │
└────────────────────────────────────────────────────────────────────┘
Kafka(cnx_vehicle_data_qb 企标报文)
│ Flink 消费(实时入湖)
▼
tsp_ods.ods_vehicle_qb_msg_report_original ←------ 两链路唯一共享点
(Paimon,按天分区,vin 分桶) MySQL 维表
│ │ Flink CDC(yaml pipeline)
├────────────────────┐ ▼
[离线] DS 调度批作业 │ Paimon 维表(ods 层)
runtime-mode=BATCH │
R1:只写 dt <= T-1 │ [实时] 常驻流作业(9 个作业链)
INSERT OVERWRITE 幂等 │ R2:只写 dt >= T(屏障)
dwd → dws → ads(原名) │ dwd_*_rt → dws_*_rt → ads_*_rt
│ │ _cum_rt 的 T-1 种子行由离线独家写
▼ ▼
┌──────────────────────────────────────────────┐
│ Doris 服务层 │
│ · Paimon Catalog 直查(当天/明细) │
│ · 异步物化视图 MV(历史聚合加速,离线表之上) │
│ · VIEW v_*(R3:历史走 MV,当天走实时直查) │
└──────────────────────────────────────────────┘
▼
BI / APP / 对账
核心变化一句话:用 Paimon 这一个湖存储,同时承载实时与离线两条链路的输出;查询层用 Doris 统一收口。
1.4.2 为什么是"同一存储、同一 Catalog"
选 Paimon 做底座,最关键的不是它的性能参数,而是这两个"同一":
- 同一存储 :实时表(
_rt后缀)与离线表(原名)都是 Paimon 表,物理上分表隔离(互不踩踏),逻辑上同源同根(口径可对齐)。实时与离线的差异只剩"写了哪张表、哪个分区",而不是"两套完全异构的系统"。 - 同一 Catalog :两条链路共用同一个 Hive Metastore Catalog。实时作业通过 LOOKUP JOIN 读维表,离线批作业直接批读同一批维表------一份维表、一处维护、两处使用。
1.4.3 流批各写各的表,查询层统一收口
在同一个湖上,两条链路各写各的表:
- 离线链路:DolphinScheduler 调度批式 FlinkSQL,
INSERT OVERWRITE整区覆盖,只写dt <= T-1(铁律 R1); - 实时链路:9 个常驻流作业分层递进,只写
dt >= T(铁律 R2); - 唯一交汇点:递推累计表
_cum_rt的 T-1 种子行,由离线种子作业独家写入------分区级单写者,零冲突。
对外查询不直接暴露湖表,而是收口到 Doris 服务层:历史数据走异步物化视图加速,当天数据走 Paimon Catalog 直查,再用一个分流 VIEW(铁律 R3)把"历史走离线、当天走实时"对 BI 屏蔽掉------BI 只换一个表名,SQL 零改动。
三条铁律(R1/R2/R3)是整套方案的基石,第 2 章展开。
1.4.4 与"两套存储各自同步"方案的本质区别
有人会问:"保持 Lambda 两套存储,每天凌晨用同步工具把离线结果灌进实时侧,不也能对齐吗?"
这正是第 12 章要专门拆解的"伪需求"。先说本质区别:
| 对比项 | 两套存储 + 同步工具 | 同一湖存储(本方案) |
|---|---|---|
| 数据份数 | 两份,靠同步保持"近似一致" | 一份,实时/离线各写各的表 |
| 一致性 | 同步延迟窗口内不一致,且同步失败会静默漂移 | 同源数据,最终天然一致 |
| 口径修正 | 要改实时 + 离线 + 同步三处逻辑 | 改离线 SQL 重算即可,实时次日被种子校准 |
| 当天明细 | 实时侧通常只存聚合值,明细查不到 | 湖里有全量明细(_rt 表),随时可查 |
| 故障半径 | 同步链路挂了,两边数据开始分叉 | 实时链路整体停掉,历史查询完全不受影响 |
一句话:"同步"是让两份不一致的数据变得看起来一致;"湖仓一体"是让数据从源头就只有一份。
1.4.5 为什么是 Paimon + Doris 这个组合
| 选型 | 关键理由 |
|---|---|
| Paimon 做湖底座 | 原生支持主键表与 upsert(实时链路必需);与 Flink 集成最紧密(FlinkSQL 直接读写);支持 Hive Metastore Catalog(与存量 Hive 数仓无缝共存);changelog 机制支撑下游流读 |
| Flink 1.20 做流批引擎 | 同一套 FlinkSQL 语法,runtime-mode 切换流批,离线批作业直接批读 Paimon;与已运行的 Flink 实时作业共用运维体系 |
| Doris 做服务层 | BI/App 的"筛选 + 聚合 + 高并发点查"是交互式负载,Paimon 的 scan 式查询扛不住;Doris 通过 Multi-Catalog 直查 Paimon,异步物化视图把历史聚合压到毫秒级 |
| Flink CDC 同步维表 | MySQL 维表整库入湖,schema.change.behavior=evolve 应对维表频繁变更 |
| DolphinScheduler 做调度 | 离线作业链的依赖编排、失败重跑、SQL 指纹防呆(第 15 章详述) |
版本组合警告 :这套组合"能用",但版本兼容是踩坑重灾区 ------Paimon connector 必须严格匹配 Flink 版本(paimon-flink-1.20-1.4.2),Doris 4.1 的 Arrow 读取与 Flink 类型映射有暗坑(DATEV2 被读成带时区 TIMESTAMP)等。本系列第 17 章会逐一拆解 14 类实测问题,选型前请务必先读那一章。
1.5 本章小结与下章预告
本章小结
┌────────────────────────────────────────────────────────────────┐
│ 第 1 章 要点回顾 │
└────────────────────────────────────────────────────────────────┘
✓ 业务全景:TSP 平台每秒海量企标报文,行程主题数仓为落地案例
✓ 数据三特点:报文级高频 / 字段频繁增补 / 维表在 MySQL
✓ 三大需求:实时看板(秒级~分钟级)/ 离线报表(权威可回算)
/ 统一口径(最终一致 + 互不踩踏)
✓ Lambda 三痛点:两套存储 / 两套逻辑口径对不齐 / 运维成本高
✓ 根本矛盾:为"互不干扰"付出"存两份、写两套"的代价,
而逻辑写两套就永远无法保证口径一致
✓ 破局思路:同一 Paimon 存储 + 同一 Catalog,流批各写各表,
Doris 统一收口;三条铁律保证互不踩踏
关键结论(供后文反复引用)
| 结论 | 后文落点 |
|---|---|
| ODS 是全仓唯一真相源,必须存报文原文 | 第 4 章 ODS 表设计 |
| "秒级~分钟级"时效容忍 1 天校准窗口 | 第 10 章 实时先行、离线校准 |
| "互不踩踏"= 每个分区只有一个写入者 | 第 2/12 章 分区契约铁律 |
| 实时链路停掉不影响对外查询 | 第 13 章 Doris 服务层切流回滚 |
下章预告
第 2 章 流批一体湖仓总体架构与三条分区契约铁律 :把本章的目标架构图逐层拆开,讲清 R1(离线只写
dt <= T-1)、R2(实时只写dt >= T)、R3(查询历史走离线、当天走实时)三条铁律的落地方式,以及唯一交汇点"种子作业"的设计。三条铁律违反任何一条,实时与离线的数据就会打架------它们是后面 16 章一切内容的基石。
官方参考资料
- Apache Paimon 官网:https://paimon.apache.org/docs/1.4.2/
- Apache Doris 官网:https://doris.apache.org/zh-CN/docs/4.x/
- Apache Flink 1.20 文档:https://nightlies.apache.org/flink/flink-docs-release-1.20/
- Flink CDC 文档:https://nightlies.apache.org/flink/flink-cdc-docs/release-3.5/
- Apache DolphinScheduler 官网:https://dolphinscheduler.apache.org/zh-cn
- 本系列配套基础教程:《Apache Paimon 1.4.2 从理论到实践》《Apache Doris 4.1 从理论到实践》《Flink CDC 3.5.0 从理论到实践》