1. 问题背景:离线数仓撑不住实时业务了
我在一家头部电商零售集团的数据中台团队负责架构演进。业务侧有个核心场景:订单履约监控。运营每天要盯着全国 200 万+ 订单的履约状态,包括支付、出库、揽收、签收等 12 个节点。过去这套逻辑跑在离线数仓上,T+1 出报表,业务方早就怨声载道。
痛点很具体:
- 大促期间订单积压,履约异常(超时未出库、物流停滞)平均 4~6 小时才能被发现,客诉量飙升 30%。
- 离线链路每天凌晨 2 点跑批,高峰期耗时 47 分钟,数据延迟导致运营看的永远是"昨天的数据"。
- 业务方要求"分钟级"看到异常,但离线链路从采集到 Hive 表就花了 20 分钟,根本不可能。
我们最初的想法很简单:引入实时计算,把离线链路替换掉。但真正动手才发现,问题远不止"换套引擎"这么简单。
2. 踩坑与现状:实时替换离线的真实故障
第一版方案很直接:用 Flink 1.18 消费 Kafka 里的订单变更事件,实时计算履约状态,写入 ClickHouse,前端直接查。听起来顺理成章,上线后却踩了一连串的坑。
2.1 数据漂移:实时和离线对不上账
最致命的问题是数据漂移。实时链路算出来的"今日订单量"和离线 Hive 算出来的对不上,差 3%~5%。原因有三层:
- 迟到数据:订单状态变更事件不是严格有序的,支付成功事件可能比创建订单事件先到,实时窗口一关就丢了。
- 维表关联不一致:实时任务关联的商家维表是内存快照,离线任务关联的是 Hive 分区表,两边数据版本不同。
- 重跑口径差异:离线可以随便回溯重算,实时流一旦消费位点推进,想重算就得整条链路重置。
2.2 双链路维护成本翻倍
实时和离线两套代码、两套调度、两套口径,团队 6 个人维护不过来。业务方问"到底以哪个为准",我们自己也答不上来。
2.3 典型报错:Checkpoint 频繁失败
上线第一周,Flink 作业频繁重启,日志里反复出现:
org.apache.flink.runtime.checkpoint.CheckpointException:
Checkpoint expired before completing.
Number of in-flight checkpoints: 1
Cause: The checkpoint was expired because the checkpoint
coordinator did not receive a checkpoint barrier within
the configured timeout (600000 ms).
排查发现是 ClickHouse 批量写入背压,barrier 无法在超时时间内穿透整条链路。我们把 execution.checkpointing.timeout 从 10 分钟调到 30 分钟,治标不治本------根子是架构问题,不是参数问题。
3. 方案:批流一体架构设计与选型
踩完坑我们意识到,不能拿实时链路替代离线链路,而是要让两条链路共用一套逻辑、一套口径。这就是批流一体。
3.1 方案对比
| 方案 | 核心思路 | 优点 | 缺点 |
|---|---|---|---|
| A. 实时替换离线 | Flink 全量接管,废弃 Hive 链路 | 架构简单,一套链路 | 数据漂移难根治,回溯能力弱,风险高 |
| B. 实时+离线双跑 | 两套独立链路,定期对账 | 各自稳定 | 口径难统一,维护成本翻倍 |
| C. 批流一体(Flink + Hive 统一 Catalog) | 同一套 SQL 逻辑,批模式跑全量、流模式跑增量,共用 Hive Metastore 和 Iceberg 表 | 口径天然一致,可回溯,资源弹性 | 初期改造成本高,需要重构现有任务 |
我们最终选了 方案 C 。核心思路是:用 Flink 1.18 的 Hive 方言 + Iceberg 1.4 作为统一存储底座,同一段 Flink SQL 既能以批模式跑历史全量,也能以流模式跑实时增量。离线 Hive 表直接读 Iceberg 的 snapshot,两边天然同源。
3.2 选型依据
- Flink 1.18 :流批一体能力成熟,
TableEnvironment支持executeSql切换执行模式,同一段 SQL 无需改写。 - Iceberg 1.4:支持 ACID、时间旅行、增量读取,批流共用一张表,实时写入的 snapshot 离线立即可查。
- Hive Metastore 3.1:作为统一元数据中心,Flink 和 Hive 共用 Catalog,彻底解决口径分裂。
4. 实操步骤:从双链路到批流一体
4.1 环境版本
| 组件 | 版本 |
|---|---|
| Flink | 1.18.1 |
| Iceberg | 1.4.3 |
| Hive Metastore | 3.1.3 |
| Kafka | 3.6 |
| ClickHouse | 23.8 |
4.2 核心改造:统一 Catalog 与建表
第一步,在 Flink SQL 里注册 Hive Catalog,让批流共用同一张 Iceberg 表:
sql
-- 注册 Hive Catalog,统一元数据
CREATE CATALOG hive_catalog WITH (
'type' = 'hive',
'hive-conf-dir' = '/etc/hive/conf',
'default-database' = 'ods'
);
-- 创建 Iceberg 订单履约表(批流共用)
CREATE TABLE IF NOT EXISTS hive_catalog.ods.order_fulfillment (
order_id BIGINT,
status STRING,
event_time TIMESTAMP(3),
watermark FOR event_time AS event_time - INTERVAL '5' MINUTE
) WITH (
'connector' = 'iceberg',
'catalog-type' = 'hive',
'uri' = 'thrift://meta-host:9083',
'write.format.default' = 'parquet',
'write.upsert.enabled' = 'true'
);
4.3 同一段 SQL,批流两用
核心技巧:逻辑完全一致,只切换执行模式。
sql
-- 流模式:实时消费 Kafka,写入 Iceberg
SET execution.runtime-mode = STREAMING;
INSERT INTO hive_catalog.ods.order_fulfillment
SELECT order_id, status, event_time
FROM kafka_order_events;
-- 批模式:回溯重算历史全量(同一段逻辑)
SET execution.runtime-mode = BATCH;
INSERT INTO hive_catalog.ods.order_fulfillment
SELECT order_id, status, event_time
FROM hive_catalog.ods.order_events_batch;
4.4 维表关联统一
之前实时用内存快照、离线用 Hive 分区表,口径不一致。改造后统一用 Flink Lookup Join 查 Hive 维表,批流都走同一份数据:
sql
SELECT o.order_id, o.status, m.merchant_name
FROM hive_catalog.ods.order_fulfillment o
LEFT JOIN hive_catalog.dim.merchant FOR SYSTEM_TIME AS OF o.proc_time m
ON o.merchant_id = m.merchant_id;
4.5 预期运行结果
改造完成后,实时链路延迟从分钟级降到秒级 ,离线回溯能力保留。同一张 Iceberg 表,实时写入的 snapshot 离线任务立即可读,口径偏差趋近于零。
5. 权衡:批流一体的代价与边界
批流一体不是银弹,它带来口径统一的同时,也引入了新的运维负担和取舍。
5.1 代价一:Iceberg 小文件爆炸
实时高频写入 Iceberg,产生了大量小文件,Hive 查询性能骤降。小文件 问题导致离线查询从 3 秒变成 30 秒。
解决 :开启 Iceberg 的 write.target-file-size-bytes 为 512MB,并配置定期 compaction:
sql
CALL hive_catalog.system.rewrite_data_files(
table => 'ods.order_fulfillment',
options => map('target-file-size-bytes','536870912')
);
5.2 代价二:Checkpoint 超时复发
改造后 Checkpoint 依然偶发超时,这次定位到是 Kafka 分区 rebalance 导致 barrier 对齐延迟 。解决方式是调大并行度并开启 execution.checkpointing.tolerable-failed-checkpoints:
yaml
execution.checkpointing.interval: 60s
execution.checkpointing.timeout: 10min
execution.checkpointing.tolerable-failed-checkpoints: 3
5.3 代价三:迟到数据导致实时结果回跳
即使加了 watermark,仍有极端迟到事件导致实时结果"回跳"。最终方案是实时结果只做预警,不做最终统计,最终数字以批模式重算的 snapshot 为准。业务侧接受"实时看趋势、离线出结论"的口径。
6. 验证数据与效果
改造上线后,我们做了为期两周的对比验证:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 履约异常发现延迟 | 4~6 小时 | 90 秒内 |
| 实时/离线口径偏差 | 3%~5% | 0.2% 以内 |
| 离线跑批耗时 | 47 分钟 | 12 分钟(直接读 Iceberg snapshot) |
| 双链路维护人力 | 6 人 | 3 人 |
大促压测中,实时链路扛住了 峰值 8.5 万 QPS 的订单事件写入,Checkpoint 成功率 99.7%,无数据丢失。
7. 总结:适用边界与哪些业务不要照搬
适用场景
- 口径一致性要求高:实时和离线必须对得上账,比如财务、履约、库存。
- 既有离线数仓又有实时需求:不想推倒重来,希望渐进式演进。
- 团队有 Flink 基础:批流一体本质是 Flink 能力,没有 Flink 经验会陡增学习成本。
不适用场景
- 纯实时、无回溯需求:比如风控实时拦截,不需要历史重算,直接用 Flink + Kafka + Redis 更轻。
- 团队小、无专职数据平台:批流一体初期改造成本高,小团队可能被运维拖垮。
- 已有稳定的 Lambda 架构:如果双链路已经跑得很稳、对账机制成熟,不必为了"先进"而重构。
边界与教训
批流一体解决的是口径统一 问题,不解决数据质量问题------源头脏数据该脏还是脏。另外,Iceberg 的 compaction 和 snapshot 管理需要持续运维,建议配套自动化巡检。
一句话总结 :批流一体的价值不在于"用一套引擎跑两种模式",而在于让实时和离线共享同一份数据、同一套逻辑、同一个口径。架构演进的核心不是技术选型,而是想清楚"你要解决什么问题"。