Apache Paimon 实时湖仓:主键表、Changelog 与 Compaction 语义解析

从主键表、Changelog Producer 与 Compaction 语义理解 Paimon 流式湖仓定位,划清与 Iceberg 的边界

袋鼠云
袋鼠云
数据治理

Apache Paimon 是流式湖仓场景下被讨论最多的存储格式之一:它把 LSM 树的更新语义带进了数据湖,让"流式写入 + 主键更新 + 下游流式读取"成为第一等公民。要理解它的定位,关键是看懂三个概念:主键表(Primary Key Table)、Changelog Producer 与 Compaction 语义。本文不展开搭建步骤,只解析这些语义,并划清它与 Iceberg 这类格式的适用边界。

主键表:把数据库的 upsert 带进数据湖

Paimon 的表分为 append-only 与 primary-key 两类。append-only 表语义接近常规数据湖格式;而主键表是 Paimon 流式能力的核心——它用主键定义"同一行",写入时按主键合并,天然支持 upsert。

实现机制是 LSM(Log-Structured Merge)树:新写入先落 level 0 的增量文件,后台按规模逐层合并成有序的 sorted run,读取时跨 run 归并、按主键去重取最新版本。因此主键表:

  • 写入路径是纯追加,不需要像数据库那样原地更新,适合高吞吐流式写入;
  • 读取路径要合并多个 run,读放大与写放大之间存在权衡,这也是 Compaction 参数存在的意义;
  • 通过 merge-engine 选项可以改变合并语义:deduplicate(默认,保留最新)、partial-update(部分列更新)、aggregation(按聚合函数合并,如累加计数)、first-row(保留首行)等,不同的下游语义选不同的引擎。

一句话定位:主键表解决的是"数据湖里能不能做流式更新"的问题,答案是"能,并且是设计目标,而不是事后补丁"。

Changelog Producer:决定流读能看到什么

主键表解决了存储侧的更新,但下游流读要拿到的是变更日志(changelog)——哪些行被插入、更新、删除。Paimon 用 changelog-producer 选项决定如何生成这份日志,四个取值对应四种代价与延迟的组合:

  • none:不生成 changelog。流读只能看到"合并后的当前结果",拿不到完整的 -U/+U 变更序列,适合只读最新状态、不需要下游精确回放的场景;
  • input:直接复用输入流自身的 changelog。前提是输入本身是完整变更日志——典型如数据库 CDC 或 Flink 有状态计算产生的输出。此时写入方把每条变更单独落 changelog 文件,代价最小、语义最完整;
  • lookup:当输入不完整(比如 partial-update、aggregation 等合并引擎改变了记录语义)但又要生成完整 changelog 时,Paimon 在提交前通过查表对比前后值生成变更日志。相比 input 多了查找开销,但延迟比 full-compaction 低,是官方推荐的折中;
  • full-compaction:每次全量合并后对比前后结果、产出差异作为 changelog。它把"写数据"与"产 changelog"解耦,适合能容忍分钟级延迟(如 10 分钟)的场景,也是资源开销最高、最省心的一种。

选择口诀:输入是 CDC → input;合并引擎复杂但要低延迟 → lookup;可容忍延迟、要最简运维 → full-compaction。lookup 与 full-compaction 还支持 changelog-producer.row-deduplicate,避免对同一记录重复生成 -U/+U。

Bucket、快照与读取模式

主键表还有一些决定并行度与读取方式的底层概念,理解它们才能解释"为什么表行为和我预期不一样":

  • Bucket:主键表按主键哈希分桶,bucket 数量决定写入并行度与文件组织。bucket 少则写入并行度低、单文件偏大;bucket 多则文件数多、需要更频繁的 Compaction 收拢。调优是在两者之间找平衡,而不是越大越好;
  • 快照:每次 checkpoint 提交都会产生一个新快照。批读按快照读取,时间旅行可以回看历史版本;流读则从指定快照位置持续消费,可用 consumer-id 记录消费进度,避免重启后重复消费;
  • 一份数据两种读法:同一张主键表既能被 Flink 流读 changelog,也能被 Spark 批读最新快照,这是"湖"的部分——存储只有一份,流读与批读共用;
  • 快照保留策略:历史快照过多会放大存储与元数据开销,需要按业务回放需求设置过期清理,与 Compaction 一并纳入运维,否则长期运行后表会"越跑越重"。

主键表与 append-only 表的边界

不是所有数据都该用主键表。append-only 表没有主键合并逻辑,语义与 Iceberg 更接近,适合日志、事件流水这类"只增不改"的数据。选型时先问业务:"同一行会不会被更新?"——不会更新就别用主键表,避免白付 LSM 的合并与读放大成本。同一份数据的两类表可以共存:事件明细用 append-only,聚合结果用主键表,各取所需。

Compaction:LSM 的代价与调度

Paimon 的 Compaction 采用类似 RocksDB Universal Compaction 的 size-tiered 策略:sorted run 数量或体积超过阈值就触发合并。几个关键默认值决定了行为边界:

  • num-sorted-run.compaction-trigger 默认 5:level 0 的 sorted run 数达到 5 触发合并;num-sorted-run.stop-trigger 默认在此基础上 +3,达到后暂停写入、防止无限堆积;
  • compaction.max.file-num 默认 50、compaction.min.file-num 默认 5:控制单次合并的文件数上下限;
  • compaction.max-size-amplification-percent 默认 200:changelog 模式下控制空间放大;
  • 数据增量过大时 Paimon 会自动触发 full compaction,把全部文件合并到最高层;也可用专用 compaction 作业(如 CALL sys.compact(...))或表选项定期触发,把合并开销从写入路径剥离;
  • write-only 选项可关闭自动合并,适合写入高峰期"先堆文件、闲时再合并"的批量导入场景,代价是读取时 run 多、延迟升高。

理解 Compaction 的核心是它不改变快照的正确性,只改变读取的代价:合并前后,同一快照下的数据内容一致,但文件数、sorted run 数、查询时的归并开销不同。因此 Compaction 调优的目标是文件数与查询延迟的平衡,而不是"合并得越勤越好"——合并过勤会放大写放大,吞掉流式写入的吞吐。

与 Iceberg 的差异:两种不同的"湖"哲学

把 Paimon 与 Iceberg 并排看,边界就清晰了:

  • Iceberg 以 copy-on-write + 快照隔离为核心:每次提交生成新快照,读侧天然一致,时间旅行与多引擎(Spark、Trino、Flink 等)生态成熟,但"更新一行"通常意味着整文件重写,流式高频 upsert 的写放大明显;
  • Paimon 以 merge-on-read + LSM 为核心:主键更新走增量文件合并,流式写入吞吐高、能产出 changelog 供下游流消费,代价是读取要跨 run 归并、Compaction 成为必须运维的对象;
  • 因此:批量分析为主、多引擎混合读、更新频率低 → Iceberg 更省心;流式写入频繁、按主键更新、下游要流读变更 → Paimon 的主键表 + changelog 组合是更自然的选择。

适用场景边界

Paimon 主键表适合:CDC 入湖构建实时 ODS/DWD、流式维表、需要低延迟流读变更日志的实时数仓分层。它不擅长(或没必要选它):低频批量覆盖写入、以 Ad-hoc 复杂分析为主且引擎分散的场景,以及团队没有 Compaction 与文件治理运维能力、期望"零运维"的纯批式湖。

下一步:用一个真实 CDC 源表建主键表做小规模验证,重点观察三件事——流读是否拿到预期 changelog、文件数与 sorted run 随写入的增长曲线、以及默认 Compaction 参数下的查询延迟;确认行为符合预期后,再决定是否把 changelog-producer 从 input 换成 lookup、把 full compaction 挪到业务低峰。

开启Data+AI 之旅

填写表单,获取专属解决方案

400-002-1024

工作日 9:00-18:00

关注公众号

获取最新产品动态与技术干货

关注公众号

企业微信

扫码添加企业微信