介绍
索引是一个关键组件,有助于 Hudi 写入端快速更新和删除,并且它在提高查询执行方面也发挥着关键作用。Hudi提供了多种索引类型,包括全局变化的Bloom索引和Simple索引、利用HBase服务的HBase索引、基于哈希的Bucket索引以及通过元数据表实现的多模态索引。索引的选择取决于表大小、分区数据分布或流量模式等因素,其中特定索引可能更适合更简单的操作或更好的性能。用户在为不同表选择索引类型时经常面临权衡,因为还没有一种能够以最小的操作开销促进写入和读取的通用性能索引。
从 Hudi 0.14.0 开始,我们很高兴地宣布 Apache Hudi 的通用索引 - 记录级别索引 (RLI)。这一创新不仅显着提高了写入效率,还提高了相关查询的读取效率。RLI 无缝集成在表存储层中,无需任何额外的操作工作即可轻松工作。
在本博客的后续部分中,我们将简要介绍 Hudi 的元数据表,这是讨论 RLI 的先决条件。接下来我们将深入研究 RLI 的设计和工作流程,然后展示性能分析和索引类型比较。该博客将以对 RLI 未来工作作为结尾。
元数据表
Hudi 元数据表[1]是 .hoodie/metadata/
目录中的读取合并 (MoR) 表。它包含与记录相关的各种元数据,无缝集成到写入器和读取器路径中,以提高索引效率。元数据分为四个分区:文件、列统计信息、布隆过滤器和记录级索引。
元数据表与时间轴上的每个提交操作同步更新,换句话说,对元数据表的提交是对Hudi数据表的事务的一部分。通过包含不同类型元数据的四个分区,此布局可实现多模式索引的目的:
• files分区跟踪Hudi数据表的分区,以及每个分区的数据文件
• column stats分区记录了数据表每一列的统计信息
• bloom filter分区存储基本文件的序列化布隆过滤器
• record level index分区包含各个记录键和相应文件组ID的映射
用户可以通过设置 hoodie.metadata.enable=true
来启用元数据表。一旦启用,文件分区将始终启用。可以单独启用和配置其他分区以利用额外的索引功能。
记录级别索引
从版本 0.14.0 开始,可以通过设置 hoodie.metadata.record.index.enable=true
和 hoodie.index.type=RECORD_INDEX
来激活记录级别索引 (RLI)。RLI 背后的核心概念是能够确定记录的位置,从而减少需要扫描以提取所需数据的文件数量。这个过程通常被称为“索引查找”。Hudi 采用主键模型,要求每个记录与一个键关联以满足唯一性约束。因此我们可以在记录键和文件组之间建立一对一的映射,这正是我们打算在记录级索引分区中存储的数据。
对于索引而言,性能至关重要。包含RLI分区的元数据表选择HFile作为文件格式,HBase的文件格式利用B+树结构进行快速查找。现实工作负载的基准测试表明,包含 100 万个 RLI 映射的 HFile[2] 可以在 600 毫秒内查找一批 100k 记录。我们将在后面的部分中介绍性能主题并进行详细分析。
初始化
为现有 Hudi 表初始化 RLI 分区可能是一项费力且耗时的任务,具体取决于记录的数量。就像典型的数据库一样,构建索引需要时间,但最终会通过加速未来的大量查询而得到回报。
上图显示了 RLI 初始化的步骤。由于这些作业都是可并行的,因此用户可以相应地扩展集群并配置相关的并行设置(例如 hoodie.metadata.max.init.parallelism
)以满足他们的时间要求。
重点关注最后一步“批量插入到 RLI 分区”,元数据表写入端使用哈希函数对 RLI 记录进行分区,确保生成的文件组的数量与分区的数量一致。这保证了记录键查找的一致性。
值得注意的是,当前的实现在初始化后修复了 RLI 分区中文件组的数量。因此用户应该倾向于过度配置文件组并相应地调整这些配置。
hoodie.metadata.record.index.max.filegroup.count hoodie.metadata.record.index.min.filegroup.count hoodie.metadata.record.index.max.filegroup.size hoodie.metadata.record.index.growth.factor
在未来的开发迭代中,RLI 应该能够通过动态重新平衡文件组来克服这一限制,以适应不断增加的记录数量。
在数据表写入时更新 RLI
在常规写入期间,RLI 分区将作为事务的一部分进行更新。元数据记录将使用传入的记录键及其相应的位置信息生成。鉴于 RLI 分区包含记录键和位置的精确映射,对数据表的更新插入将导致将相应的键更新插入到 RLI 分区。所采用的哈希函数将保证相同的键被路由到同一文件组。
写入索引
作为写入流程的一部分,RLI 遵循高级索引流程,与任何其他全局索引类似:对于给定的记录集,如果索引发现每个记录存在于任何现有文件组中,它就会使用位置信息标记每个记录。关键区别在于存在性测试的真实来源——RLI 分区。下图说明了标记流程的详细步骤。
标记的记录将被传递到 Hudi 写入句柄,并对它们各自的文件组进行写入操作。索引过程是对表应用更新的关键步骤,因为其效率直接影响写入延迟。在后面的部分中,我们将使用基准测试结果展示记录索引的性能。
读取流程
记录级别索引也集成在查询端。在涉及针对记录键列进行相等性检查(例如,EqualTo 或 IN)的查询中,Hudi 的文件索引实现优化了文件裁剪过程。这种优化是通过利用 RLI 精确定位完成查询所需读取的文件组来实现的。
存储
存储效率是设计的另一个重要方面。每个RLI映射条目必须包含一些精确定位文件所必需的信息,例如记录键、分区路径、文件组id等。为了优化存储,RLI采用了一些压缩技术,例如对文件组id进行编码(以UUID的形式) ) 转换为 2 个 Long 来表示高位和低位。使用 Gzip 压缩和 4MB 块大小,单个 RLI 记录的平均大小仅为 48 字节。为了更实际地说明这一点,假设我们有一个包含 100TB 数据的表,其中包含大约 10 亿条记录(平均记录大小 = 100Kb)。RLI 分区所需的存储空间约为 48 Gb,不到总数据大小的 0.05%。由于 RLI 包含与数据表相同数量的条目,因此存储优化对于使 RLI 实用起来至关重要,特别是对于 PB 大小及以上的表。
RLI 利用低成本存储来实现类似于 HBase 索引的快速查找过程,同时避免运行额外服务的开销。在下一节中我们将回顾一些基准测试结果以展示其性能优势。
性能
我们对记录级别索引进行了全面的基准分析,评估写入延迟、索引查找延迟和数据shuffle等方面,并与 Hudi 中现有的索引机制进行比较。除了写入操作的基准之外,我们还将展示点查的查询延迟的减少。实验使用Hudi 0.14.0和Spark 3.2.1。
与 Hudi 中的全局简单索引 (GSI) 相比,记录级别索引 (RLI) 的设计具有显着的性能优势,因为大大减少了扫描空间并最大限度地减少了数据shuffle。GSI 在数据表的所有分区中的传入记录和现有数据之间执行join操作,从而导致大量数据Shuffle和精确定位记录的计算开销。另一方面 RLI 通过哈希函数有效地提取位置信息,通过仅从元数据表加载感兴趣的文件组,从而显着减少数据shuffle量。
写入延迟
在第一组实验中,我们建立了两个管道:一个使用 GSI 配置,另一个使用 RLI 配置。每个管道在包含 10 个 m5.4xlarge 核心实例的 EMR 集群上执行,并设置为将批量 200Mb 数据摄取到包含 20 亿条记录的 1TB 数据集中。RLI 分区配置有 1000 个文件组。对于 N 批次的摄取,使用 RLI 的平均写入延迟比 GSI 显着提高了 72%。
注意:在Hudi中的Global Simple Index和Global Bloom Index之间,由于记录键的随机性,前者产生了更好的结果。因此我们在图表中省略了GSI的呈现。
索引查找延迟
我们还使用 HoodieReadClient 隔离了索引查找步骤,以准确衡量索引效率。通过在包含 20 亿条记录的 1TB 数据集中查找 400,000 条记录 (0.02%) 的实验,RLI 比 GSI 提高了 72%,与端到端写入延迟结果一致。
数据Shuffle
在索引查找实验中,我们观察到 GSI 大约有 85Gb 的数据shuffle ,而RLI只有 700Mb 的数据shuffle。这反映出与 GSI 相比,使用 RLI 时数据shuffle减少了 92%。
查询延迟
记录级别索引将极大地提高在记录键列上使用“EqualTo”和“IN”谓词的 Spark 查询。我们创建了一个 400GB Hudi 表,包含 20,000 个文件组。当我们执行基于单个记录键的查询时,我们观察到查询时间有了显着的改进。 启用 RLI 后,查询时间从 977 秒减少到仅 12 秒,延迟减少了 98%。
何时使用
RLI 总体表现出出色的性能,将更新和删除效率提升到一个新的水平,并在执行键匹配查询时快速跟踪读取。启用 RLI 也很简单,只需设置一些配置标志即可。下面我们总结了一个表格,突出显示了 RLI 与其他常见 Hudi 索引类型相比的重要特征。
Record Level Index | Global Simple Index | Global Bloom Index | Bucket Index |
Performant look-up in general | Yes | No | No |
Boost both writes and reads | Yes | No, write-only | No, write-only |
Easy to enable | Yes | Yes | Yes |
许多实际应用程序将受益于 RLI 的使用。一个常见的例子是满足 GDPR 要求。通常当用户提出请求时,将提供一组 ID 来标识要删除的记录,这些记录将被更新(列无效)或永久删除。通过启用 RLI,执行此类更改的离线作业将变得更加高效,从而节省成本。在读取方面,通过某些跟踪 ID 收集历史事件的分析师或工程师也将体验到来自键匹配查询的极快响应。
虽然 RLI 相对于所有其他指数类型具有上述优势,但在使用它时考虑某些方面也很重要。与任何其他全局索引类似,RLI 要求表中所有分区的记录键唯一性。由于 RLI 跟踪所有记录键和位置,因此对于大型表来说,初始化过程可能需要一些时间。在大型工作负载极度倾斜的场景中,由于当前设计的限制,RLI 可能无法达到所需的性能。
未来的工作
在记录级别索引的初始版本中有某些限制。正如“初始化”部分中提到的,文件组的数量必须在创建 RLI 分区期间预先确定。Hudi 确实对现有表使用一些启发式方法和增长因子,但对于新表,建议为 RLI 设置适当的文件组配置。随着数据量的增加,当需要额外的文件组进行扩展时,RLI 分区需要重新引导。为了满足重新平衡的需要,可以采用一致的哈希技术。
另一个有价值的增强功能涉及支持辅助列与记录关键字段的索引,从而满足更广泛的查询。在读取器方面,计划将更多查询引擎(例如 Presto 和 Trino)与记录级别索引集成,以充分利用 Hudi 元数据表提供的性能优势。