摘要
把数据库变更同步到 Iceberg,真正困难的不是把一条新记录写成 Parquet,而是处理一条 UPDATE 或 DELETE:
当流处理程序只知道主键
id = 42时,怎样找到 Iceberg 中旧记录所在的 data file 和 row position?
Apache Iceberg 官方 Kafka Connect sink 至今没有接受原生 upsert(PR #14797) 实现,并不是社区认为 CDC 没有价值,而是一个轻量 sink connector 很难同时承担全局主键索引、跨批次更新定位、状态恢复和 delete file 维护。已有提案只能在同一批次内利用已知位置生成 deletion vector;跨批次更新仍需生成 equality delete,并把昂贵的定位和清理工作留给查询引擎或外部 compaction。
Flink 的区别在于,它本来就是一个有状态流处理引擎。它能够按 key 分发数据,以 checkpoint 保存状态,并在 Iceberg 1.12.0 中通过 convertEqualityDeletes() 维护主键到物理位置的索引,将 equality delete 转换成 Iceberg V3 deletion vector。
S3 Tables 不能凭空让官方 Kafka Connect sink 获得 upsert 语义,但它能补上社区最担心的另一半:自动执行 compaction、应用 row-level deletes、清理快照和无引用文件。它把原本需要用户部署 Spark 或其他维护作业的工作,变成托管的 Iceberg table maintenance。
因此,S3 Tables 的核心优势不是"消灭 upsert 成本",而是:
把 CDC 的变更语义交给合适的流处理组件,把持续且不可避免的表维护交给托管服务。
一、Iceberg Upsert 的本质:逻辑主键不等于物理位置
假设数据库产生一条变更:
text
UPDATE customer SET level = 'gold' WHERE id = 42;
CDC 事件通常能够告诉下游:
text
primary key = 42
after.level = gold
operation = update
但 Iceberg 中的旧记录可能位于:
text
s3://.../data/00037.parquet
row_position = 8,912
流处理程序如果不知道这个物理位置,就只能在下面两条路中选择。
方案 A:写 Equality Delete
写入一个逻辑删除条件:
text
DELETE WHERE id = 42
然后再追加新版本的数据。
优点是写入快,因为不必先扫描 Iceberg 表。缺点是读取时必须把 equality delete 与候选 data files 做匹配。delete files 持续积累后,查询规划、数据扫描和跨引擎兼容性都会变差。
方案 B:写 Position Delete 或 Deletion Vector
直接记录:
text
file = 00037.parquet
position = 8,912
Iceberg V3 可以把这些位置编码成更紧凑的 deletion vector。读取更加直接,但写入程序必须先知道旧记录的物理位置。
这形成了 Iceberg 流式 upsert 的基本矛盾:
text
不知道物理位置
|
+-- 写 equality delete:写入便宜,读取和维护变贵
|
+-- 先定位再写 DV:读取便宜,写入侧必须维护索引或扫描表
无论选择哪条路,id = 42 到 file + position 的映射成本都不会消失。系统只能决定由谁承担、何时承担。
二、为什么 Apache Iceberg 官方 Kafka Connect Sink 没有 Upsert
截至 2026-10-19,Apache Iceberg 1.12.0 的官方 Kafka Connect sink 仍然没有 upsert-mode-enabled 或 cdc-field 这样的正式配置。当前 writer 对非空记录执行追加写入,并忽略 Kafka tombstone。id-columns 可以表达行标识字段,但它本身并不会把 append writer 变成 delta writer。
社区 PR #14797 曾尝试补上这条路径,但已关闭且没有合并。
提案是怎样实现 Upsert 的
该方案的思路可以概括为:
- 为 Kafka Connect 增加 delta writer。
- 对 UPDATE 先删除旧值,再写入新值。
- 如果旧值是当前 batch 刚写入的记录,writer 已知其物理位置,可以生成 deletion vector。
- 如果旧值来自历史 batch,writer 不知道位置,只能退回 equality delete。
因此,它解决的是 in-batch deduplication,而不是完整的跨批次主键索引问题。
社区顾虑 1:Kafka Connect 没有全局行位置索引
一个 Kafka Connect task 通常只知道自己消费到的 Kafka records。它并不知道整个 Iceberg 表中每个主键当前对应哪个 data file,也不知道 compaction 后记录被移动到了哪里。
如果为了生成 deletion vector 而在每次 UPDATE 前扫描表,worker 会产生大量扫描和数据物化,无法扩展到高频 CDC。
如果在 connector 内维护 primary key -> file + position 索引,又会引入一组新的系统问题:
- 索引状态如何持久化和恢复;
- task rebalance 后状态如何重新分配;
- Kafka partition 是否与业务主键一致;
- Iceberg compaction 改写 data files 后如何更新索引;
- 多个 task 或其他引擎同时写表时如何保持索引正确;
- 索引规模随表中存量行增长时如何管理内存和磁盘。
做到这一步,connector 已经不再是一个相对无状态的 sink,而是在重新实现一个流处理状态后端。
社区顾虑 2:Equality Delete 会产生持续维护债务
跨 batch 更新如果退回 equality delete,系统必须定期将 delete files 应用到 data files,才能避免查询性能持续下降。
这意味着 Kafka Connect 之外还需要 Spark、Flink 或其他引擎执行 aggressive maintenance。社区担心的不只是代码能不能写出来,也包括默认用户体验:
- 功能表面上叫 upsert,但性能高度依赖分区设计和 compaction;
- connector 自身无法完成完整生命周期管理;
- 用户容易在测试阶段得到正确结果,却在生产规模下遭遇 delete files 和小文件累积;
- 维护责任分散在 connector、计算引擎和表运维作业之间。
社区顾虑 3:正确性不只是"同一个 key 最后写入"
数据库 CDC 还涉及事件顺序、重放、重复事件、分区字段变化和 before/after image。
例如,一条 UPDATE 改变了 Iceberg 分区列,仅凭 after image 生成的 equality delete 可能被写入新分区,而旧记录仍在旧分区。又例如,同一主键被分散到不同 Kafka partition 时,"最后一次提交"未必等于数据库中的最终顺序。
S3 上的数据文件最终能够被 compact,并不代表这些上游语义自动正确。
三、Flink 为什么可以实现,而 Kafka Connect 很难实现
Flink 与 Kafka Connect 的关键差异,不是 Flink 找到了一个没有代价的 Iceberg 写法,而是 Flink 有能力管理这份代价。
第一阶段:使用 Equality Delete 实现基础 Upsert
Iceberg 的 Flink sink 可以基于 primary key 或 identifier fields 开启 upsert。典型 UPDATE 会产生:
text
equality delete(id = 42)
+ new data row(id = 42, level = gold)
对于分区表,分区字段对应的源列必须包含在 equality fields 中,从而限制 delete 的适用范围并保证写入语义成立。
Flink 还可以通过 hash distribution 将同一分区或同一 equality key 的记录路由到确定的 writer。与普通 Kafka Connect task 分配相比,数据分发是写入拓扑的一部分,而不是外部偶然形成的条件。
不过,这一阶段并没有消除 equality delete 的读取和维护成本。
第二阶段:把 Equality Delete 转成 Deletion Vector
Iceberg 1.12.0 的 Flink table maintenance 增加了 ConvertEqualityDeletes。它可以与 IcebergSink 的 post-commit maintenance 集成:
java
IcebergSink.forRowData(dataStream)
.table(table)
.tableLoader(tableLoader)
.upsert(true)
.equalityFieldColumns(List.of("id"))
.convertEqualityDeletes()
.append();
目标表需要使用 Iceberg format version 3。
其核心不是一个简单的文件格式转换,而是一条有状态的维护流水线:
text
CDC records
|
v
IcebergSink 写 data files + equality deletes
|
v
Planner 读取待处理 snapshot
|
v
PK index 根据主键找到 file + row position
|
v
生成并合并 deletion vectors
|
v
提交到 target branch
这个主键索引保存在 Flink keyed state 中,并通过 checkpoint 增量维护。官方文档建议在索引可能超过堆内存时使用 RocksDB state backend。
这正是 Flink 能够承担、而 Kafka Connect 社区不愿轻易塞进 sink connector 的职责:
- 有状态计算;
- 按 key 分区;
- checkpoint 与故障恢复;
- 状态规模管理;
- 独立并行度和背压;
- 与表维护任务协调。
当然,Flink 方案也不是免费午餐。主键索引的大小随 live rows 增长;转换速度如果低于写入速度,待处理 snapshot 会排队;外部 compaction 改写文件后,索引还需要感知并更新。
Flink 的优势是把这些成本显式地放进一个为有状态计算设计的系统中,而不是假装它们不存在。
四、S3 Tables 能否弥补社区对 Kafka Connect Upsert 的顾虑
答案是:能弥补一部分,而且恰好是非常重要的一部分,但不能替代 upsert writer 本身。
Amazon S3 Tables 为 Iceberg 表提供自动 table maintenance,包括:
- 自动 compaction;
- compaction 时应用 row-level deletes;
- 自动 snapshot expiration;
- 自动清理 unreferenced files;
- 根据表的 sort order 选择或配置 binpack、sort、z-order 等 compaction 策略。
它还提供 Iceberg REST endpoint,因此兼容 Iceberg REST Catalog 的计算引擎可以把 S3 Tables 当作托管 Iceberg catalog 与表存储层。
S3 Tables 真正补上的能力
社区对 Kafka Connect 方案的一个明确顾虑是:写完 equality delete 后,用户还必须部署 Spark 或同等能力的引擎,持续清理 deletes 和小文件。
S3 Tables 可以替代这层外挂维护基础设施:
text
传统自管方式
Kafka Connect/Flink -> Iceberg data + delete files
|
+-> 自建 Spark/Glue/EMR compaction
+-> snapshot expiration
+-> orphan file cleanup
+-> 调度、扩缩容、重试、监控
使用 S3 Tables
Kafka Connect/Flink -> S3 Tables
|
+-> 托管 compaction
+-> 托管 snapshot expiration
+-> 托管 unreferenced file cleanup
这会显著改善社区担心的产品体验:
| 社区顾虑 | S3 Tables 的作用 | 是否完全解决 |
|---|---|---|
| 高频提交产生小文件 | 自动 compaction 合并小文件 | 大幅缓解 |
| Equality delete 持续累积 | compaction 应用 row-level deletes | 大幅缓解,但存在异步窗口 |
| 需要额外 Spark 维护作业 | 维护由 S3 Tables 托管 | 基本解决运维问题 |
| 快照和无引用文件膨胀 | 自动过期与清理 | 基本解决 |
| Connector 不知道旧行位置 | 不提供主键位置索引 | 未解决 |
| CDC 乱序、重复、分区键变化 | 不负责事件语义 | 未解决 |
| 官方 Kafka Connect 没有 delta writer | 不会替 connector 生成 equality delete | 未解决 |
必须避免的误解
下面这个等式不成立:
text
官方 Iceberg Kafka Connect + S3 Tables = 自动获得 Upsert
正确的关系是:
text
能够正确产生 row-level changes 的 writer
+
S3 Tables 托管维护
=
更容易长期运行的 Iceberg CDC 架构
如果 writer 只会 append,S3 Tables 不知道两条相同主键的记录中哪一条应该失效。
如果 writer 已经能够产生 equality delete,例如第三方 connector、定制版本或 Flink sink,那么 S3 Tables 才能在后续 compaction 中应用这些 deletes,并收敛文件布局。
五、从这些约束推导出的 S3 Tables 优势
1. 将"写入语义"与"表维护"解耦
Flink 或 delta-capable connector 负责回答:
text
这条 CDC 事件代表 INSERT、UPDATE 还是 DELETE?
哪组字段定义一行?
事件顺序和重复怎样处理?
S3 Tables 负责回答:
text
何时合并小文件?
何时把 deletes 应用到 data files?
何时过期快照并清理无引用文件?
怎样长期保持表布局可查询?
职责拆分后,不需要强迫 Kafka Connect 同时成为流处理引擎和 Iceberg 运维平台。
2. 把不可避免的维护工作产品化
Equality delete 的代价不会因为迁移到 S3 Tables 而消失。应用 deletes 仍然需要读取并重写数据,compaction 也会产生费用。
但 S3 Tables 把资源申请、任务调度、失败重试和维护周期变成服务能力。对团队而言,这比"每个 CDC 项目再附带一套 Spark compaction 平台"更容易标准化。
它解决的是"谁来稳定地做",不是"这件事不需要做"。
3. 降低低延迟写入与查询性能之间的运维冲突
更短的提交间隔通常意味着更多小文件。传统架构里,数据新鲜度和查询性能经常形成直接冲突。
S3 Tables 持续维护文件布局,使 ingest job 可以更多关注写入延迟,不必把所有文件尺寸优化逻辑塞进 CDC 作业。对于 equality delete 路径,它也缩短 delete files 长期堆积的风险窗口。
这里仍需监控 compaction 是否跟得上变更速率,因为自动化并不代表无限吞吐或零延迟。
4. 为不同写入引擎提供统一的维护底座
今天可以用 Flink CDC,某些低复杂度场景可以使用 delta-capable Kafka Connect,批量回填可以使用 Spark。只要这些引擎遵循 Iceberg 提交协议,S3 Tables 可以作为统一的 catalog、storage 和 maintenance plane。
这使架构不必把表维护逻辑绑定到某一个摄入引擎。
5. 保留开放 Iceberg 生态,而不是换成专有 Upsert 表格式
S3 Tables 管理的仍然是 Apache Iceberg 表,并提供标准 Iceberg REST 接口。团队可以继续使用兼容的查询与处理引擎,同时把最容易被忽略的 compaction、snapshot 和文件清理交给托管服务。
这正是它相对于"在 connector 内实现一个封闭状态系统"的长期优势。
六、推荐架构
推荐方案:Flink CDC + Iceberg V3 + S3 Tables
text
MySQL/PostgreSQL
|
v
Debezium 或 Flink CDC Source
|
v
Flink keyed state
- 解析 CDC
- 管理顺序与状态
- Iceberg upsert
- equality delete -> DV
|
v
Amazon S3 Tables
- managed Iceberg catalog/storage
- compaction
- snapshot expiration
- unreferenced file cleanup
|
v
Athena / Redshift / Spark / Flink / 其他兼容引擎
该方案的分工最清晰:
- Flink 负责有状态变更语义和 deletion vector 转换;
- Iceberg V3 负责标准化的行级删除表示;
- S3 Tables 负责长期、持续的物理表维护。
轻量方案:Delta-capable Kafka Connect + S3 Tables
如果团队不希望运行 Flink,可以采用能够正确生成 equality delete 的 Kafka Connect sink,再由 S3 Tables 承担 compaction。
这条路径的优势是摄入层更轻,但需要自行承担:
- connector 版本和代码维护;
- CDC 顺序及重复处理验证;
- 分区字段变化的正确性;
- equality delete 兼容性测试;
- compaction 延迟窗口内的查询性能。
它适合更新比例较低、分区设计良好、维护时效要求可接受的场景,不应被理解为与 Flink 有状态方案完全等价。
不推荐的假设:Append-only Kafka Connect + 查询时去重
把所有版本都 append 到 Iceberg,再让每个查询用 ROW_NUMBER() 取最新记录,看似绕开了 delete files,实际上把成本永久转移给每一次查询,也很难正确处理物理删除、迟到事件和增量消费。
这更适合作为 raw CDC history 层,而不是最终 current-state 表。
七、结论
Apache Iceberg 社区没有把 Kafka Connect upsert 合入官方 sink,反映的不是一个简单的功能缺口,而是一条明确的架构边界:
Upsert 需要状态。跨批次更新需要把逻辑主键解析成物理位置。Equality delete 可以延后这项工作,却不能消灭它。
Flink 的方案能够成立,是因为 Flink 原生提供 keyed state、checkpoint、背压与故障恢复,并且 Iceberg 1.12.0 已把 equality delete 到 deletion vector 的转换纳入 Flink table maintenance。
S3 Tables 的价值则位于另一层。它不负责判断哪条 CDC 事件应该覆盖哪条记录,但能够自动完成 delete 应用、小文件合并、快照过期和无引用文件清理。
由此可以得到一个更准确的判断:
S3 Tables 不是 Kafka Connect Upsert 的替代实现,而是让任何正确的 Iceberg Upsert 实现更容易长期运行的托管维护底座。
对于数据库实时同步到 Iceberg 的场景,最完整的组合不是让某一个组件包办一切,而是:
text
Flink 管状态与变更语义
+ Iceberg V3 管行级变更格式
+ S3 Tables 管持续表维护
这也是 S3 Tables 从社区顾虑中推导出的最大优势:它让 CDC 写入系统不必同时背负一套长期运行的 Lakehouse maintenance platform。
想了解具体落地实现方案,点击 S3 Tables 实战:两种方案,把 MySQL 数据实时"搬"进 S3 Tables
参考资料
Apache Iceberg
- Apache Iceberg 1.12.0 Kafka Connect 文档源码
- Apache Iceberg 1.12.0 Flink Writes 文档源码
- Apache Iceberg 1.12.0 Flink Table Maintenance 文档源码
- Kafka Connect Delta Writer / CDC 提案 PR #14797
- Apache Iceberg 规范