1. CDC 写放大到底"放大"在哪里?
CDC(Change Data Capture)入湖常见形态是持续 UPDATE/DELETE/INSERT(尤其 Upsert)。在"不可变数据文件(Parquet/ORC)+ 表格式元数据"的体系里,写放大主要来自三层:
-
数据层写放大(Data write amplification)
- 典型 Copy-on-Write(COW):更新/删除少量行也可能需要重写整批数据文件(把未变更行"搬运"到新文件),写入字节数远大于变更字节数。
-
删除表示层写放大(Delete representation amplification)
-
典型 Merge-on-Read(MoR)在 Iceberg v2 使用 position delete files:频繁微批/流式更新会生成大量小 delete files("雪花"式堆积)。
-
结果是:写入端要写很多小文件,后续还要 compaction/rewrite;同时元数据(manifest)也膨胀。
-
-
元数据与计划层写放大(Metadata / planning amplification)
-
文件数上升 → manifest/manifest list 里要记录更多条目 → 写一次 commit 要更新更多元数据。
-
读取/查询 planning 也会变慢(需要处理更多 delete 相关条目)。
-
你问的"CDC 场景下的写放大",通常说的核心矛盾是:
行级变更比例很小,但系统要付出"接近重写/重组整个文件集合"的写入成本。
Iceberg v3 的 DV 主要瞄准的是:
-
在 MoR 路线上,把"删除表示层 + 元数据层"写放大压下去;
-
并在一定条件下,间接减少"数据层"为了删除而触发的重写压力。
2. Iceberg v2 MoR(position delete files)为什么在 CDC 下容易爆?
Iceberg Spec 将行级删除分为两类:
-
Position deletes :按(data file path + row position)标记删除;在 v2 中由 position delete file 编码,在 v3+ 可以由 deletion vector 编码。
-
Equality deletes:按列值(如 id=5)标记删除。
来源:Iceberg Table Spec(Row-level deletes 段落)
在 CDC 场景(比如每分钟一批 Upsert)里,v2 position delete files 的典型路径是:
-
找到受影响的数据文件(可能靠分区裁剪/索引/元数据统计,但仍然会涉及一定范围的文件)
-
对每个被更新/删除的行:写入一条 position delete 记录
-
产生一个或多个 delete files,并在 commit 里把 delete file 作为"新文件"加入元数据
问题在于:
-
流式/微批的每次提交变更量通常不大(几十、几百、几千行),delete file 很容易变成"小文件"。
-
delete file 数量会随提交次数线性增长(甚至更快,因为一个批次可能影响多个数据文件/多个分区)。
-
delete file 还会被 manifest 跟踪,导致 manifest 条目膨胀。
于是出现"写放大"链路:
小变更 → 产出大量小 delete files → 元数据膨胀 + 后续不得不做 delete file rewrite/compaction → 额外写入更多文件
3. Iceberg v3 删除向量(DV)的核心思路
3.1 DV 把"删除集合"从"多文件列表"变成"单文件位图"
Puffin 规范定义了 deletion-vector-v1 blob:
-
DV 是一个 bitmap :bit 置位表示对应 row position 已删除。
-
支持 64-bit row position,但为了优化常见场景(大多数 position 在 32-bit 范围内),采用"按高 32 位分桶 + Roaring bitmap 存低 32 位"的组合结构。
来源:Puffin Spec -- deletion-vector-v1 blob type
这意味着:
-
针对同一个 data file 的删除集合,不再需要大量"追加式 delete 文件";
-
删除信息可以以非常紧凑的形式(Roaring bitmap)存储。
3.2 DV 存在 Puffin 文件里,并通过元数据指向它
Puffin 是"存放 Iceberg manifest 里放不下的信息(indexes/statistics等)"的文件格式;DV blob 的 metadata 强制包含:
-
referenced-data-file:DV 作用的数据文件 location -
cardinality:删除的行数
来源:Puffin Spec -- deletion-vector-v1 blob type
2.3 规范层面"约束 DV 的形态"是关键(避免回到小文件地狱)
你给的文章提到 v3 还"强制要求"每个数据文件维护单一 DV(而不是多个 delete 文件散落)。
从工程效果看,这个约束的价值在于:
-
让删除信息天然具备"聚合"形态(一个文件一个位图),而不是"每次提交一个小文件"的形态。
-
删除集合的更新变成"更新位图",从文件数量增长转变为"位图内容变化"。
4. DV 如何解决 CDC 写放大:分解为 3 条机制
下面把"写放大"拆成三类,对应解释 DV 的收益边界。
4.1 机制 A:把 CDC 的"每批新增 delete file"写放大,变成"更新一个位图"
v2 position delete files:
- 每个 micro-batch 都可能产生新的 delete file(文件数 ∝ 提交次数 × 影响文件数)
v3 DV:
-
针对同一个 data file,删除集合以 bitmap 聚合。
-
micro-batch 的删除集合可以被"并入/合并"到该文件的 DV 表达里。
结果:
-
delete 表示层写放大显著下降:小文件数量下降、delete 文件 rewrite 压力下降。
-
元数据写放大下降:manifest 里需要跟踪的 delete file 条目减少(至少从"多个小 delete file"变为"一个 DV 指向/更少的 delete artefact")。
4.2 机制 B:把"写入字节量"从 O(重写数据文件) 降到 O(变更行数)
CDC 的典型特征是:
-
每个 batch 改动行数 k 很小
-
但被命中的数据文件可能很大(比如 512MB/1GB 的 Parquet)
COW 路线的数据写放大:
-
即使只删除 1 行,也要把该文件中未删除的 N-1 行重写到新文件
-
写入字节量近似等于"被命中数据文件总大小"
DV 路线(MoR + DV)的数据写放大:
-
删除只体现为在 bitmap 里置位若干 row positions
-
写入字节量近似等于"bitmap 增量 + 少量元数据更新"
直观对比(量级估算):
-
512MB Parquet 文件里删除 10,000 行:
-
COW:可能接近重写 512MB(甚至更多,含新文件、元数据)
-
DV:写入的是"10,000 个 position 的集合"经 Roaring 压缩后的 bitmap(通常远小于 512MB)
-
注意:DV 本身是不可变文件的一部分(Puffin/Blob 也是文件),所以更新 DV 往往意味着写一个新的 DV blob / Puffin 文件并更新引用。
但关键差别是:
- 你写的是"删除集合的压缩表示",不是"原始数据的全部未变更行"。
4.3 机制 C:降低后续运维性写放大(compaction / rewrite delete files)
在 v2 position delete files 模式下,CDC 会触发两类"运维性写放大":
-
rewrite_position_delete_files:为了合并大量小 delete files
-
rewrite_data_files:为了清理高 delete ratio 的数据文件、回收空间、改善读性能
DV 主要影响第 1 类:
- 当删除集合已被"单 DV 聚合"时,至少不会出现成千上万小 delete files 等待合并。
对第 2 类(rewrite_data_files)影响是间接的:
-
如果删除越来越多,哪怕是 DV,读时也需要过滤更多行;
-
迟早你仍可能需要 rewrite_data_files 来真正回收空间/降低删除比例。
也就是说:
DV 解决的是"删除表示导致的文件爆炸与元数据爆炸",不是"无限期不重写数据文件"。
5. 为什么 DV 对 CDC 特别有效:CDC 的"稀疏、持续、小批量"与位图压缩天然匹配
CDC 更新的典型模式:
-
每批次删除/更新的行在全表中占比很小
-
对单个 data file 来说,被删行 position 往往是稀疏集合
Roaring bitmap 就是为稀疏整数集设计的压缩结构(Puffin spec 的 DV blob 也明确采用 Roaring 的 portable format)。
来源:Puffin Spec -- deletion-vector-v1 uses Roaring bitmap portable format
因此在 CDC 下:
-
你写入的 DV 往往能保持很小的体积
-
且"累计很多批次"后仍然可控(直到删除比例很高才需要触发 rewrite_data_files)
6. DV 的边界与代价:它缓解写放大,但不是"免费午餐"
6.1 DV 不会消除"新增版本行"的写入
CDC 的 UPDATE 在湖格式里通常是:
-
DELETE old row(用 DV 标记)
-
INSERT new row(写到新的数据文件)
所以:
-
DV 能把"旧行删除"从重写大文件变成写位图
-
但"新增行"仍然要落到新的数据文件(这部分写入是不可避免的)
6.2 DV 仍需要元数据更新与可能的 Puffin 文件重写
Puffin spec 说明 DV blob metadata 里 snapshot-id / sequence-number 在创建时可能未知,需要占位(例如 -1)并在后续由表提交关联。
来源:Puffin Spec -- snapshot-id/sequence-number handling
实际实现中,这意味着:
-
DV 的生成/提交要与 Iceberg snapshot/sequence number 生命周期协调
-
更新 DV 也会产生新的 artefact(只是体积和数量更可控)
6.3 读放大可能下降,但也可能在"高删除比例"时上升
DV 读时需要:
-
读取数据文件 + DV
-
在执行层做"按 position 过滤"
当某些文件删除比例过高:
-
读时仍要扫描大量被删行对应的 page/row group(取决于引擎实现)
-
最终还是要通过 rewrite_data_files 把"活行"抽出来,真正降低 IO
7. 面向落地:CDC 场景用 DV 的实践建议(从"写放大"出发)
下面是"机制导向"的建议:目的不是泛泛谈参数,而是保证 DV 真正把写放大压住。
7.1 选择策略:CDC 以 MoR + DV 为主,COW 作为"最终整理"
-
实时链路:MoR + DV(削减每批写放大)
-
后台整理:定期 rewrite_data_files(回收空间、改善读性能)
7.2 控制"DV 增长阈值"触发数据重写
你可以用两个信号触发 rewrite_data_files:
-
delete ratio(某文件 DV cardinality / row count)
-
DV cardinality 绝对值(删除集合过大,影响 CPU/过滤)
Iceberg 的维护过程(如 rewrite_data_files)本身就有与 delete ratio 相关的选项(具体各引擎/版本略有差异)。
7.3 避免"DV 过于频繁地被更新"导致的写放大回弹
虽然 DV 很小,但如果你每秒都生成一个新 Puffin/DV artefact,同样会产生:
-
小文件数量增多
-
元数据提交频率过高
因此 CDC pipeline 一般需要:
-
适当的 micro-batch 粒度(例如按分钟/按批量行数聚合)
-
对同一 data file 的 delete positions 先在内存聚合,再落盘形成 DV
(这一点更偏实现/引擎策略,但原理上与"单 DV 聚合"是一致的。)
8. 总结:DV 对 CDC 写放大"解决了什么、没解决什么"
解决了:
-
把 v2 position delete files 在 CDC 下的"小文件爆炸"压到"单文件位图聚合"形态(减少 delete 表示层写放大)。
-
把"删除/更新旧行"的数据层写入从"重写大数据文件"降为"写压缩 bitmap + 少量元数据"。
-
降低了为了合并 delete files 而产生的运维性写放大(rewrite_position_delete_files/小文件合并压力)。
没有直接解决的:
-
UPDATE 的"新增行写入"仍然存在(新版本行必须落新文件)。
-
高删除比例文件最终仍需要 rewrite_data_files 来回收空间与优化查询。
-
DV 的收益高度依赖引擎实现质量(DV 的生成、提交、过滤优化、以及与 row group/page 的协同)。