Amoro 是一个构建在 Apache Iceberg 等开放数据湖表格之上的湖仓管理系统,提供了一套可插拔的数据自优化机制和管理服务,旨在为用户带来开箱即用的湖仓使用体验。
以 Apache Iceberg 为代表的开放表格式提升了在数据湖上存储结构化/半结构化数据的能力,其提供的 ACID 事务,表结构进化,分区进化,时间旅行等特性极大提升了在数据湖上管理、查询大量数据的效率,进而推动了 Lakehouse 架构的浪潮,实现了一套大数据存储架构满足所有的数据使用场景的可能。
基于 Apache Iceberg 搭建 Lakehouse 架构过程中,用户无法绕开的一个问题便是如何高效维护所有 Apache Iceberg 表,Lakehouse 架构中会有各种各样的引擎写入/读取 Iceberg 表,与传统的数据仓库架构不同,Lakehouse 架构中并没有一个现成的管理服务负责维护所有的 Iceberg 表,保证其在所有场景下的使用效率。常见的 Iceberg 表维护任务包括:小文件合并,Delete File 合并,元数据文件合并,过期快照清理,孤儿文件清理等。虽然 Apache Iceberg 社区提供了工具帮助用户完成这些维护任务,但是仍然需要用户手动执行这些维护任务,或者自行开发平台来调度管理这些任务。
Apache Amoro(Incubating) 即是一个自动化维护包括 Apache Iceberg 在内数据湖表的湖仓管理系统。Apache Amoro 可以自动发现需要进行优化的表,管理所需的执行资源,调度优化任务,确保所有数据湖表在使用必要的资源情况下,达到最佳的使用性能。项目在文件合并上做了大量优化,使得其在 Apache Iceberg 表上的合并效率相较于 Iceberg Spark RewriteFiles Procedure 有超过10倍以上的性能提升。本文将为大家解密 Apache Amoro 高效合并 Iceberg 表的原理,并以实际的测试样例验证其合并方案的效率。
为了实际对比 Apache Amoro 与 Apache Iceberg Spark RewriteFiles Procedure 的合并效率,我们构造了一组测试用例,对比在相同写入情况下,使用相同的合并资源,对比两种合并方式的合并效率,我们将观测合并任务的执行时间,合并前后表上的查询性能。
测试包含两个主要场景: Append Only 场景和 Upsert 场景。
1.测试方案
考虑到实时写入场景对文件合并的需求更大,也更能体现出合并效率的差异,测试选择使用一个 Apache Flink Streaming 任务不断往一张 Iceberg 表中写入数据,并使用两种合并方案不断对表中的数据进行合并,再使用 Apache Spark 查询表中的数据。
测试的表结构如下:
使用下面的SQL验证合并前后的查询效率
2. Append Only 场景
Append Only 场景 Flink 任务每一分钟会产生一次提交,由于 Flink 任务的写入并发是8,所以每次提交会产生8个文件,每个文件的平均大小为6KB,频繁的提交小文件对于及时完成文件合并提出了较高的要求。合并的目标文件大小为128MB。
整个场景使用的计算资源如下:
Flink 写入任务:8C 16GB
Spark 合并任务:1C 2GB
Amoro 合并任务:1C 2GB
Spark 查询任务:4C 8GB
2.1 Spark RewriteFiles Procedure 合并结果
Append Only 场景下,执行 RewriteFiles Procedure 的周期成了影响合并效率的关键因素,我们分别测试了每1分钟,每10分钟,每60分钟进行一次合并时的情况。
下面是每1分钟进行一次合并,写入过程持续一小时的合并记录(裁剪了部分结果):

整个过程的合并耗时为:394秒,合并前的平均查询耗时为:0.7秒。
过于频繁的合并频率导致合并过程造成的写放大非常严重,但是能保证表上的查询耗时。
下面是每10分钟进行一次合并,写入过程持续一小时的结果:

整个过程的合并耗时为:75.3秒,合并前的平均查询耗时为:1.7秒。
将合并频率调低能明显减少写放大问题,但是表上查询时间也会显著上升。
下面是每60分钟进行一次合并,写入过程持续一小时的合并记录:

整个过程的合并耗时为:23.8秒,合并前的平均查询耗时为:7.0秒。
将合并频率继续降低,将继续减少合并带来的写放大问题,不过表上的查询时间也随着小文件的堆积而大量上升。
2.2 Apache Amoro 合并结果
Apache Amoro 会自动判断表上的文件情况是否达到合并的标准,并生成合理的合并任务计划,调度合理的资源完成合并。
下面是使用 Apache Amoro 自动优化的合并记录:


整个过程的合并耗时为:90秒,合并前的平均查询耗时为:1.0秒。
Apache Amoro 实时监控表中碎片文件的堆积情况(测试场景的触发阈值为40个)以每五分钟为间隔调度合并任务,先将碎片文件(<16MB) 合并成中等大小文件(>16MB),再逐步合并中等大小文件到目标大小,以达到合并资源开销和性能的平衡。
2.3 结果对比
Append Only 场景选择合适的合并周期成为了平衡合并效率和查询耗时的关键,Apache Amoro 自动根据表上的小文件情况决定合并触发时机,相较于 Spark Rewrite Files 最高提升3倍+的合并效率(对比第一组数据),最高提升8倍查询效率(对比第三组数据)。
另外实际场景中表上的写入流量会存在波动,Amoro 自适应的合并方案相较于固定周期的合并方案能有更大的合并效率提升效果。
3. Upsert 场景
Upsert 场景模拟 CDC 写入场景, 该场景我们会先在表上初始化5亿条/230GB的初始数据,Flink 任务同样每一分钟会产生一次提交,由于 Flink 任务的写入并发是8,所以每次提交会产生16-24个文件,其中8个是 insert file, 8个 equality delete file,可能还会产生8个 position delete file。频繁的提交小文件和 delete file 对于及时完成文件合并提出了更高的要求。合并的目标文件大小为128MB。
整个场景使用的计算资源如下:
Flink 写入任务:8C 16GB
Spark 合并任务:32C 64GB
Amoro 合并任务:32C 64GB
Spark 查询任务:32C 64GB
3.1 Spark RewriteFiles Procedure 合并结果
下面是开启实时写入后,每5分钟执行一次合并,持续30分钟的结果:

从结果上看,随着写入的持续进行,查询耗时越来越高,且未随着合并任务的执行而有所下降。排查发现是因为大量的 delete file 并没有与历史的 insert file 发生合并,随着 delete file 越来越多,表上的查询效果越来越差。
我们在合并任务上加上参数:delete-file-threshold = 10, 在表上 delete file 堆积较多时去触发合并,提升表上的查询性能。
下面是加上参数后,每10分钟执行一次合并,持续60分钟的结果:

整个过程合并耗时 2596 秒,合并前平均响应时间36.8,合并后平均响应时间:29秒。
3.2 Apache Amoro 合并结果
下面是 Upsert 场景开启实时 Amoro 自动优化一小时的结果:


整个过程合并耗时 583 秒,合并前平均响应时间32.8秒,合并后平均响应时间:30.6秒。
在 Amoro 的统计里,合并过程中被重复读取的 Delete File 会进行重复统计,故合并前文件数,合并前文件大小会大于 Spark RewriteFiles 的统计方式。
3.3 结果对比
Upser 场景会产生大量 Delete File,特别是使用 Flink 实时写入时,会同时产生 Position Delete File 与 Equality Delete File,其中 Equality Delete File 对与表的分析性能影响巨大。Spark RewriteFiles Procedure 默认不会去合并这些 Delete File 导致表上的分析性能越来越差,这种情况必须带上 delete-file-threshold 参数来及时合并掉 Delete File,但是合并过程几乎会重写整个表导致合并开销巨大。
Amoro 自动优化采用 minor optimizing 及时将 Equality Delete File 转换成 Position Delete File,在避免了 Equality Delete File 对表上查询性能影响的同时,大大减少了合并代价。Amoro 自动优化在保证相似查询效率的情况下,单次合并效率有近10倍提升,整体合并效率有近5倍提升。
Amoro 相较于 Spark RewriteFiles Procedure,大大减少了每次合并读取/写入的数据量,同时还能减少对象存储上的请求带宽,减少新增快照的存储成本放大。
从上面两个场景的测试结果我们发现,在 Append Only 场景 Apache Amoro 能够自动根据表上的文件情况触发合并任务,避免过于频繁的合并带来的写放大问题,同时也能规避一直不合并带来的性能下降问题。在 Upsert 场景 Amoro 通过先将对查询性能影响较大的 Equality Delete File 合并成 Position Delete File 的方法避免频繁合并历史文件带来的写放大问题。
我们可以将 Apache Amoro 的合并原理总结为:自动触发和分层合并两个特性。
1. 自动触发
上图是 Amoro 的架构图,核心组件包括:
Amoro Management Service(AMS):中心管理组件,负责接收来自引擎端上报的 Iceberg 表上产生的读写 metric,根据 metric 生成合并任务,下发给 Optimizer 执行并接收合并结果;对外暴露 Rest API,提供表维护相关的配置/查看接口。
Optimizer:执行节点,拉取 AMS 上的合并任务,执行并将结果上报 AMS。
Metrics Reporter:安装到引擎内的插件,收集 Iceberg 表上的读写事件上报 AMS。
Meta Storage:元数据存储系统,通常为关系型数据库。
通过这些组件一次表上的自动优化任务的执行包括包括:
引擎上产生了一次表的写入。
Metrics Reporter 产生表写入时间,上报 AMS。
AMS 通过 Metrics 判断表上的文件情况达到合并阈值。
AMS 产生合并任务,并拆分成多个子任务。
Optimizer 从AMS拉取到合并任务并执行。
AMS 接收到所有任务的执行结果,并提交合并结果。
上面的流程中的一个关键步骤为如何判断表是否应该合并,并如何进行合并,这部分是保证表上合并效率的关键,我们将这个合并算法称为分层合并。
2. 分层合并
为了保证表上的合并效率,我们将 Iceberg 表上合并任务分成了三类。

它们分别为:
Minor optimizing:快速合并掉表上的碎片文件,同时将 Equality Delete File 转换成 Position Delete File,实时写入场景,通常每几分钟就执行一次。
Major optimizing:将中等大小的文件合并到目标大小,当 Position Delete File 堆积太多时会将它们与 Data File 完成合并,实时写入场景,通常每一个小时一次。
Full optimizing:对整个表或者部分分区进行完全的重写,还会完成全表排序,通常每天执行一次。
让我们进一步对比 Amoro 与 Spark Rewrite Files Procedure 的差异来看看为什么会带来合并效率上的差异。

上面是 Append Only 场景下 Spark RewriteFiles 与 Amoro Self-optimizing 的对比图,Spark RewriteFiles 每次都会将没有达到目标大小的文件进行合并,而 Amoro 先将碎片文件合并到中等大小的文件后,再将中等大小的文件合并到目标文件,通过拆分成两部有效减少了对较大文件的重复读写。
Upsert 场景下, Spark RewriteFiles 需要频繁对近乎所有 Insert File 进行重写,这个过程会造成大量的写放大问题。Amoro Self-optimizing 中的 Minor optimizing 会及时将表中的 Eqiality Delete File 重写成 Position Delete File ,读取过程对于 Insert File 只需要读取主键列,有效避免了对存量 Insert File 的重写带来的问题,Eqiality Delete File 重写成 Position Delete File 过程只需要读 Insert File 的 Upsert 键,极大提升了合并的效率。此处效率的提升与表的字段个数成正相关,表上的列越多,提升越大。
END
精彩回顾
社区动态:
用户案例:
更多资讯
Amoro 社群
后台回复【社群】
或扫描二维码添加小助手,邀你进群~

