数据集成层:Kafka + Flink CDC 入湖

数据集成层解决"数据如何又快又稳地进湖":批式同步负责存量与低时效数据, 实时 CDC 负责增量与分钟级时效数据,而 Kafka 是实时链路的"主动脉"。 本页从 Kafka 机制、Flink CDC 原理、端到端一致性语义,到链路架构、容量规划与适用边界, 完整拆解湖仓一体的数据接入体系。

本层定位:两条入湖通道

湖仓一体的存储底座(对象存储 + 开放表格式)只回答"数据以什么形态放", 数据集成层回答"数据从哪里来、多久到、以什么一致性保证到"。工程上收敛为两条通道:

批式同步通道:定时(小时/天级)全量或增量抽取业务库数据, 经 Spark/DataX 等工具清洗后写入 Iceberg。链路简单、回溯容易、成本低, 适合维表快照、低时效报表、冷数据归档。
实时 CDC 通道:捕获数据库 binlog 的行级变更, 经 Flink CDC → Kafka → Flink 加工后以 upsert 语义写入 Iceberg, 分钟级(可至秒级)数据可见,适合实时大屏、风控、监控类消费。

两条通道最终汇入同一份湖仓存储,整体分层如下:

业务数据源:MySQL / Postgres / Oracle / 日志 / 第三方 API
捕获与传输:Flink CDC(无锁快照 + binlog 订阅)· 批式同步(DataX/Spark)
实时主干:Kafka(分区/副本/持久化,削峰 + 多订阅 + 重放)
流式加工:Flink 消费 → 清洗 / 维表关联 / 聚合(ODS → DWD)
湖仓落地:Iceberg on 对象存储(快照提交,支持 upsert 与 Schema 演进)
最佳实践:两条通道不是二选一——同一张表可以"实时通道保时效、每日批通道保对账"。 以 Kafka 为中心的实时主干天然支持多个下游(Flink 加工、审计落档、风控旁路)订阅同一份数据流。

Apache Kafka:实时主干

核心机制:分区 / 副本 / ISR

Kafka 是分布式提交日志(Commit Log),实时链路的主动脉。三个核心概念决定了它的吞吐、 顺序与可靠语义:

Leader 故障时,Controller 从 ISR 中选举新 Leader——只有 ISR 内的副本才有资格当选, 因此 ISR 数量与 min.insync.replicas 共同构成持久化下限: replication.factor=3, min.insync.replicas=2 表示"最多容忍 1 台 broker 宕机仍可安全写入"。

broker 核心配置(server.properties)

# Kafka broker 核心配置(server.properties)
broker.id=1
listeners=PLAINTEXT://kafka-1:9092
log.dirs=/data/kafka1,/data/kafka2

# 网络与 IO 线程:万兆网络高吞吐场景的常规起点
num.network.threads=8
num.io.threads=16
socket.send.buffer.bytes=1048576
socket.receive.buffer.bytes=1048576

# 新建 topic 的默认分区数与副本数(兜底值,建 topic 时显式指定更稳)
num.partitions=6
default.replication.factor=3
min.insync.replicas=2

# 数据保留:时间与大小双上限,命中任一即删除旧 segment
log.retention.hours=72
log.retention.bytes=214748364800
log.segment.bytes=1073741824          -- segment 1GB,滚动后才能被过期清理
log.retention.check.interval.ms=300000

# 副本拉取线程与日志压缩(用于留痕/对账类 topic)
num.replica.fetchers=4
log.cleaner.enable=true

生产者可靠性:acks 与幂等

acks 决定"写入多少副本才算成功":0(发出即忘,可能丢)、 1(Leader 写入即成功,Leader 宕机可能丢)、all(ISR 全部写入才成功,配合 min.insync.replicas≥2 可做到不丢)。配合幂等生产者 (enable.idempotence=true,broker 按 PID + 序列号去重)与有限重试, 可在"不丢"与"不重"之间取得工程上的最优解:

Properties props = new Properties();
props.put("bootstrap.servers", "kafka-1:9092,kafka-2:9092,kafka-3:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

// 可靠性三件套:acks=all + 幂等 + 有限重试(单分区幂等有序)
props.put("acks", "all");
props.put("enable.idempotence", "true");
props.put("retries", "100");
props.put("max.in.flight.requests.per.connection", "5");   // 幂等开启时 ≤5 即可保序

// 吞吐三件套:批量 + 压缩 + 攒批等待
props.put("batch.size", 131072);
props.put("linger.ms", "20");
props.put("compression.type", "lz4");
props.put("buffer.memory", 67108864);

// 跨事务幂等(如按 order_id 建补偿)由下游保证,Kafka 只保证单分区不重不丢
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
producer.send(new ProducerRecord<>("ods.shop.orders", orderId, payload),
              (meta, e) -> { if (e != null) e.printStackTrace(); });
注意:acks=all 在 ISR 收缩为 1(即只剩 Leader)时会退化成"Leader 写入即成功"。 生产环境务必同时设置 min.insync.replicas=2,让 ISR 不足时写入直接报错而不是静默降级。

容量规划:分区数估算公式

分区不是越多越好(更多分区意味着更多文件句柄、更久的 leader 切换与 rebalance、更大的 controller 元数据压力),规划的核心公式只有一个:

分区数 = 目标峰值吞吐 ÷ 单分区预期吞吐 × 预留系数

其中:
- 单分区生产吞吐经验值:10 MB/s(顺序写、页缓存充裕时的保守估计)
- 预留系数:1.5 ~ 2.0(业务增长 + 大促脉冲 + 故障重放)

数值示例:业务库 binlog 解析后峰值 100 MB/s,规划 2 倍冗余:

目标峰值吞吐 = 100 MB/s × 2 = 200 MB/s
分区数       = 200 ÷ 10 = 20 → 向上取整到 broker 数的整数倍 → 24 个分区

校验消费端:24 分区 × 每消费者并行处理 8 MB/s = 192 MB/s ≈ 达标
校验副本流量:200 MB/s × 3 副本 = 600 MB/s 集群写入带宽
  → 8 台 broker 均摊 75 MB/s/台,万兆网卡余量充足,千兆网卡会成为瓶颈

retention 策略

提示:磁盘需求 = 峰值吞吐 × retention 时长 × 副本数 ÷ 压缩比。 上例 72h 保留:100 MB/s × 259200s × 3 副本 ÷ 4(lz4)≈ 19 TB,8 台 broker 每台约 2.4 TB, 再留 30% 余量规划每台 3.2 TB 以上数据盘。

无锁快照 + binlog 原理

Flink CDC 底层复用了 Debezium 的 binlog 解析能力,但把"采集"与"计算"合并在 Flink 引擎内完成。 其革命性在于增量快照算法(无锁快照),读取存量数据不再需要锁表:

  1. 分块(Chunking):按主键把表逻辑切分为多个 chunk(如每块 8 万行);
  2. 并发快照:多个 chunk 并行读取,读的是 MVCC 一致性视图(REPEATABLE READ), 不加全局锁、不阻塞业务写入;
  3. 高低水位(high-low watermark):每块快照前后各记录一次当前 binlog 位点, 快照结束后回放这段时间的 binlog 增量,并按 op_ts 与快照数据合并去重;
  4. 无缝切换:全部分块完成后,source 优雅切换为纯 binlog 订阅模式,全程不断流。

与早期 CDC 工具的对比:

能力 Canal Debezium(Kafka Connect) Flink CDC
部署形态 独立服务伪装 MySQL 从库 Kafka Connect 集群中的连接器 Flink 作业内的 source 算子
存量同步 需自行编写全量任务 + 锁表 需配合其他全量工具 内置增量快照,无锁、并发、断点可续
精确一次语义 at-least-once + 下游幂等 依赖 Connect 语义,较难保证 与 Flink checkpoint 集成,端到端可控
数据加工 只采集,需下游再处理 只采集,落 Kafka 采集即计算:清洗、维表 JOIN、聚合一体完成
运维链路 Canal + 数据总线两套 Connect + Kafka + 下游消费 一个 Flink 作业闭环

Exactly-Once 到 Iceberg:端到端语义拆解

端到端链路 MySQL → Flink CDC → Kafka → Flink → Iceberg 上, "精确一次"要逐段拆解,工程上有两条路线:

为什么幂等可行:CDC 流按主键(order_id)回放 upsert,同一主键写两次, 第二次只是覆盖/删除重放,Iceberg 快照的最终状态与"恰好一次"完全一致。
前提:主键必须真实唯一且稳定(禁止用无业务语义的自增列跨环境同步); 聚合类中间结果不能靠幂等去重,必须走回撤流(retract)语义。

断点续传与位点管理

Flink 的 checkpoint 是位点管理的载体:每次 checkpoint 把各 source 的消费位点 (Kafka 分区 offset / binlog 文件名+位置)连同算子状态(维表缓存、聚合中间态)一起持久化到状态后端。 作业故障重启后从最近一次成功的 checkpoint 恢复,数据既不丢也不重算:

# 提交作业:开启 checkpoint、RocksDB 状态后端、保留多点恢复
$FLINK_HOME/bin/sql-client.sh \
  -f sql/ods_orders_to_iceberg.sql \
  -Dexecution.checkpointing.interval=60s \
  -Dstate.backend=rocksdb \
  -Dstate.checkpoints.dir=s3://lake/flink/ckpt/ods_orders \
  -Dstate.checkpoints.num-retained=10 \
  -Drestart-strategy=fixed-delay,attempts=10,delay=30s

# 故障后从指定 checkpoint 恢复(断点续传)
$FLINK_HOME/bin/flink run -s s3://lake/flink/ckpt/ods_orders/chk-5832 ...

完整 Flink SQL 作业:mysql-cdc → 维表关联 → Iceberg

-- ============================================================
-- 作业:shop.orders CDC 实时入湖(含维表打宽),目标 Iceberg ODS/DWD
-- ============================================================
SET 'execution.checkpointing.interval' = '60s';
SET 'execution.checkpointing.min-pause' = '30s';
SET 'table.exec.sink.upsert-materialize' = 'AUTO';
SET 'pipeline.auto-watermark-interval' = '3s';

-- 1. 源表:MySQL CDC(增量快照无锁读取存量 + binlog 增量)
CREATE TABLE ods_orders_src (
    order_id   BIGINT,
    user_id    BIGINT,
    sku_id     BIGINT,
    amount     DECIMAL(12, 2),
    status     TINYINT,
    pay_time   TIMESTAMP(3),
    proc_time  AS PROCTIME(),                          -- 处理时间:维表 lookup 用
    op_ts      TIMESTAMP(3) METADATA FROM 'op_ts' VIRTUAL,  -- 变更发生时间
    PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
    'connector' = 'mysql-cdc',
    'hostname' = 'mysql-master.shop.svc',
    'port' = '3306',
    'username' = 'flink_cdc',
    'password' = '******',
    'database-name' = 'shop',
    'table-name' = 'orders',
    'scan.incremental.snapshot.enabled' = 'true',       -- 无锁增量快照
    'scan.incremental.snapshot.chunk.size' = '8096',    -- 按主键分块,控制单块大小
    'scan.startup.mode' = 'initial',                    -- 存量 + 增量
    'server-time-zone' = 'Asia/Shanghai'
);

-- 2. 维表:商品维表 lookup JOIN(缓存 1 万行 / 60 秒)
CREATE TABLE dim_sku (
    sku_id    BIGINT,
    sku_name  STRING,
    category  STRING,
    PRIMARY KEY (sku_id) NOT ENFORCED
) WITH (
    'connector' = 'jdbc',
    'url' = 'jdbc:mysql://mysql-master:3306/shop',
    'table-name' = 'sku',
    'username' = 'flink_cdc',
    'password' = '******',
    'lookup.cache.max-rows' = '10000',
    'lookup.cache.ttl' = '60s'
);

-- 3. 目标表:Iceberg(v2 + upsert,写入与 checkpoint 对齐提交)
CREATE TABLE dwd_orders_lake (
    order_id   BIGINT,
    user_id    BIGINT,
    sku_id     BIGINT,
    sku_name   STRING,
    category   STRING,
    amount     DECIMAL(12, 2),
    status     TINYINT,
    pay_time   TIMESTAMP(3),
    dt         STRING,
    PRIMARY KEY (order_id) NOT ENFORCED
) PARTITIONED BY (dt) WITH (
    'connector' = 'iceberg',
    'catalog-name' = 'lakehouse',
    'catalog-type' = 'hive',
    'uri' = 'thrift://hive-meta:9083',
    'warehouse' = 's3://lake/warehouse',
    'format-version' = '2',                             -- v2 支持 equality delete(upsert)
    'write.format' = 'parquet',
    'write.upsert.enabled' = 'true',                    -- 主键 upsert:CDC 语义核心开关
    'write.target-file-size-bytes' = '134217728',       -- 目标文件 128MB,抑制小文件
    'write.metadata.metrics.default' = 'truncate(16)'   -- 列统计,供下游查询文件跳过
);

-- 4. 消费写入:CDC 变更流 + 维表打宽 → 按天分区入湖
INSERT INTO dwd_orders_lake
SELECT o.order_id, o.user_id, o.sku_id,
       s.sku_name, s.category,
       o.amount, o.status, o.pay_time,
       DATE_FORMAT(o.pay_time, 'yyyy-MM-dd') AS dt
FROM ods_orders_src AS o
LEFT JOIN dim_sku FOR SYSTEM_TIME AS OF o.proc_time AS s
  ON o.sku_id = s.sku_id;
最佳实践:checkpoint 间隔是"延迟 × 小文件 × 恢复粒度"的三角权衡—— 60s 是 CDC 入湖的常用起点(秒级延迟需求可降到 10~30s,但必须同步加大 write.target-file-size-bytes 或引入定期 compaction,否则小文件会淹没元数据)。

实时入湖链路架构

分层链路全景

生产级实时入湖链路的标准形态:CDC 采集层解耦业务库,Kafka 主干削峰与多播, Flink 加工层承担清洗与打宽,Iceberg 以快照粒度落地。前一节的"直连"示例适合单表简单场景; 一旦表数量、下游消费者增多,中间的 Kafka 主干就不可或缺:

业务库 MySQL 订单 / 用户 / 商品 Flink CDC 无锁快照 + binlog Schema Registry schema 演进协同 Kafka 主干 ods.shop.* 分区/副本 Flink 消费加工 清洗 / 维表 JOIN / 聚合 其他订阅者 审计 / 风控 / 审计旁路 Iceberg 湖仓 ODS / DWD 对象存储 · 快照提交 · upsert

延迟与吞吐的权衡

手段 延迟收益 吞吐代价
缩短 checkpoint 间隔(60s → 10s) 数据可见延迟近似减半 快照提交频繁 → 小文件激增,必须配套 compaction
降低攒批(linger/批大小) 采集端延迟下降 Kafka 吞吐下降、broker CPU 上升
增大 Flink 并行度 积压时端到端延迟下降 资源成本线性上升;超过分区数后无增益
去掉 Kafka 直连(CDC → Iceberg) 少一跳,延迟最低 失去削峰缓冲与多订阅能力,链路耦合变高

乱序与回撤流处理

Schema Evolution:新加列的上下游协同

上游业务库加列(如 orders 新增 coupon_id)时,链路各环节需按序协同:

  1. 业务库:执行向后兼容的 DDL——ADD COLUMN coupon_id BIGINT DEFAULT NULL, 避免直接 rename/not null 这类破坏性变更;
  2. Schema Registry:注册新 schema 版本,校验兼容性(向后兼容), Kafka 消息按新 schema 写入,老消费者仍可跳过未知字段;
  3. Iceberg:同步执行 ALTER TABLE ... ADD COLUMN(元数据操作,秒级完成, 不重写数据文件);
  4. Flink 作业:更新 SELECT 列表与目标表定义,从 savepoint 重启 恢复位点与状态,历史数据新列自动补 NULL;
  5. 下游消费:BI/查询引擎通过 catalog 刷新感知新列,报表按需纳管。
-- 湖仓侧:加列(元数据操作,不重写文件)
ALTER TABLE lakehouse.dwd.dwd_orders
ADD COLUMN coupon_id BIGINT AFTER amount;

-- Flink 作业侧:改 SELECT 后从 savepoint 原地重启,位点与状态无损迁移
-- bin/flink stop --savepointPath s3://lake/flink/sp/ dwd_orders_job && 重启
注意:列删除、类型收窄(STRING → INT)、重命名对 CDC 链路是破坏性变更, 需走"新列双写 → 切读 → 删旧列"的迁移流程,切忌直接改名导致整条链路一夜故障。

批式集成

并非所有数据都值得上实时链路。批式通道以"定时全量/增量抽取 + 批量写湖"的方式, 用更低的复杂度覆盖了大部分数据源,是湖仓数据供给的基座:

维度 Spark 批同步 实时 CDC 链路
数据时效 T+1 / 小时级 分钟级 ~ 秒级
数据形态 快照(某时刻全量状态) 变更流(upsert 最新状态 + 事件留痕)
复杂转换 擅长:重 JOIN、回刷、重算历史分区 只做轻量清洗打宽,复杂逻辑易致状态膨胀
故障恢复 失败重跑整个分区即可,语义简单 依赖 checkpoint/savepoint,运维门槛高
资源成本 按时启停,弹性低成本 常驻资源 + Kafka + 状态存储
适用场景 维表快照、低时效报表、历史回刷、对账修正 实时大屏、风控、监控告警类消费

DataX / Sqoop 的定位

-- Spark 批同步:每日快照写 Iceberg 天分区(overwrite 指定分区,幂等重跑)
spark-sql \
  --conf spark.sql.sources.partitionOverwriteMode=dynamic \
  -e "
INSERT OVERWRITE lake.ods.sku_snapshot PARTITION (dt = '2026-11-04')
SELECT sku_id, sku_name, category, price
FROM jdbc_src.sku
WHERE updated_at >= '2026-11-03';"

容量规划方法

容量规划是把"业务流量"翻译成"中间件与集群资源"的系统方法,四步闭环:

步骤一:采集峰值流量

从业务库监控取 binlog 生成速率的 P99 峰值(而非平均值),并统计表数量与单表大小, 得到入口流量画像。示例采集结论:平均 15 MB/s,大促峰值 60 MB/s,4 张核心大表。

步骤二:确定预留倍数

在峰值之上再预留增长与缓冲:×3 是常见工程系数 (1 倍当前峰值 + 1 倍年度增长 + 1 倍故障重放/回溯)。 本例目标吞吐 = 60 MB/s × 3 = 180 MB/s。

步骤三:反推分区与资源

Kafka 分区数  = 180 ÷ 10(单分区 10 MB/s)= 18 → 取 24(broker 数整数倍)
Kafka 副本带宽 = 180 × 3 = 540 MB/s(集群口径),8 broker 均摊 67.5 MB/s → 万兆达标
Kafka 磁盘    = 180 MB/s × 72h × 3 副本 ÷ 4(lz4) ≈ 33 TB → 每台 4.2 TB × 1.3 余量 ≈ 5.5 TB

Flink source 并行度 = 24(= 分区数,再大无增益)
Flink 处理能力      = 每 parallelism 6 MB/s → 24 × 6 = 144 MB/s
                     -- 若需扛满 180 MB/s,并行度提到 32(Kafka 分区同步扩)
Flink 资源          = 每 parallelism 1 core + 2 GB 堆 → 约 32 core / 64 GB(含 JM 冗余)

Iceberg 小文件     = 60s checkpoint × 32 并行度 ≈ 每分钟 32~64 个文件
                     → 每日 4.6~9.2 万文件 → 必须规划每小时 compaction 作业

步骤四:压测验证

  1. Kafka:kafka-producer-perf-test 以 180 MB/s 打流 1 小时, 观察 broker 延迟(request latency)与 ISR 稳定性;
  2. 端到端:录制真实 binlog 回放至 2 倍峰值,观察 Flink 反压(backpressure)、 checkpoint 时长(应 < 间隔的 1/3,即 < 20s)与 consumer lag 是否归零;
  3. 落地:检查 Iceberg 提交延迟、manifest 数量与查询引擎的文件跳过效率, 确认 compaction 节奏跟得上;
  4. 留档:把压测结论固化为容量基线,每季度或大促前复测。
最佳实践:容量规划的核心输出不是一张配置表,而是 "流量指标 → 资源水位"的换算关系(如 10 MB/s/分区、6 MB/s/parallelism), 流量翻倍时按关系式线性扩容即可,避免每次都从零推演。

适用边界

何时直接 CDC 入湖,而非经 Kafka

经 Kafka 的判断信号:下游 ≥ 2 类消费者、需要保留重放窗口、 多团队按需订阅、未来可能新增风控/审计旁路——出现任意一条,Kafka 主干的投入就开始回本。
直连的代价:失去削峰缓冲(上游脉冲直接冲击 Flink)、 消费逻辑变更只能靠 savepoint 而无法重放历史、采集与加工耦合(改加工逻辑要动采集作业)。

何时不该上实时链路

总结:数据集成层的准绳——Kafka 为实时主干、Flink CDC 为采集与加工引擎、 Iceberg 为唯一事实源;批式与实时两条通道按消费时效分工而非互相替代; 每一段链路都先问"谁消费、要什么时效、能容忍什么语义",再决定引入什么组件。 "为了实时而实时"是湖仓集成层最常见也最昂贵的反模式。