3. 技术选型:Flink vs Spark Streaming,Hudi vs Iceberg,Kafka作为消息队列的角色
做离线回填,技术选型是第一步。选错了,后面全是坑。
我见过不少团队,一上来就追新框架,结果发现跟自己的业务场景根本不搭。今天咱们就掰开揉碎,聊聊这几个核心组件的选型逻辑。
3.1 计算引擎:Flink vs Spark Streaming
先说结论:离线回填场景,我首选 Flink。为什么?
你想想看,离线回填的本质是什么?是把历史数据重新跑一遍,然后跟实时流合并。这里有两个关键点:一是要能精确回溯,二是要保证状态一致。
Flink 的 Exactly-Once 语义和 Savepoint 机制,简直就是为这个场景量身定做的。我在项目中遇到过好几次,回填到一半发现数据有问题,需要暂停、修代码、再恢复。Flink 的 Savepoint 可以让我从指定位置继续跑,不用从头再来。Spark Streaming 虽然也有 Checkpoint,但恢复的粒度远不如 Flink 精细。
核心对比:
- 状态管理:Flink 原生支持 Keyed State 和 Operator State,回填时能精确控制每个算子的状态。Spark Streaming 的状态管理依赖 DStream 的 updateStateByKey,性能差不少。
- 事件时间处理:Flink 的事件时间语义是业界标杆。回填时,我们经常需要按原始时间戳重新计算,Flink 的 Watermark 机制能完美处理乱序数据。
- 批流一体:Flink 1.12 之后,批流 API 统一了。同一个作业,改个参数就能从流模式切到批模式。Spark 虽然也有 Structured Streaming,但批和流的 API 还是有差异。
当然,Spark Streaming 也不是一无是处。如果你的团队 Spark 技术栈很成熟,而且回填的数据量特别大(比如每天 PB 级),Spark 的批处理性能确实更猛。但说实话,大部分公司的回填场景,数据量远没到那个级别。
| 维度 | Flink | Spark Streaming |
|---|---|---|
| 状态管理 | 原生支持,粒度细 | 依赖 updateStateByKey,性能一般 |
| 事件时间 | Watermark 机制,完美处理乱序 | 支持,但精度和灵活性不如 Flink |
| Savepoint | 支持,可精确恢复 | Checkpoint,恢复粒度较粗 |
| 批流一体 | API 统一,切换成本低 | API 有差异,需要额外适配 |
| 社区活跃度 | 高,阿里、字节等大厂都在用 | 高,但流计算方向投入不如 Flink |
我的建议:
如果你是从零开始搭建离线回填系统,直接上 Flink。如果已有 Spark 集群,且回填场景简单(比如只是重跑 T+1 的数据),用 Spark Streaming 也能搞定。但一旦涉及复杂的状态管理和精确恢复,Flink 是唯一选择。
3.2 存储层:Hudi vs Iceberg
存储层的选型,说白了就是选一个能支持 ACID 事务、增量查询、时间旅行 的表格式。Hudi 和 Iceberg 是目前最主流的两个选择。
我记得 2020 年第一次用 Hudi 时,被它的 Copy-on-Write 和 Merge-on-Read 两种存储类型搞晕过。后来才明白,这其实是 Hudi 的核心设计哲学:用空间换时间,或者用时间换空间。
- Copy-on-Write (COW):每次写入都生成新文件,适合写少读多的场景。回填时,如果数据量不大,用 COW 能保证查询性能。
- Merge-on-Read (MOR):写入时只写增量日志,读取时再合并。适合写多读少的场景。回填时,如果数据量巨大,用 MOR 能提升写入速度。
Iceberg 的设计更简洁。它没有 COW 和 MOR 的概念,而是通过 Manifest 文件 来管理元数据。每次写入都会生成新的 Manifest,读取时直接定位到最新版本。Iceberg 的 Hidden Partitioning 功能也很实用,不用手动指定分区字段,系统会自动推断。
避坑指南:
我曾经在一个项目中,用 Hudi 的 MOR 类型做回填。结果因为小文件太多,查询性能急剧下降。后来才发现,Hudi 的 Clustering 功能需要定期触发,否则小文件会越积越多。Iceberg 也有类似的问题,它的 Compaction 机制需要手动或定时执行。
| 维度 | Hudi | Iceberg |
|---|---|---|
| 存储类型 | COW / MOR 两种选择 | 统一存储,无类型区分 |
| ACID 事务 | 支持,基于乐观锁 | 支持,基于快照隔离 |
| 增量查询 | 支持,通过 Incremental Query | 支持,通过 Snapshot 和 Incremental Read |
| 时间旅行 | 支持,通过指定 Commit 时间 | 支持,通过指定 Snapshot ID |
| 小文件管理 | 需要手动触发 Clustering | 需要手动触发 Compaction |
| 社区生态 | 与 Spark、Flink 集成成熟 | 与 Flink 集成更紧密 |
选型建议:
如果你的回填场景以 写为主(比如每天回填大量历史数据),Hudi 的 MOR 类型更适合。如果以 读为主(比如回填后频繁查询),Iceberg 的查询性能更稳定。我个人习惯用 Iceberg,因为它的元数据管理更清晰,排查问题更方便。
3.3 消息队列:Kafka 的角色
Kafka 在离线回填架构中,扮演的是 数据管道 的角色。它不负责计算,也不负责存储,但它连接了所有组件。
具体来说,Kafka 有以下几个关键作用:
- 解耦生产者和消费者:实时数据写入 Kafka,Flink 作业从 Kafka 消费。回填时,Flink 可以从 Hudi/Iceberg 读取历史数据,然后写入 Kafka,再被下游消费。整个过程,上下游完全解耦。
- 数据缓冲:回填时,数据量可能会暴增。Kafka 的持久化机制可以缓冲这些数据,防止下游系统被压垮。我见过一个案例,回填时数据量是平时的 10 倍,如果没有 Kafka 缓冲,下游的数据库直接挂了。
- 消息回溯:Kafka 支持按 Offset 或时间戳回溯消费。这在回填场景中非常有用。比如,你可以指定从某个时间点开始重新消费,而不需要修改代码。
- 多消费者订阅:同一个 Topic 可以被多个消费者组订阅。回填时,你可以启动多个 Flink 作业,分别消费不同的数据范围,实现并行回填。
我的经验:
Kafka 的 Topic 分区数设置很关键。分区数太少,并发度上不去;分区数太多,管理成本高。我一般按 每秒吞吐量 / 单个分区吞吐量 来估算。比如,每秒需要处理 100MB 数据,单个分区能处理 10MB,那就设 10 个分区。另外,消息大小 也要注意,默认的 1MB 限制在回填场景可能不够,需要适当调大。
3.4 整体架构图
下面这张图,展示了离线回填架构的核心组件和流程。你可以看到,Kafka 作为数据管道,连接了实时流、Flink 计算引擎和 Hudi/Iceberg 存储层。
3.5 选型总结
说了这么多,最后总结一下我的选型思路:
- 计算引擎:Flink 优先,除非团队 Spark 技术栈特别强。
- 存储层:Iceberg 优先,元数据管理更清晰。如果写多读少,考虑 Hudi 的 MOR。
- 消息队列:Kafka 是标配,没有替代品。