作者:Nadine Farah
译者:阳龙生
Apache Hudi社区一直在快速发展,开发人员正在寻找利用其强大功能高效写入和管理大规模数据集的方法。每周我都会收到一些常见的问题,这些问题与最大限度地提高Hudi体验的技巧和窍门有关。我遇到的最重要的问题之一是Hudi如何执行插入和更新,确保低延迟访问最新数据。这就是我们将在本博客中介绍的内容!
选择正确的存储表类型
对于快速追加,需要考虑的主要因素之一是选择正确的存储表类型。Hudi支持两种不同的存储表类型——写时复制(COW)和读时合并(MOR)。由于处理数据更新的方法不同,每种表类型都可能对插入和更新的性能产生不同的影响。
写时复制表(COW)
与MOR表相比,COW表的操作更简单,因为所有更新都写入了Apache Parquet格式的base文件。您不需要运行像(compaction)合并这样的单独服务来管理任何日志文件来提高读取或存储效率。通过完全重写文件以生成新的base文件来处理更新。因此,COW表表现出更高的写入放大,因为为了创建新的基本文件版本,会发生同步合并。然而,COW表的一个关键优势是它们的读取无放大,因为所有数据都在基本文件中可用,随时可以读取。查询所需的磁盘读取量很小,因为它们不需要读取多个位置或合并数据。
读时合并(MOR)
与COW表相比,MOR表具有更高的操作复杂性。MOR不是重写整个文件,而是将更新写入单独的日志文件,然后这些日志文件与base文件在以后合并为新的base文件版本。如果您熟悉Hudi,这是通过compaction服务完成的。需要compaction来限制日志文件的增长,这样查询性能就不会下降,存储也会得到优化。直接写入日志文件可以避免多次重写整个基本文件,从而降低写入放大率——如果您使用的是流数据,这种差异就会变得明显。这使得MOR表可以进行写优化。然而,由于需要读取基本文件和日志文件并动态合并数据,MOR表在压缩之间对快照查询具有更高的读取放大率(读取时间更长)。
COW和MOR表的注意事项
如果您的更新:插入比率很高,并且对数据接入的延迟很敏感,那么MOR表可能是表类型的一个好选择。流媒体资源就是一个例子——通常,您希望更快地根据见解采取行动,为用户提供相关和及时的信息。然而,如果您的工作负载更基于插入,并且您可以容忍合理的数据接入延迟,那么COW表是一个不错的选择。
基于你的记录主键选择正确的索引
通过利用索引,Hudi在插入数据定位记录时避免了极度消耗时间和资源的全表扫描。Hudi的索引层将记录键映射到它们对应的文件位置。索引层是插件式的,而且有几种索引类型可供选择。需要考虑的一件事是,索引效率取决于多个因素,如摄入了多少数据,表中有多少数据,是分区表还是非分区表,选择的索引类型,工作负载的更新量以及记录键的时间特征。根据性能需求和唯一性保证,Hudi提供了开箱即用的不同索引策略,可以分类为全局索引或非全局索引。
全局索引或非全局索引
非全局索引
Hudi确保一对分区路径和记录键在整个表中是唯一的。索引查找性能与正在插入记录中匹配到的分区的大小相对成比例
全局索引
这种索引策略在表的所有分区中强制主键的唯一性,即,它保证给定记录键在表中只存在一条记录。全局索引提供了更强的保证,但更新/删除成本随着表的大小而增加。
全局与非全局之间的主要考虑因素之一与索引查找延迟有关,这是由于唯一性保证的差异:
非全局索引只查找匹配到的分区:例如,如果您有100个分区,并且传入的批处理只查找最后2个分区的记录,则只查找属于这2个分区中的文件组。对于大规模的追加工作负载,您可能需要考虑非全局索引,如非全局bloom、非全局simple和bucket。
全局索引查看所有分区中的所有文件组:例如,如果您有100个分区,并且传入的一批记录具有最后2个分区的记录,则将查找所有100个分区中的全部文件组(因为hudi必须保证整个表中只存在一个版本的记录键)。这可能会导致大规模upsert的延迟增加。
Hudi提供的开箱即用的索引类型
布隆索引:这种索引用于高效地upsert管理和从文件组进行记录查询。该索引利用布隆过滤器,布隆过滤器是一种概率数据结构,有助于确定特定文件组中是否存在给定的记录。它有全局和非全局两种类别。
简单索引:这种索引策略提供了一种将记录键映射到相应文件组的直接方法。它将传入的更新/删除记录与从存储表中提取的键进行精简联接。它有全局和非全局两种类别。
Hbase索引:这种索引使用Hbase记录数据和数据在文件组位置的映射。它只有全局索引。
Bucket索引(桶索引):这种索引策略将数据用哈希桶的方法定位到文件组。它只有非全局索引类别。
一致性哈希的Bucket索引:它是上面桶索引的高级版本。虽然bucket索引需要为每个分区预先分配文件组,但通过一致的哈希索引,我们可以根据负载动态增加或缩小每个分区的文件组。它只有非全局索引类别。
更新负载比较重时要考虑的索引类型
Bloom索引:如果记录键是按照某些标准(例如,基于时间戳)排序的,并且更新与最近的一组数据有关,那么这是一个很好的索引策略,用于更新繁重的工作负载。例如,如果记录密钥是根据时间戳排序的,并且我们在最近几天内更新数据。
Bloom索引用例:假设每10分钟就有一批新的数据被摄入。我们假设新批次包含过去3天内的数据异常。根据Bloom索引,Hudi识别文件组中候选更新的记录,并从base文件页脚获取Bloom筛选器,并进一步修剪文件组中每个文件中要查找的记录。如果没有找到记录,它们将被视为只有插入(无更新)。
简单索引:如果您在表的整个范围内偶尔更新文件,并且记录键是随机的,即不基于时间戳,那么这是一个很好的索引策略,可以用于更新繁重的工作负载。
简单的索引用例:如果您有一个维度表,其中记录键是行程ID(随机UUID),分区是按城市ID划分的。如果我们想更新分布在一系列城市的10000次行程,Hudi首先根据传入的城市ID识别相关分区。从那里,Hudi通过执行精简联接来有效地定位包含记录的文件。
Bucket索引:如果每个分区存储的数据总量在所有分区中都相似,那么这是一个很好的索引策略。对于给定的表,每个分区的存储桶(或文件组)数量必须预先定义。这是Sivabalan 写的一篇文章,详细介绍了这一点。
Bucket索引示例:在Hudi中,定义了所需的Bucket数量后,Hudi会对记录键应用哈希函数,以便在Bucket之间均匀分布记录。hash函数为每个记录ID分配一个bucket编号。当更新发生时,Hudi将散列函数应用于记录ID,并确定相应的bucket。然后,Hudi将写操作委托给相应的存储桶(文件组)。
分区路径的粒度
分区是一种用于根据数据集中的某些属性或列将大型数据集拆分为更小、可管理的部分的技术。这可以极大地提高查询性能,因为在查询过程中只需要扫描数据的一个子集。然而,分区的有效性在很大程度上取决于分区的粒度。
一个常见的问题是分区设置过于细粒度,例如用<city>/<day>/<hour>划分分区。根据您的工作负载,可能没有足够的每小时粒度的数据,导致许多只有几千字节的小文件。如果您熟悉小文件问题,那么小文件越多,磁盘查找成本就越高,查询性能也会降低。其次,在查询方面,小文件也会影响索引查找,因为修剪不相关的文件需要更长的时间。根据您正在实现的索引策略,这可能会对写入性能产生负面影响。我建议用户总是从更粗糙的分区方案开始,比如<city>/<day>,以避免小文件的陷阱。如果您仍然觉得需要细粒度分区,我建议您根据查询模式重新评估分区方案,您可以潜在地利用集群服务来平衡写入和查询性能。
我希望这个博客能帮助你了解Hudi的一些特定部分,你可以调整,以便快速获得惊喜。没有具体的公式可供使用——您如何配置应用程序并对其进行调优,很大程度上将取决于工作负载类型。如果您对与工作量相关的upsert性能有更具体的问题,可以在Hudi社区找到我!