hive 出力不讨好
在传统 Hive 数仓的场景下,更新操作是比较重的。举一个比较直观的例子。假设有一个hive分区表,其中一个分区中有500个文件存储着100w条数据,如果我们有100条需要更新的数据分布在这个分区中,那么hive如何进行更新的呢?
- 从这500个文件中读取100w条数据
- 在内存中将数据和这100条数据关联后取最新值
- 把最新的100w条数据写入临时目录中,然后覆盖原先的数据
可以想象这三步操作都是比较重的,我们可以想一下,仅仅更新100条数据,有没有必要读取100w条数据呢,按照极端情况考虑,100个文件最多只能分布在100个文件中,而hive需要把整个分区下500个文件中的100w条数据全部读取出来属实比较笨蛋。
思考一下,更新100条数据hive为什么要把分区下的数据全部读取出来呢?因为hive只知道这100条数据在这个分区下,具体是在哪个文件中,它不知道呀,所以需要吭哧吭哧的把整个分区文件都读取出来,难为孩子了。如果不是分区表,它需要吭哧吭哧的读取整个表的文件呢。
如果知道数据是在哪个文件中存储就好了。
于是hudi想了一个办法,它通过索引可以快速的定位到每条数据存储的文件位置。接下来咱就唠唠hudi吧。
实时数仓引擎Hudi
文件组织结构
要唠hudi的文件组织结构,得先讲讲hudi的表类型,类型不同,文件组件稍微有点差别。hudi中有两种表类型,分别是COW和MOR。
- COW:copy on write,写时复制,数据文件由base file组成,也就是parquet文件;
- MOR: merge on read,读时合并,数据文件由base file和delta file组成,delta file也就是log文件,基于行存的avro格式;
hudi提供类似hive的分区组织方式,主要区别在于分区路径下的文件组织形式。
左边这张图是MOR表的file group组织形成,右边这张图则是COW表的file group组织形式。
写时复制 vs 读时合并
- 写时复制
为了方便理解,我这里简单介绍写hudi表数据写入流程,省略一些和此处无关的流程。新建一个hudi表,表中没有数据。此时来了10条数据,未查到这10条数据的索引信息,因此认为这10条数据全部是新增数据,于是把这10条数据全部写入file1-v1.parquet文件中。然后又来了5条数据,其中有3条数据是更新数据,2条是新增数据,那么hudi会把这两条新增数据新写入到file2-v1.parquet文件中,通过索引发现这三条更新数据在file1-v1.parquet已经存在了,于是会把file1-t1.parquet文件中的数据读取出来,然后和这三条更新数据做关联取最新值,最后把这10条最新的数据写入到file1-v2.parquet文件中。可以看到,hudi在更新时需要先读取旧版本数据,然后关联,最后把数据重新写入到新文件中。只有3条数据,实际重写了10条数据,而且还有读取过程,因此会影响写入性能。
- 读时合并
为了解决写入性能瓶颈的问题,hudi引入了mor表。同样的,新建一个hudi表,表中没有数据。此时来了10条数据,未查到这10条数据的索引信息,因此认为这10条数据全部是新增数据,于是把这10条数据全部写入file1-v1.parquet文件中。然后又来了5条数据,其中有3条数据是更新数据,2条是新增数据,hudi会把这两条新增数据新写入到file2-v1.parquet文件中,通过索引发现这三条更新数据在file1-v1.parquet已经存在了,于是把这三条数据写入到file1-v1.log文件中。可以看到这个流程不存在写放大的问题。数据MOR表时会把log文件和parquet文件做合并,这样的话会影响查询性能。hudi提供了一种表服务:compact。可以根据场景设置合适的合并策略,基于时间或者基于commit次数的合并策略。当满足合并策略时自动的合并log文件和parquet文件。合并操作可以异步触发也可以同步触发,同步触发的话会阻塞写入进程。
今天先写这么多吧,hudi的高级特性在表服务这里,可以提高写入性能和查询性能,东西挺多的。后续的话主要写写一下几个关键点:
- 数据布局优化技术
- 索引及bucket index
- schema变更