Hudi解读 | Apache Hudi设计与架构最强解读
感谢 Apache Hudi contributor:王祥虎 翻译&供稿。
最近Apache Hudi社区统计了过去几个月相关的maven依赖被下载的趋势统计。
通过这个折线图,我们发现了一个趋势:Hudi相关依赖的下载量从2020/3月的4万左右,突然在4月猛增到16万左右。也就是说,在短短的一个月内,其增长接近原来的4倍。作为一个只发布源码的项目,其依赖的拉取量足以很直观地反应用户对这个项目的关注度。而且,这份统计还是在4月份Hudi没有发布任何新版本的情况下。
Hudi 社区正在计划接下来的0.5.3以及0.6.0版本的发布,自三月份发布0.5.2后的短短一个多月的时间内,社区迎来了一段贡献的密集期。即将发布的0.6.0版本将包含多方面的特性、重构、优化等,敬请期待!
Apache Hudi(简称:Hudi)使得您能在Hadoop兼容的存储之上存储大量数据,同时它还提供两种原语,使得除了经典的批处理之外,还可以在数据湖上进行流处理。本文将介绍Apache Hudi的基本概念、设计以及总体基础架构。
1.简介
Update/Delete记录:Hudi使用细粒度的文件/记录级别索引来支持Update/Delete记录,同时还提供写操作的事务保证。查询会处理最后一个提交的快照,并基于此输出结果。
变更流:Hudi对获取数据变更提供了一流的支持:可以从给定的时间点获取给定表中已updated/inserted/deleted的所有记录的增量流,并解锁新的查询姿势(类别)。
效率的提升:摄取数据通常需要处理更新、删除以及强制唯一键约束。然而,由于缺乏像Hudi这样能对这些功能提供标准支持的系统,数据工程师们通常会采用大批量的作业来重新处理一整天的事件,或者每次运行都重新加载整个上游数据库,从而导致大量的计算资源浪费。由于Hudi支持记录级更新,它通过只处理有变更的记录并且只重写表中已更新/删除的部分,而不是重写整个表分区甚至整个表,为这些操作带来一个数量级的性能提升。
更快的ETL/派生Pipelines:从外部系统摄入数据后,下一步需要使用Apache Spark/Apache Hive或者任何其他数据处理框架来ETL这些数据用于诸如数据仓库、机器学习或者仅仅是数据分析等一些应用场景。通常,这些处理再次依赖以代码或SQL表示的批处理作业,这些作业将批量处理所有输入数据并重新计算所有输出结果。通过使用增量查询而不是快照查询来查询一个或多个输入表,可以大大加速此类数据管道,从而再次像上面一样仅处理来自上游表的增量更改,然后upsert或者delete目标派生表。
新鲜数据的获取:减少资源还能获取性能上的提升并不是常见的事。毕竟我们通常会使用更多的资源(例如内存)来提升性能(例如查询延迟)。Hudi通过从根本上摆脱数据集的传统管理方式,将批量处理增量化带来了一个附加的好处:与以前的数据湖相比,pipeline运行的时间会更短,数据交付会更快。
统一存储:基于以上三个优点,在现有数据湖之上进行更快速、更轻量的处理意味着仅出于访问近实时数据的目的时不再需要专门的存储或数据集市。
2.设计原则
流式读/写:Hudi借鉴了数据库设计的原理,从零设计,应用于大型数据集记录流的输入和输出。为此,Hudi提供了索引实现,可以将记录的键快速映射到其所在的文件位置。同样,对于流式输出数据,Hudi通过其特殊列添加并跟踪记录级的元数据,从而可以提供所有发生变更的精确增量流。
自管理:Hudi注意到用户可能对数据新鲜度(写友好)与查询性能(读/查询友好)有不同的期望,它支持了三种查询类型,这些类型提供实时快照,增量流以及稍早的纯列数据。在每一步,Hudi都努力做到自我管理(例如自动优化编写程序的并行性,保持文件大小)和自我修复(例如:自动回滚失败的提交),即使这样做会稍微增加运行时成本(例如:在内存中缓存输入数据已分析工作负载)。如果没有这些内置的操作杠杆/自我管理功能,这些大型流水线的运营成本通常会翻倍。
万物皆日志:Hudi还具有 append only、云数据友好的设计,该设计实现了日志结构化存储系统的原理,可以无缝管理所有云提供商的数据。
键-值数据模型:在写方面,Hudi表被建模为键值对数据集,其中每条记录都有一个唯一的记录键。此外,一个记录键还可以包括分区路径,在该路径下,可以对记录进行分区和存储。这通常有助于减少索引查询的搜索空间。
3. 表设计
有序的时间轴元数据:类似于数据库事务日志。
分层布局的数据文件:实际写入表中的数据。
索引(多种实现方式):映射包含指定记录的数据集。
支持快速,可插拔索引的upsert();
高效、只扫描新数据的增量查询;
原子性的数据发布和回滚,支持恢复的Savepoint;
使用MVCC(多版本并发控制)风格设计的读和写快照隔离;
使用统计信息管理文件大小;
已有记录update/delta的自管理压缩;
审核数据修改的时间轴元数据;
满足GDPR(通用数据保护条例)、数据删除功能。
3.1 时间轴
操作类型:对数据集执行的操作类型;
即时时间:即时时间通常是一个时间戳(例如:20190117010349),该时间戳按操作开始时间的顺序单调增加;
即时状态:instant的当前状态;
COMMIT:一次提交表示将一组记录原子写入到数据集中;
CLEAN:删除数据集中不再需要的旧文件版本的后台活动;
DELTA_COMMIT:将一批记录原子写入到MergeOnRead存储类型的数据集中,其中一些/所有数据都可以只写到增量日志中;
COMPACTION:协调Hudi中差异数据结构的后台活动,例如:将更新从基于行的日志文件变成列格式。在内部,压缩表现为时间轴上的特殊提交;
ROLLBACK:表示提交/增量提交不成功且已回滚,删除在写入过程中产生的所有部分文件;
SAVEPOINT:将某些文件组标记为"已保存",以便清理程序不会将其删除。在发生灾难/数据恢复的情况下,它有助于将数据集还原到时间轴上的某个点。
REQUESTED:表示已调度但尚未初始化;
INFLIGHT: 表示当前正在执行该操作;
COMPLETED: 表示在时间轴上完成了该操作。
3.2 数据文件
3.3 索引
全局索引:不需要分区信息即可查询记录键映射的文件ID。比如,写程序可以传入null或者任何字符串作为分区路径(partitionPath),但索引仍然会查找到该记录的位置。全局索引在记录键在整张表中保证唯一的情况下非常有用,但是查询的消耗随着表的大小呈函数式增加。
非全局索引:与全局索引不同,非全局索引依赖分区路径(partitionPath),对于给定的记录键,它只会在给定分区路径下查找该记录。这比较适合总是同时生成分区路径和记录键的场景,同时还能享受到更好的扩展性,因为查询索引的消耗只与写入到该分区下数据集大小有关系。
4. 表类型
4.1 Copy On Write表
对于updates, 该文件ID的最新版本都将被重写一次,并对所有已更改的记录使用新值
对于inserts.记录首先打包到每个分区路径中的最小文件中,直到达到配置的最大大小。之后的所有剩余记录将再次打包到新的文件组,新的文件组也会满足最大文件大小要求。
4.2 Merge On Read表
插入到日志文件:有可索引日志文件的表会执行此操作(HBase索引);
插入Parquet文件:没有索引文件的表(例如布隆索引)
5. 写设计
5.1 写操作
upsert操作:这是默认操作,在该操作中,首先通过查询索引将数据记录标记为插入或更新,然后再运行试探法确定如何最好地将他们打包到存储,以对文件大小进行优化,最终将记录写入。对于诸如数据库更改捕获之类的用例,建议在输入几乎肯定包含更新的情况下使用此操作。
insert操作:与upsert相比,insert操作也会运行试探法确定打包方式,优化文件大小,但会完全跳过索引查询。因此对于诸如日志重复数据删除(结合下面提到的过滤重复项选项)的用例而言,它比upsert的速度快得多。这也适用于数据集可以容忍重复项,但只需要Hudi具有事务性写/增量拉取/存储管理功能的用例。
bulk insert操作:upsert和insert操作都会将输入记录保留在内存中,以加快存储启发式计算速度,因此对于最初加载/引导Hudi数据集的用例而言可能会很麻烦。Bulk insert提供了与insert相同的语义,同时实现了基于排序的数据写入算法,该算法可以很好的扩展数百TB的初始负载。但是这只是在调整文件大小方面进行的最大努力,而不是像insert/update那样保证文件大小。
5.2 压缩
同步压缩:这里的压缩由写程序进程本身在每次写入之后同步执行的,即直到压缩完成后才能开始下一个写操作。就操作而言,这个是最简单的,因为无需安排单独的压缩过程,但保证的数据新鲜度最低。不过,如果可以在每次写操作中压缩最新的表分区,同时又能延迟迟到/较旧分区的压缩,这种方式仍然非常有用。
异步压缩:使用这种方式,压缩过程可以与表的写操作同时异步运行。这样具有明显的好处,即压缩不会阻塞下一批数据写入,从而产生近实时的数据新鲜度。Hudi DeltaStreamer之类的工具支持边界的连续模式,其中的压缩和写入操作就是以这种方式在单个Spark运行时集群中进行的。
5.3 清理
按commits / deltacommits清理:这是增量查询中最常见且必须使用的模式。以这种方式,Cleaner会保留最近N次commit/delta commit提交中写入的所有文件切片,从而有效提供在任何即时范围内进行增量查询的能力。尽管这对于增量查询很有帮助,但由于保留了配置范围内所有版本的文件片,因此,在某些高写入负载的场景下可能需要更大的存储空间。
按保留的文件片清理:这是一种更为简单的清理方式,这里我们仅保存每个文件组中的最后N个文件片。诸如Apache Hive之类的某些查询引擎会处理非常大的查询,这些查询可能需要几个小时才能完成,在这种情况下,将N设置为足够大以至于不会删除查询仍然可以访问的文件片是很有用的。
5.4 DFS访问优化
小文件处理特性会剖析输入的工作负载,并将内容分配到现有的文件组,而不是创建新文件组(这会导致生成小文件)。
在writer中使用一个时间轴缓存,这样只要Spark集群不每次都重启,后续的写操作就不需要列出DFS目录来获取指定分区路径下的文件片列表。
用户还可以调整基本文件和日志文件大小之间的比值系数以及期望的压缩率,以便将足够数量的insert分到统一文件组,从而生成大小合适的基本文件。
智能调整bulk insert并行度,可以再次调整大小合适的初始文件组。实际上,正确执行此操作非常关键,因为文件组一旦创建就不能被删除,而智能如前面所述对其进行扩展。
6.查询
|
|
|
|
|
|
|
|
|
|
|
|
6.1 快照查询
6.2 增量查询
6.3 读优化查询
|
|
|
|
|
|
|
|
|
HBase 官方社区推荐必读好文
关注 HBase 技术社区,获取更多技术干货