full-compaction 模式的 Changelog Producer 的工作机制
在 Apache Paimon 的官方文档中,主键表(Primary Key Table)流式读取的一个核心概念就是 Changelog Producer(变更日志生成器)。流式写入时,为了让下游流式读取(如 Flink 流读)能够获取到完整、正确的增量修改数据(特别是带有 -U / +U 即 UPDATE_BEFORE 和 UPDATE_AFTER 的回撤流),必须通过 Changelog Producer 机制来生成 Changelog。
以下基于 Apache Paimon 的 master 分支文档及底层设计,详细剖析 full-compaction(全量压缩) 模式的工作机制、配置、底层原理及应用场景:
一、 full-compaction 机制的核心概述
对于主键表,如果用户在创建表时指定了聚合引擎(如 partial-update 局部更新、aggregation 预聚合等),写入器(Writer)在刷盘时仅凭输入的单条记录是无法直接推导出“旧值”的。full-compaction 模式的提出,就是为了在 不依赖复杂外部状态/点查索引 的情况下,生成完整正确的变更日志。
- 基本原理:Paimon 会在全量压缩(Full Compaction)的过程中,将要合并的数据与最高层(Top Level)的历史数据进行比较,通过两者的“差分”(Difference)来推导并产生 Changelog。
- 特点:
- 通用性强:能够为任何类型的输入数据源和任何合并引擎(Merge Engine)产生 100% 正确且完整的变更日志。
- 高延迟解耦:将 Changelog 的生成开销从“每次写入(Write)”剥离,延迟到“压缩(Compaction)”阶段。
- 适合高延迟场景:因为全量压缩比较消耗资源,一般建议设置较长的触发周期(如 10 分钟或 30 分钟),从而更适合对实时性要求不极端的场景。
二、 工作生命周期与技术机制
在 LSM 树存储结构中,数据按照 Level 分层组织。新数据首先写入内存(MemTable),然后刷盘到 Level 0,最后通过 Compaction 逐层下沉,最终合并到最高层。full-compaction 模式下的 Changelog 生成流程如下:
1. 触发全量压缩
当触发全量压缩时,LSM 树中所有层级(从 Level 0 到最高的 Max Level)的所有数据文件都会参与合并。由于这是一次彻底的全局合并,所有属于同一个主键的旧版本、新写入和待删除(Delete)的数据都会被聚合在一起。
2. 底层合并与变更生成(差分逻辑)
在合并过程中,底层对应的核心组件是 FullChangelogMergeTreeCompactRewriter 和 FullChangelogMergeFunctionWrapper:
- 新增记录(INSERT):如果某个主键在最高层(Max Level)中不存在历史记录(说明这是一条全新的主键),合并后生成一条
+I(INSERT)的 Changelog。 - 更新记录(UPDATE):如果该主键在最高层中已存在历史记录(旧值
v_old),在经历了本次 Compaction 归并后得到了最新值(新值v_new):- 如果
v_old与v_new不相等,系统会自动生成一对变更日志:-U(UPDATE_BEFORE,携带v_old)以及+U(UPDATE_AFTER,携带v_new)。 - 通过这种“最高层旧值 vs 合并后新值”的比较,完全避免了在平时写入阶段去进行点查的开销。
- 如果
- 删除记录(DELETE):当检测到墓碑标记(Delete Tombstone)与历史记录合并后,生成
-D(DELETE)的 Changelog。
3. 写入专用的 Changelog 文件
计算出的这些变更记录(+I、-U、+U、-D)不会和普通的数据合并到最终的数据文件中,而是被独立输出并保存到专门的 Changelog File 中。
下游的流式读取器(Paimon Source)通过扫描快照元数据(Snapshot),可以直接并按序读取这些 Changelog 文件,向游提供保序的 Changelog 流。
三、 关键参数配置
在建表属性中,以下几个参数专门用于微调 full-compaction 模式下的行为:
| 参数名称 | 默认值 | 类型 | 描述 |
|---|---|---|---|
changelog-producer |
none |
Enum | 设为 full-compaction 以启用此机制。 |
full-compaction.delta-commits |
无(或 1) |
Integer | 控制触发全量压缩的频率。在流式写入中,每经过指定次数的 delta commits(即 Flink Checkpoint 提交次数),就会触发一次 Full Compaction。• 设为 1:表示每次 Checkpoint 都会执行一次全量压缩并产出 Changelog。虽然延迟低(等于 Checkpoint 间隔),但写放大极其严重。• 设为较大值(如 10):表示每 10 次 Checkpoint 才全量压缩一次,虽然 Changelog 产出延迟增大,却能大幅减轻吞吐压力。 |
changelog-producer.row-deduplicate |
false |
Boolean | 控制是否对同一记录进行去重。如果全量压缩后的新值与最高层的旧值实际上完全一样(即数值未发生改变),若设为 true 则不产生 redundant 的 -U/+U 日志。 |
changelog-producer.row-deduplicate-ignore-fields |
无 | String | 与 row-deduplicate 搭配使用,指定在比较新旧值时,哪些字段的变化可以被忽略(不触发变更日志生成)。 |
四、 与其他模式的对比
为了帮助更好地理解 full-compaction 的设计意图,可以将其与常用的 lookup 模式进行简单对比:
lookup模式:- 在每次数据刷盘(Flush)提交到快照前,点查(Lookup)本地内存或本地磁盘索引来获取旧值。
- 优点:生成 Changelog 的延迟低,紧跟 Checkpoint 周期。
- 缺点:在内存和本地磁盘上会产生巨大的索引缓存压力(需配置额外缓存,且容易受磁盘 IO 瓶颈限制)。
full-compaction模式:- 不进行任何额外的主键点查。
- 优点:完全避免了由于点查(Lookup)导致的复杂索引维护及本地磁盘 IO 消耗;由于 Compaction 过程本身就需要读取这些数据,因此其 Changelog 生成属于“顺带进行”,原理更为自然直接。
- 缺点:Changelog 产生完全受限于全量压缩周期。如果周期短(如
delta-commits = 1),全量合并所有层的数据开销很大,会对系统写入吞吐带来一定限制。
五、 最佳实践建议
- 容忍延迟,换取吞吐:在资源有限或表极宽的情况下,建议将
full-compaction的触发延迟放宽(例如通过控制 Checkpoint 间隔配合合理的full-compaction.delta-commits周期,或者通过独立进程定期触发 Compact),让其在业务低峰期或以更低频的形式工作。 - 在全量阶段暂时关闭:如果是进行历史数据的全量同步阶段(如首次 CDC 初始化导入),建议暂时不配置此参数,待全量历史写入完成后,再通过
ALTER TABLE开启,进入增量流读阶段,以避免全量阶段产生严重的性能阻碍。