☰
Flink CDC + Kafka + Iceberg 日志实时入湖入流方案实战
2026/10/14 4:45:26 网站建设 项目流程

前阵子接手了一个内部实时日志平台的重构,老架构是日志采集端写 Elasticsearch,离线任务每小时再往 Hive 里灌一次,实时分析和离线报表两条链路各写各的,口径经常对不上,还得维护两套代码。当时我定的核心方案就是基于 Flink CDC 做日志的实时入湖入流:一条链路把变更日志实时流入 Kafka 供下游流计算消费,另一条链路同时写入 Iceberg 数据湖支撑即席查询和离线报表。整套方案上线后,日志从产生到可分析的时间从小时级压到了秒级,链路也从两条并成一条。这篇就把我的设计思路、选型依据、实操细节和踩过的坑完整写出来,给正在做类似实时链路的朋友一个参考。

1. 整体设计与方案选型:为什么是 Flink CDC + Kafka + Iceberg

1.1 日志实时化的核心矛盾

做日志实时链路,第一件事是搞清楚"日志"到底指什么。我把它分成两类:一类是业务数据库的变更日志,也就是 MySQL 的 binlog、PostgreSQL 的 WAL 这类底层日志,它记录了每一行数据的增删改;另一类是应用产生的业务日志,比如订单状态流转、用户登录行为、风控事件,这些通常以 JSON 或 key-value 的形式写入消息队列。

这两类日志的共性是"只追加、带时间戳、需要被多个下游消费"。而传统架构的问题恰恰在于:要么只入流不入湖,导致历史数据无法高效分析;要么只入湖不入流,实时性又跟不上;更常见的是两条链路各建一遍,实时作业读一遍消息队列,离线任务再从数据库或日志文件抽一遍,双份存储、双份计算,数据口径分裂几乎是必然的。

所以我在设计时定了一条原则:日志数据必须"一次采集、两路分发"。采集端只做一次,然后由统一的 Flink 作业负责同时写 Kafka(入流)和 Iceberg(入湖),下游谁需要谁消费,不再各搞一套 Source。

1.2 选型时对比过的几条路线

CDC 工具这块,我当时重点对比了 Canal、Debezium 和 Flink CDC。Canal 是早年间的常选项,但它和 Flink 的集成需要自己写 adapter,源端格式变更后维护成本很高;Debezium 本身很成熟,不过要单独部署 Kafka Connect 集群,等于凭空多出一套运维组件。Flink CDC 最大的优势是它天然就是 Flink 生态的一部分,不需要额外部署服务,直接以 connector 的形式出现在 Flink SQL 里,而且内置了全量快照加增量日志的无缝衔接机制。

数据湖格式上也做了对比。Hudi、Iceberg、Paimon 这三者目前都能和 Flink 配合,但当时我们团队对 Iceberg 的熟悉度更高,而且在我们的场景里,Iceberg 的 ACID 事务、时间旅行、隐藏分区和流批一体的能力最贴合需求。尤其是它有稳定的 Flink writer 和两阶段提交实现,能保证 checkpoint 语义下的精确一次写入,这对日志这种"只许对不许错"的数据很关键。

1.3 为什么一定要经过 Kafka 而不是直接写数据湖

这里我想多说一句,很多人会问:既然 Flink 都能直接写 Iceberg 了,为什么中间还要过一个 Kafka?我的答案很直接:为了解耦和削峰。

日志的写入天然是突发式的,大促、秒杀、故障回放都会导致流量瞬间飙升。如果 Flink 作业直接把数据怼进 Iceberg,峰值流量会直接转化为大量小文件,而小文件是数据湖性能的杀手,后续的 compaction 和查询都会变慢。放进 Kafka 以后,Flink 作业的消费速度是自己可控的,上游无论怎么突刺,下游都能按最大吞吐稳定写入。

另外,Kafka 承担了"多消费者"的职责。实时数仓里不是只有一个 Flink 作业需要这份日志,实时指标计算要读、风控规则引擎要读、数据湖的写入也要读。数据进 Kafka 以后,每个下游自己维护 offset,互不干扰。这条链路我们在线上跑了近一年,稳定性的核心保障之一就是这个缓冲层的设计。

2. Flink CDC 的核心机制与企业级配置要点

2.1 说说 CDC 连接器底层的原理

Flink CDC 的 MySQL connector 底层其实是嵌入了 Debezium 的引擎,它会伪装成一个 MySQL slave 实例,向主库请求 binlog 流,然后解析成结构化的变更事件。理解这点对排查问题很重要——比如为什么连接 MySQL 必须要配置 server-id,而且多个并行任务要配置不同的 server-id,就是因为 Debezium 在 binlog 复制协议里就是以 slave 身份存在的,同一个 id 会被主库认为是连接中断重连。

Flink CDC 最省心的设计是"全量加增量"的无缝衔接。任务启动时如果配置scan.startup.mode = initial,它会先做一次全量快照,同时从当前位点开始缓存 binlog,快照完成后自动切换到增量模式,全程不需要人工干预。这个机制的实现细节是:全量阶段读取的是表数据,binlog 增量事件在旁边攒着,最后合并对齐,因此不会丢失快照期间的变更。

2.2 企业级架构下的参数配置参考

在实际配置时,有几个参数我会格外注意。第一个是server-id,线上 4 个并行度就必须给 4 个不同的 id,我习惯用5400-5404这样的区间,避免和其他任务的 id 冲突。第二个是connect.timeout和connect.max-retries,网络抖动在分布式环境里太常见了,宁愿重试多几次也不愿意整个 job 挂掉。第三个是scan.incremental.snapshot.chunk.size,这个值控制全量阶段每批扫多少行,默认 8096,在单表几千万行的大表场景我会调到 4096,避免单次扫描内存压力过大。

下面是当时我用的一个比较典型的 CDC source 配置片段:

CREATE TABLE mysql_cdc_source ( id BIGINT, user_id STRING, action STRING, ts TIMESTAMP(3), PRIMARY KEY (id) NOT ENFORCED ) WITH ( 'connector' = 'mysql-cdc', 'hostname' = '10.0.12.18', 'port' = '3306', 'username' = 'cdc_user', 'password' = '********', 'database-name' = 'app_log_db', 'table-name' = 'log_event', 'server-id' = '5400-5404', 'scan.startup.mode' = 'initial', 'debezium.snapshot.mode' = 'schema_only', 'connect.timeout' = '30s', 'connect.max-retries' = '5', 'scan.incremental.snapshot.chunk.size' = '4096' );

注意debezium.snapshot.mode,这个参数在 Flink CDC 里容易被忽略。如果表结构经常变,我建议使用schema_only或when_needed,避免每次重启任务时都去重新全量扫数据;如果表数据量小、结构稳定,直接保留默认值就行。这个取舍完全取决于你的表特征,没有一个配置能通吃所有场景。

2.3 日志场景的时区与类型陷阱

日志数据里有两类东西最坑人:时间戳和 Decimal。我们有一张表的时间字段是timestamp(0),结果入湖以后发现 Iceberg 里时间整体少了 8 小时,排查了半天才发现是连接器默认时区是 UTC,需要在作业配置里显式指定table.local-time-zone = Asia/Shanghai。这个问题在 Flink CDC 1.x 时代尤其明显,很多团队都踩过。

Decimal 精度的问题更隐蔽。MySQL 的decimal(20,4)转成 Flink 的DECIMAL(20,4)看似没问题,但如果源表有decimal(65,30)这种大精度字段,写入 Iceberg 时可能会因为精度截断导致数据校验失败。我的建议是:所有日志表字段在上线前做一次全量字段扫描,凡是 Decimal 都加一个精度映射规则,该转字符串的转字符串,别等到作业跑起来才发现。

3. 核心链路实操:从建表到入流入湖

3.1 整体架构和批流一体设计

这套方案的完整链路长这样:应用服务把业务日志写入 MySQL(或直接写入 Kafka),Flink CDC 读取 MySQL binlog 得到变更事件流,然后在一个 Flink 作业里同时定义两个 sink——一个写 Kafka topic,一个写 Iceberg 表。下游的实时指标作业消费 Kafka topic 做秒级聚合,即席查询和离线报表直接查 Iceberg。

如果把应用日志直接放 Kafka,那么链路就变成:应用日志 → Kafka(原始日志 topic)→ Flink 作业 → Kafka(清洗后的 topic)+ Iceberg。本质上是一样的,核心都是"一个 Flink 作业,两个输出分支"。这样的设计能保证入流入湖使用的是同一份处理逻辑,处理口径完全一致,也就不会出现实时数据和离线数据对不上的问题。

3.2 Flink SQL 中的双写实现

入流和入湖在 Flink SQL 里实现起来很直观。先创建一个 Kafka sink 表,再创建一个 Iceberg sink 表,然后用一条INSERT INTO语句同时写入两个表,或者用两条INSERT INTO语句在同一个 checkpoint 周期内完成双写。

我当时用的是在一个作业里写两条 insert 语句的方式:

-- 写入 Kafka,供实时消费 INSERT INTO kafka_log_sink SELECT id, user_id, action, ts, PROCTIME() AS process_time FROM mysql_cdc_source; -- 写入 Iceberg,供分析查询 INSERT INTO iceberg_log_sink SELECT id, user_id, action, ts, CURRENT_TIMESTAMP AS ingest_time FROM mysql_cdc_source;

两个 sink 在同一个作业里跑,Flink 的 checkpoint 机制会保证两条链路要么都成功、要么都失败重试,不会出现 Kafka 里有数据而 Iceberg 里没有的情况。这里有一个关键点:Iceberg 的 Flink writer 实现了两阶段提交协议,所以当 checkpoint 完成时,Iceberg 的快照也就提交了,Kafka 的写入也是事务性的,这样双写的一致性就得到了保证。

3.3 入湖前的数据治理与分区策略

日志入湖不是把原始数据直接怼进去就完事的,分区策略直接决定后续查询的效率和成本。我选的方案是按天分区加隐藏分区字段,Iceberg 支持隐藏分区,即使原始数据里没有分区列,也可以基于时间字段自动派生。

建表语句长这样:

CREATE TABLE iceberg_log_sink ( id BIGINT, user_id STRING, action STRING, ts TIMESTAMP(3), dt STRING, ingest_time TIMESTAMP(3) ) PARTITIONED BY (dt) WITH ( 'connector' = 'iceberg', 'catalog-name' = 'log_iceberg', 'catalog-type' = 'hive', 'warehouse' = 'hdfs://namenode/log_warehouse', 'write.format.default' = 'parquet', 'write.target-file-size-bytes' = '134217728', 'write.upsert.enabled' = 'false' );

write.target-file-size-bytes我设成了 128MB,这是 Iceberg 比较推荐的目标文件大小。日志数据量大,如果设得太小会产生大量小文件,设得太大又会导致写入延迟增高。实际效果是,我们日均日志量大概 3 TB,小文件数量比最初默认配置时减少了 70% 以上。

3.4 性能调优:从吞吐到延迟的平衡

写日志链路,绕不开吞吐和延迟这对矛盾。Flink CDC 读取 binlog 是单并行度读取的(因为 binlog 是有序的),这在高并发场景下会成为瓶颈。我的处理方式是:source 保持单并行度保证顺序,但 sink 端并行度可以放开到 8 或 16,让下游写入通过重新分区来并行。

更重要的调优点是 checkpoint 配置。日志入湖的延迟和 checkpoint 间隔强相关,因为 Iceberg 只有在 checkpoint 完成时才提交快照。我当时把 checkpoint 间隔设成了 60 秒,这样日志从产生到能在 Iceberg 查到,最坏情况也就一分钟。如果想更实时,可以压到 30 秒,但代价是 Iceberg 的小文件会变多,要自己权衡。Kafka 入流的延迟不受 checkpoint 影响,只要上游有数据,消费端就能实时拿到,通常能做到秒级。

4. 上线后的稳定性保障与常见问题排查

4.1 监控指标与告警体系

架构上线只是开始,真正考验功力的是稳定运行。日志链路最核心的监控指标有三个:source 端延迟(通过对比 binlog 位点和当前时间)、checkpoint 完成时间、以及 Iceberg 小文件数量。我们当时的做法是在 Prometheus 里配了三套告警规则:source 延迟超过 5 分钟告警、checkpoint 连续三次超时告警、单日小文件增长率超过阈值告警。

这里有一个容易被忽视的点:Flink CDC 作业不会因为你设置了connect.max-retries就永远不失败。如果源表做了 DDL 变更,比如新增列或修改列类型,Flink CDC 与 Iceberg sink 之间的 schema 同步如果没配好,作业会直接报错退出。所以上线前一定要确认 Iceberg 表的 schema 兼容策略,否则一次业务发版就会导致整个链路停摆。

4.2 日志链路最常见的五个坑

第一个坑是 binlog 被清理导致作业启动失败。MySQL 默认 binlog 保留时间可能只有几天,如果 Flink 作业停了一周再启动,之前记录的 binlog 位点已经失效,作业会报The slave is connecting ... has the wrong position。解决方式是让 CDC 作业恢复时自动切换到最近的有效位点,但这会丢失停机期间的增量数据,所以更稳妥的做法是提前和 DBA 约定,确保日志库的 binlog 保留至少 7 天。

第二个坑是目标表主键冲突。如果日志源是业务表,而业务表的主键在业务逻辑里发生了更新(比如先删后插),Flink CDC 在 2.x 版本中默认情况下对这类场景支持的有限,容易在 sink 端出现 unique constraint violation。我们的做法是:日志入湖表不设置主键约束,只保留分区字段,用"追加模式"写入,规避了这个问题。

第三个坑是并行度变化导致的数据乱序。如果任务重启后并行度改小了,从 Kafka 读出来的数据可能在时间维度上出现轻微乱序。对于大多数分析场景这无所谓,但如果有下游做时序聚合,就一定要在 SQL 里显式使用WATERMARK并容忍乱序,或者干脆在查询层做去重。

第四个坑是 Iceberg 元数据文件膨胀。Iceberg 每次 commit 都会生成新的 metadata 文件,日志量大的时候,元数据文件本身会成为 NameNode 的压力。我们的应对是定期执行expire_snapshots清理过期快照,同时开启 metadata 的压缩。

第五个坑是下游消费 Kafka 时遇到 offset 越界。Kafka 的日志清理策略如果设置的 retention 太短,而实时分析作业恰好有一阵子没消费(比如重启、出故障),就会出现 offset 越界告警。我的建议是:Kafka 的日志保存时间要比下游作业可能的最大停机时间长 2 倍以上,我们是按下游最长停机 6 小时来反推 retention 设置的。

4.3 一次完整的故障复盘

有一次线上出现了一个特别典型的问题:日志入湖延迟从 1 分钟慢慢涨到 30 分钟,但 Flink web UI 显示作业还在正常跑,没有任何 ERROR。排查过程是这样的——先看 source 端指标,binlog 位点没有落后,说明 CDC 读取没问题;再看 Kafka sink 指标,写入速率正常;最后看 Iceberg sink 的 commit 耗时,发现 commit 延迟越来越高,每个 checkpoint 的提交时间从 2 秒涨到 5 秒、10 秒,不断累积。

根因是那一周日志量涨了 40%,而 Iceberg 表的 manifest 文件因为一直没有做合并优化,commit 时需要读取和重写的元数据成倍膨胀。解决办法有两个:一是调大 checkpoint 间隔,减少 commit 频率;二是对 Iceberg 表执行rewrite_manifests和expire_snapshots操作,压缩元数据规模。从那以后,我把 Iceberg 的元数据维护任务加入了每周定时运维清单,再没出过同类问题。

5. 从日志入湖到数据资产化:一些进阶实践

5.1 Schema 变更的自动化处理

企业级日志链路躲不开 schema 变更,尤其是业务方经常在日志里加字段。Flink CDC 3.x 开始支持基于 Pipeline 的整库同步和 schema 变更同步,如果用的是 2.x 的 connector,那就得自己处理 schema 兼容。我的做法是:在 Iceberg 表上开启 schema 自动演进,允许新增字段和删除字段,不改变已有字段的类型。这样业务方在前端加了个参数,日志入湖后自动多出一列,老数据不受影响。

当然自动演进不代表无脑放行。类型变更(比如 int 改 string)我都是禁止的,这类变更必须先走审核流程,由数据团队评估对下游报表的影响后再手动处理。这个约束是靠 Iceberg 的schema.update权限控制实现的,说白了就是"自动加列、手动静默"。

5.2 数据质量检查在链路中的嵌入

日志入湖后不等于万事大吉,脏数据随时可能出现。我在 Flink 作业里加了一层数据质量规则检查,对常见的异常情况做实时过滤和标记:时间字段为空的打上异常标签、action 不在枚举范围内的写入死信队列、主键为 null 的丢弃并计数。

这一层设计其实很轻量,就是在 SQL 里加几个 CASE WHEN 判断,然后多写一个 Kafka topic 专门存放异常数据。它的价值在于:问题数据不会再污染下游的数据湖和实时计算,而且异常数据有了统一出口,业务方可以直接消费这个 topic 排查自己的问题,不用在几万条日志里大海捞针。

5.3 成本优化:日志保留策略与冷热分层

日志数据的价值密度是随时间快速衰减的。我们按日志类型制定了不同的保留策略:线上核心业务日志保留 90 天,中间层日志保留 30 天,调试日志保留 7 天。Iceberg 的时间旅行和快照过期机制让这个策略落地很自然。

成本优化还有一招:对超过 30 天的历史分区执行压缩和列裁剪。因为老日志的分析维度比较固定,只保留常用字段可以把存储量再压缩一半。实际操作中我写了套定时任务,每月跑一次分区归档,把冷数据统一转成高压缩比的列式存储格式,然后删掉原始小文件。这套策略上线以后,存储成本降了大概 40%,查询性能反而更稳定了。

6. 一些私人的经验分享

如果让我总结这套 Flink CDC 日志实时入湖入流方案最核心的心得,我会说:技术选型其实是次要的,关键是先把"数据从哪来、到哪去、谁在用"这三件事想清楚。方案里每一个设计决策背后都有真实的业务诉求——为什么要过 Kafka,是因为下游消费方太多、流量波动太大;为什么要入 Iceberg,是因为分析场景需要 ACID 和快照能力;为什么要用 Flink CDC,是因为它让批流链路共用一套处理逻辑,不再维护两套代码。

最后分享一个小建议:如果你所在的团队刚开始做实时日志链路,不要一上来就追求最复杂的架构。先用 Flink CDC 加上一个最简单的 Iceberg 表把链路跑通,确认数据能实时入湖入流了,再逐步补上监控、治理、成本优化这些企业级能力。架构不是一步到位的,而是跟着真实的业务痛点一步步长出来的。这套方案运行一年多的结果证明,只要底层链路扎实,上层的能力可以根据需要不断生长,而当你需要扩展新下游时,你会发现当初"一次采集、两路分发"这个决定,省掉的远不止是运维成本。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询