实战笔记:Spring Boot 结合 Flink CDC 与 Iceberg 落地实时数据湖

最近带团队重构公司的数据底座,把以前那套"Canal + Kafka + Flink + Hive"的老古董换成了"Flink CDC + Iceberg"的实时数据湖。过程中踩了不少坑,也摸索出一些最佳实践。这篇文章不讲虚的,直接从一线开发的视角,聊聊怎么用 Spring Boot 做管控基座,把 Flink CDC 和 Iceberg 串起来,搞定流式 ETL、Schema 自动演进和 Exactly-Once。


1. 为什么要换掉老架构?

以前那套 Lambda 架构,运维同学天天骂娘。Binlog 采集要搞 Canal,消息队列要搞 Kafka,计算要搞 Flink,组件多就算了,链路还特别长。上游数据库稍微改个字段,下游 Flink 任务直接报错挂掉,半夜还得爬起来修数据。

Flink CDC 出来之后,算是把这些痛点按在地上摩擦。它最爽的一点就是"全增量一体化"和"原生 Schema 演进"。不用再去管什么先跑批再切流,也不用担心 DDL 变更导致任务崩溃。对于 Java 开发来说,能少维护几个中间件,就能多活几年。


这里得先澄清一个很多新手容易犯的错:千万别在 Spring Boot 的 main 方法里直接 env.execute() 跑 Flink 流! 生产环境下,Spring Boot 只是作为任务编排、配置管理和提交客户端(Client),真正的 Flink 作业是要打包提交到 Yarn 或 K8s 集群上去跑的。

2.1 依赖管理

Flink 和 Spring Boot 的版本对齐是个玄学,建议 Flink 直接上 1.18,CDC 用 3.0,这样能吃到 CDC 3.0 整库同步的红利。

xml 复制代码
<properties>
    <java.version>11</java.version>
    <spring.boot.version>2.7.18</spring.boot.version>
    <flink.version>1.18.0</flink.version>
    <flink.cdc.version>3.0.0</flink.cdc.version>
</properties>

<dependencies>
    <!-- Flink Core & CDC -->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-streaming-java</artifactId>
        <version>${flink.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-connector-mysql-cdc</artifactId>
        <version>${flink.cdc.version}</version>
    </dependency>
    
    <!-- Iceberg Sink -->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-connector-iceberg</artifactId>
        <version>${flink.version}</version>
    </dependency>
</dependencies>

2.2 配置封装

在 Spring Boot 里,我们通常把 Flink 的环境配置抽离到 application.yml,通过 @Configuration 初始化环境。

java 复制代码
@Configuration
public class FlinkEnvConfig {

    @Bean
    public StreamExecutionEnvironment flinkEnv(@Value("${flink.parallelism:4}") int parallelism,
                                               @Value("${flink.checkpoint.interval:60000}") long cpInterval) {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(parallelism);
        
        // 开启 Checkpoint,生产环境必备
        env.enableCheckpointing(cpInterval, CheckpointingMode.EXACTLY_ONCE);
        // 取消任务时保留 Checkpoint,方便恢复
        env.getCheckpointConfig().setExternalizedCheckpointCleanup(
            CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
            
        return env;
    }
}

3. 扒一扒全增量一体化的底层逻辑

Flink CDC 读 MySQL,不是简单地先 select * 再读 Binlog。如果表有个几千万行,直接查绝对 OOM。

它底层用的是 Chunk 分片机制。简单来说,就是根据主键把大表切成一个个小块(Chunk),每个 Chunk 当作一个独立的小事务来读。读完一个 Chunk,就记录一下位点。等所有 Chunk 都啃完了,再无缝切到 Binlog 监听。这样就算任务中途挂了,重启的时候也能从断点的那个 Chunk 继续,不用从头再来。

java 复制代码
// 实际开发中的 Source 配置
MySqlSource<String> mysqlSource = MySqlSource.<String>builder()
    .hostname("127.0.0.1").port(3306)
    .databaseList("biz_db").tableList("biz_db.orders")
    .username("cdc_user").password("xxx")
    .scanStartupMode(StartupMode.INITIAL) // 默认就是全增量一体化
    // 这个参数很关键,控制 Chunk 大小,表太大就调小点,防止内存打满
    .chunkMetaGroupSize(1024) 
    .deserializer(new JsonDebeziumDeserializationSchema())
    .build();

env.fromSource(mysqlSource, WatermarkStrategy.noWatermarks(), "MySQL CDC Source");

4. 流式 ETL 与双流 Join 的坑

数据进湖前总得洗一洗。Flink 有 DataStream API 和 SQL 两套玩法。我的建议是:简单的过滤、字段映射用 DataStream,性能更好;一旦涉及到多表关联、复杂聚合,老老实实写 Flink SQL。

4.1 双流 Join 实战

实时数仓里最头疼的就是宽表构建。用 Flink SQL 的 Interval Join 是个不错的选择,能有效控制状态大小,防止状态无限膨胀。

java 复制代码
// 注意:Flink 1.15+ 推荐直接使用 StreamTableEnvironment
StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);

// 注意:这里必须把流注册为带时间属性的视图,否则 Interval Join 跑不起来
tEnv.createTemporaryView("orders", orderStream, 
    $("id"), $("amount"), $("user_id"), $("proc_time").proctime());
tEnv.createTemporaryView("users", userStream, 
    $("id"), $("name"), $("level"), $("proc_time").proctime());

// 关联订单和用户,限制时间窗口为 1 小时
String joinSql = "SELECT o.id, o.amount, u.name, u.level " +
                 "FROM orders o " +
                 "JOIN users u ON o.user_id = u.id " +
                 "AND o.proc_time BETWEEN u.proc_time - INTERVAL '1' HOUR " +
                 "AND u.proc_time + INTERVAL '1' HOUR";

Table resultTable = tEnv.sqlQuery(joinSql);

5. Schema 演进:终于不用半夜改代码了

以前上游加个字段,下游 Flink 代码得改,Iceberg 表也得改,还要重启任务。Flink CDC 3.0 终于把这事儿给自动化了。

CDC 3.0 把数据流拆成了 DataEventSchemaChangeEvent。下游的 Iceberg Sink 只要开启 Schema 演进,收到 DDL 变更事件,就会自动去改 Iceberg 的表结构。

如果是整库同步,CDC 3.0 现在强烈推荐用 YAML 配置,不用写一堆 Java 代码:

yaml 复制代码
# pipeline.yaml
source:
  type: mysql
  hostname: localhost
  port: 3306
  username: root
  password: password
  tables: db.\.*
  
sink:
  type: iceberg
  catalog-name: my_catalog
  catalog-properties:
    type: rest
    uri: http://localhost:8181

pipeline:
  name: mysql-to-iceberg
  parallelism: 4
  schema.change.behavior: evolve # 开启 Schema 自动演进

在 Spring Boot 里,我们只需要解析这个 YAML,然后调用 PipelineBuilder 提交就行了。对于不兼容的变更(比如删字段),可以配置 try_evolve 或者 exception,看业务容忍度。


6. Iceberg:告别反人类的 Hive 分区

把数据写进 Iceberg,核心就是用好它的隐藏分区和快照。

以前在 Hive 里,天天搞 dt=2023-10-01 这种按天分区,查询的时候还得在 SQL 里带上分区键,少写一个就全表扫描,坑爹得很。Iceberg 的隐藏分区就优雅多了。

sql 复制代码
-- 建表时直接按小时隐藏分区
CREATE TABLE orders (
    id BIGINT,
    amount DECIMAL(10, 2),
    create_time TIMESTAMP(3)
) PARTITIONED BY (hours(create_time));

用户查询的时候,直接 where create_time > '2023-10-01 12:00:00',Iceberg 底层会自动做分区裁剪,根本不需要用户关心分区逻辑。

写入 Iceberg 的代码:

java 复制代码
Table icebergTable = catalog.loadTable(TableIdentifier.of("db", "orders"));

FlinkSink.forRowData(dataStream)
    .table(icebergTable)
    .tableLoader(TableLoader.fromCatalog(catalog, TableIdentifier.of("db", "orders")))
    .writeParallelism(4) 
    // 如果是 upsert 场景,记得在表定义时指定好主键
    .build();

7. Exactly-Once 与 Checkpoint 调优血泪史

流处理没有 Exactly-Once 等于白搭。Flink 写 Iceberg 用的是两阶段提交(2PC)。Checkpoint 触发时,数据先写到临时文件;等所有 Task 都 ACK 了,再原子性地提交到正式目录。

但在生产环境,Checkpoint 失败是家常便饭。这里分享几个调优参数(注意 Flink 1.15+ 的配置项有变化):

java 复制代码
Configuration conf = new Configuration();
// 1. 开启非对齐 Checkpoint。解决数据反压时 Checkpoint 超时的问题,但会吃内存!
conf.setBoolean("execution.checkpointing.unaligned", true);
// 2. 开启增量 Checkpoint。配合 RocksDB,只上传增量状态,大幅减少 HDFS/S3 的 IO 压力
conf.setBoolean("execution.checkpointing.incremental", true);
// 3. 超时和间隔设置
conf.set("execution.checkpointing.timeout", Duration.ofMinutes(10));
conf.set("execution.checkpointing.min-pause", Duration.ofSeconds(30));

env.configure(conf);

避坑提醒 :开了非对齐 Checkpoint 后,Network Memory 的消耗会直线上升。如果 TaskManager 频繁报 OOM,别犹豫,把 taskmanager.memory.network.fraction 调大,或者直接给 Network Memory 设个固定的下限(比如 1.5GB)。


8. 选型纠结:Kafka、Hudi 还是 Iceberg?

经常有人问,既然有了 Iceberg,还要 Kafka 干嘛?或者 Hudi 和 Iceberg 到底选哪个?

别想着一个组件打天下。

Kafka 是消息队列,毫秒级延迟,适合做实时风控、实时推荐这种对时效性要求极高的缓冲层。但它不支持复杂的 OLAP 查询,Update/Delete 也很弱。

Iceberg 和 Hudi 都是湖格式。Hudi 在处理高频 Upsert 时确实有一手,但 Iceberg 的架构更轻量,尤其是隐藏分区和跟 Spark/Trino 配合的查询性能,目前看来更胜一筹。

我们现在的玩法是"混合双打":极度实时的数据先打给 Kafka,报表和 BI 分析的数据通过 Flink CDC 慢慢沉淀到 Iceberg。最后用 Trino 做个联邦查询,把两边的数据拼起来用。


9. 元数据与血缘:别让数据湖变成数据沼泽

数据湖建好了,如果没有治理,过半年就变成"数据沼泽"了。

Iceberg 的 Catalog 推荐用 REST Catalog,把元数据管理和业务解耦,方便后面接 Apache Atlas 或者 DataHub。

血缘追踪方面,我们接入了 OpenLineage。在 Spring Boot 里通过 Flink 的 Listener 机制,在任务启停时把 DAG 解析出来,推给 Marquez。这样上游 MySQL 改个字段,立马就能在 DataHub 上看到影响了哪些下游报表,再也不用靠人去群里吼了。


10. 生产环境的"保命"指南

最后聊聊部署和运维。现在基本都上 K8s 了,直接用 Flink Kubernetes Operator。

10.1 内存调优

Flink 的内存模型很绕。重点盯住两个:

  1. Network Memory:刚才说了,非对齐 Checkpoint 和大量 Shuffle 时很吃这个,给足。
  2. Managed Memory:如果用 RocksDB 存状态,这个必须给够,不然状态写盘太频繁,性能直接拉胯。

10.2 反压排查

任务跑得慢,第一反应去看 Flink Web UI 的 BackPressure 面板。如果标红(HIGH),说明下游堵了。通常是 Iceberg 写入太慢(比如小文件太多导致 Commit 慢)。解决办法:增加 Sink 并行度,或者调大 Iceberg 的 commit.interval 减少提交频率。

10.3 故障自愈

硬件总会坏的。在 FlinkDeployment CRD 里配好 restart-strategy,用指数退避策略。Pod 挂了 K8s 会自动拉起,Flink Operator 会自动从最新的 Checkpoint 恢复。只要 Checkpoint 没丢,业务就无感知。

yaml 复制代码
# K8s CRD 片段
spec:
  job:
    upgradeMode: savepoint 
  flinkConfiguration:
    restart-strategy.type: exponential-delay
    restart-strategy.exponential-delay.initial-backoff: 10s
    restart-strategy.exponential-delay.max-backoff: 5m

几点掏心窝子的总结

搞实时数据湖,技术选型只是一半,另一半是工程落地和细节调优。

Flink CDC 3.0 的整库同步和 Schema 演进确实香,大幅降低了同步链路的复杂度;Iceberg 的隐藏分区和快照让数据湖真正具备了数仓的分析能力。

但千万别迷信"银弹"。生产环境里,Checkpoint 的调优、内存的精细控制、反压的排查,这些脏活累活才是决定系统能不能稳定跑下去的关键。保持对底层原理的敬畏,多盯监控,少信玄学,才是数据开发该有的样子。


🎁 福利时间

如果你正在备战面试或者想要学习其他知识,给大家推荐一个宝藏知识库,作者整理了一些列 Java 程序员需要掌握的核心知识,有需要的自取不谢。

知识库地址:https://farerboy.com/


相关推荐
SelectDB技术团队7 天前
Apache Doris 5.0 年度版本前瞻(一):构建统一的多模湖仓实时分析平台
大数据·doris·数据湖·湖仓一体·lakehouse·湖仓分析·多模分析
迈巴赫车主2 个月前
湖仓一体(Data Lakehouse)简介
大数据·数据仓库·数据湖·湖仓一体
不吃天鹅肉3 个月前
数据湖Delta Lake 初试
大数据·数据湖
阿坤带你走近大数据3 个月前
Paimon相关概念的介绍
flink·数据湖·paimon
递归尽头是星辰3 个月前
不同架构层级下的多版本设计:从业务设计到微服务与大数据层
数据湖·乐观锁·mvcc·分布式架构·多版本控制
XSKY星辰天合4 个月前
从“能存下”到“训得动”:XSKY XEOS 支撑头部 AI 实验室建设 EB 级数据湖
数据湖·对象存储·分布式存储
hf2000125 个月前
深入分析:Iceberg v3「删除向量(Deletion Vectors, DV)」如何缓解 CDC 场景写放大
大数据·spark·数据湖·湖仓一体·lakehouse
hf2000125 个月前
Apache Iceberg vs Apache Paimon :数据湖表格式深度对比与选型指南
大数据·spark·数据湖·湖仓一体·lakehouse