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 有以下几个关键作用:

  1. 解耦生产者和消费者:实时数据写入 Kafka,Flink 作业从 Kafka 消费。回填时,Flink 可以从 Hudi/Iceberg 读取历史数据,然后写入 Kafka,再被下游消费。整个过程,上下游完全解耦。
  2. 数据缓冲:回填时,数据量可能会暴增。Kafka 的持久化机制可以缓冲这些数据,防止下游系统被压垮。我见过一个案例,回填时数据量是平时的 10 倍,如果没有 Kafka 缓冲,下游的数据库直接挂了。
  3. 消息回溯:Kafka 支持按 Offset 或时间戳回溯消费。这在回填场景中非常有用。比如,你可以指定从某个时间点开始重新消费,而不需要修改代码。
  4. 多消费者订阅:同一个 Topic 可以被多个消费者组订阅。回填时,你可以启动多个 Flink 作业,分别消费不同的数据范围,实现并行回填。

我的经验:

Kafka 的 Topic 分区数设置很关键。分区数太少,并发度上不去;分区数太多,管理成本高。我一般按 每秒吞吐量 / 单个分区吞吐量 来估算。比如,每秒需要处理 100MB 数据,单个分区能处理 10MB,那就设 10 个分区。另外,消息大小 也要注意,默认的 1MB 限制在回填场景可能不够,需要适当调大。

3.4 整体架构图

下面这张图,展示了离线回填架构的核心组件和流程。你可以看到,Kafka 作为数据管道,连接了实时流、Flink 计算引擎和 Hudi/Iceberg 存储层。

离线回填架构核心组件 实时数据源 Kafka 消息队列 Flink 计算引擎 Hudi / Iceberg 离线数据源 说明:实线为实时数据流,虚线为离线回填数据流。Kafka 作为数据管道,连接所有组件。

3.5 选型总结

说了这么多,最后总结一下我的选型思路:

  • 计算引擎:Flink 优先,除非团队 Spark 技术栈特别强。
  • 存储层:Iceberg 优先,元数据管理更清晰。如果写多读少,考虑 Hudi 的 MOR。
  • 消息队列:Kafka 是标配,没有替代品。