尚硅谷大数据技术之数据湖Hudi-2

尚硅谷大数据技术之数据湖Hudi-2

核心概念

基本概念

时间轴TimeLine

文件布局File Layout

存储方式

image-20240308143428151

1
2
3
4
5
接下来看一下hudi的文件布局啊,什么意思呢?也就是说hudi它在存储系统上面,它是怎么一个表现形式啊,说白了,比如说咱们用HDFS作为它的数据存储的话啊,它以什么样的格式,什么样的目录来存储对应的数据,还有原数据啊,那其实这个这个地方我们可以类比为hive表。Hive的话,它一张表对应HDFS是不是一个目录啊,对吧?那目录名就是表名,那我们知道hive是不是可以有分区,它在表明目录下面是不是还有分区目录,那在分区目录当中存放的是不是就是数据文件,这个是hive对不对啊

那么hudi也有点类似啊,有点类似,那hudi一张表呢,它也是体现为文件系统的一个目录啊,也就是表的名字就是目录的名字,那么在之后呢,它同样有什么呢?也有分区这个概念,那这个分区跟hive有点类似啊,它也是一个分区,是一个目录,一个分区是一个目录。那在分区当中呢,也包含了数据文件啊,那么区别在于哪呢?区别就在于来,我用红色的标出来,它在表的目录下面,除了数据的这些分区目录之外啊,它还有一个是原数据的目录,里面存放了hudi的一些原数据,这张表的原数据啊,表的原数据它是一个.hoodie这么一个文件夹名称,它这个文件夹在这个文件夹当中呢,就有呃,Timeline instant相关的一些原数据信息啊,都在这里面。

啊,所以大家可以看到,呃,如果是hive的话,它的原数据是不是存放在数据库当中了,默认是derby,我们一般会改到像MYSQL这种数据库是不是啊,那么对于hudi来讲呢,它用的就是直接在文件存储器,呃,直接在文件系统上啊,以目录,以文件的方式来存储原数据啊,这是一个。第一个要注意的地方,那第二个地方呢,就是它的分区路径,呃里面呢,呃,数据文件它跟hive不一样了啊,它基本上只有两种啊,第一种是什么呢?列式存储的parquet文件,第二种呢,是行式存储的log文件,他这个log文件就是avro编码的啊,前面我们也介绍了hudi说它使用了开源的数据格式,那其实这两种格式就很常见的啊。

image-20240308144030533

1
2
3
行,那可能简单这么一说大家应该都明白,接下来给大家看一个呃,实际的例子啊,那这边我已经启动了我的hudi的集群,那大家看看我之前跑过的一个hudi表就可以了啊,现在大家应该还没跑啊,但是大家先搂一眼吧,这是我之前跑的,呃,随便找一张表,那么大家看一下这个是我的路径而已。那接下来这些是什么表明,这是忽底表的表明了,跟hive一样啊,那我随便点一张表啊,比如说OdS_log,好,这个就是忽hudi的表名路径,在这个路径下面大家可以看到有什么一个.hoodie,这个就是所谓的原数据,那后面这些呢,是有我这张表是按照天来做一个分区的啊,所以大家可以看到它的分区目录名啊,就是某年某月某日,那我们呃,来点一下数据的分区来点。

进来之后,大家可以看到目前我这张表啊,有一个文件是什么点log。当然也会有什么.parquet啊,当然我现在这张表还没有让它生成这个.parquet,也就是其实没有执行一个compassion啊,这个后面再聊啊,那正常的话还有.parquet的这么一个文件啊

image-20240308144433324

image-20240308144512890

1
那除此之外大家看看是不是还有一个东西,这个不是目录了,你看它不是目录啊,它是个文件啊,每个分区它还有一个原数据文件来点一下啊,我们看一下它里面很简单,就是什么呢?有一个commit time,说白了就是这个分区路径创建的时间,时间戳啊,它是一串数字,是年月日10分秒啊,你看2022年9月20号18:46:40 692ms啊,这个是精确到毫秒是不是啊,然后还有一个什么分区的深度,也就是说你分区路径下还可以有啊其他分区。就多级分区这种样子了,现在我是一级分区,所以它的深度只有什么1啊好,那这个点log其实是数据文件,你看我点一下你看。在这边就能看到一些数据了啊。这是数据啊,阿波罗格式的,行,这个简单搂眼啊,另外一个分区也一样啊。

image-20240308144836780

1
2
3
4
5
6
接下来我们搂眼这个原数据目录啊,点.hoodie。好点一下进来之后啊,我先刷新一下,那么大家在这里可以看到一些文件啊,首先是一个点开头的啊,它会自己会用的一些东西,对吧?像什么schema,还有临时目录temp啊这些东西,还有归档archieve啊,hudi配置啊这些

除此之外还有另外一些重要的东西是这个,那前面是什么呢?时间戳年月日10分秒毫秒对吧,精确到毫秒的,大家注意,我再强调一遍,它的时间戳,嗯,并不是说按照距离1970年1月1号啊,0点00:00:00 0ms距离多少个毫秒,不是这个啊,它直接就是年月日拼接在一起,10分秒拼接在一起,好另外呢,我们可以看到它后面都是什么,deltacommit,我我们前面聊了instant里边啊,其中的一种动作是不是叫增量提交,对吧?啊,也就是说我其实使用的方式是增量提交,deltacommit,另外呢,还记得instant。啊的组成吧,啊,我们看这个文件名就好,前面这个是不是instant里面的一个什么呢?是不是它发生的时时间嘛,第二个什么什么commit,这个是不是他的action。在后面这个是什么呢?就是它的state,你看inflight, requested对吧,当然还有一个是completed对吧?

那么有的人就有疑问了,那为什么你看同一个时间,同一个动作既有。这个又有这个又有没后缀的呢?那么大家注意看一下前面它们的大小分别是什么,也就是说,呃,调度中还未开始的requested,它是空的,那就是没有嘛,就不在这个状态嘛啊,那正在执行中的inflight也是0,那还有一个,这个是完成的,那说明它的状态就是已经完成的,是这样啊,它每一个action,它就是三种状态都对应一个文件啊,都对应一个文件,其他的也一样,你看。这个是不是有2对吧,那你再看这个,这个是不是有3个啊,这个是不是有3个,这个是不是有3个啊,就大概的去搂眼就行,那其他路径基本都是你现在看到的大部分是空的啊。对吧。还有另外的这个aux里面有一个ckp_Meta啊,一些具体的原数据,确认好的原数据。还还是一样啊,有什么complete,inflight。还有这个什么配置对吧。这些嗯,平时我们应该用不上,你看像这个,这是Rose DB pass啊在哪里,然后超时时间什么什么一堆东西,你看参数名就能读得懂。那临时路径呢,是空的啊,Skimer也是空的,现在archive归档也是空的,就看你有没有用上啊,也没有用上啊,刷新一下看一下户的配置啊。
好。对吧,这里就。有一些护底生效的配置是什么样?就这张表的一些配置啊,行呃,这个简单搂页就可以了,这个是我们现在提到的文件布局,也就是说它在文件系统上是怎么存储的啊,要按照什么规则存储?
文件管理

image-20240308171259464

1
2
3
4
5
6
7
8
9
那么这个文件布局当中,我们还要了解一个他对文件的一个管理。因为我们知道hudi前面也聊了一些特性,它支持那个呃版本控制,然后呢又可以去做一些compassion,对吧?那这个时候就注意新版本跟老版本文件的一个问题了。那我们先了解一个概念啊,看这张图呃,这个是一个分区路径啊,大家注意啊,外面这个大黑框表示的是某一个分区的护底表的某一个分区目录了。在一个分区当中,刚才带大家看的是不是有.log文件了啊,其实也有可能有点.parquet文件是吧?好,那他还做了一件事,他将这些parquet跟log封装成一个一个的,也不是封装了啊,就是划分为一个一个的文件组。大家注意叫文件组file group。那每一为什么叫group呢?因为它每一个组里面存储了多个文件片,每一个文件片代表一个版本,能理解吧?啊,比如说file slice1啊,这是老版本,file slice2就是现在目前最新的版本。

那我们看一下具体的说明。呃,hudi将表组织成文件系统的目录,对吧?这个刚才看过了,表名就是目录名啊,分区也是个目录名,表划分为多个分区,对不对啊?分区是包含该分区的一个文件夹,类似于hive啊,这前面这两点好理解。在第三个啊,每个分区中文件被组织成什么文件组?另外有一个词大家记住,由文件ID唯一标识,什么意思呢?啊,我在一个分区路径下可能有多个文件组,每一个文件组都有其对应的固定的ID,每一个组有一个固定的文件ID啊,记住这一点啊。好,那每个文件组里面包含可能有多个文件片叫file slice对吧?文件片啊每个文件片包含什么呢?大家注意一个基本文件.parquet的这是列式存储,多个日志文件.log的后缀啊,这是多个啊。

那么正常来讲呃,在一个基本文件就表示某一次提交或者合并后,在那个时刻生成的一个所有的数据,这不好理解是吧?那后续再来聊啊,也就是说正常的是有一个.parquet的文件的啊,要么是提交之后,要么是合并之后啊,会生成的.parquet的文件。呃,那么如果是.log文件,这是我们前面简单提到的是MOR这种表里面啊,它会有这种增量提交,那就是.log,这些日志文件就包含自生成基本文件以来,对基本文件中的数据插入和更新。那COW没没有这个是copy on right啊,就写示拷贝,这是hudi里面的两种表啊,回头我们后面会介绍啊,先留个印象,说白了文件片就是有parquet有log能理解吧?parquet是一个log是多个啊

另外一件事就是为什么会有多个文件片呢?因为我们保存了多版本多版本的数据,多版本并发控制。啊,那每一次compassion操作它会合并这种点log文件,还有parquet这种基本文件,产生新的什么文件片。哎,大家注意这句话,当我执行一次compassion呢啊这些多个.log文件就会跟parquet的进行一次合并,生成一个大的新的parquet文件啊。那这样的话你看比如说他们那他们呃这个新生成的发可能文件是不是最最新最全的数据啊,对吧?啊,这样就生成了一个新版本的一个一个数据文件。

另外有我们知道instant里面有一种操作叫清理,对不对啊?它就会清理不使用的或者旧的文件片,对吧?已回收。因为可能我经过多次compassion之后啊啊,比如说我进行了十次compassion,那么这十次是不是会每一次compassion都会对应一个新版本的数据文件生成,是不是?那时间越久这个compassion越多的话,你不可能所有的版本都存着,是不是?那肯定有个策略说,哎,我我满足什么规则的这些旧的版本文件片,我要把它清理掉啊,是有这么一个事儿。

image-20240308172912723

1
2
3
好。另外注意一个事啊,那我们先来聊一下parquet的文件的一些细节。这个基本文件它在里面的footer的meta里面是记录了什么record key,它里面用的是布隆过滤bloomfilter。说白了就是有一个索引,同学们啊有一个索引。嗯,他这样通过这个东西就能高效的去检测这个record key在不在啊,只有不在的时候才去呃需要去扫描整个文件,去消灭假阳性。我们知道布隆过滤是不是有假阳性啊,假阳性就是说布隆过滤这种实现方式,我们只能百分百确定啊不存在。但是如果不能过滤显示存在,那是不是也有可能它不存在,对吧?有个假阳性率,或者咱们直白大白话来讲,就是有一个准确率的问题。你说不在那就一定不在,但是你说在那只能说可能在能理解这个意思吧,因为你说在是有准确率的。啊,这个布隆过滤呃,自个去了解吧,它就有一个哈希函数,还有多个哈希函数,哎,还有K值啊,怎么样去计算?嗯,呃总而言之,这一段话什么意思呢?也就是说呃这个parket里面它记录了每一条数据的一个唯一key,叫record key。啊,这个词大家稍微记一下啊,会经常提到,经常用到啊。举个例子啊,我有张表啊,别管分不分区呢,比如说我有数据123啊1A2B3C这么三条数据吧。啊那假那么这三条数据刚好就在一个parquet文件里面啊啊,就别说刚好就是在一个parquet文件里面。这个时候呢其实呃这个Parquet文件会记录一个索引index。那我们可以基于布隆来做这个索引的查找。因为布隆布隆会快一点,并且节省空间嘛,效率高嘛,是不是啊?布隆过滤。那这个时候呢哎比如说我现在要呃更新或者插入,这个时候你是不是得嗯先判断一下相同key的在不在。这个key是由咱们去呃可以由我们去指定的啊,就是这个key啊,比如说1A这个key,我用第一列作为key,也就是说第一行的key是1,第二行key是2。

啊,有点像什么?有点像关系型数据库里面那种组件这种概念能理解吧?啊,类似于组件这个概念啊有点类似啊,我只是说类似好这个时候比如说我要插入一条2D的数据,那这个时候他会呃判断一下2这个key有没有啊。这个时候他请通过这个索引机制,通过布隆的方式呃就能判断到哎我这个二是存在的,那这个时候这条数据就可以单独去处理这条把它更新掉,这也是hudi实现呃行式,还有部分列的upsert的这种呃很重要的索引机制啊,这个我在这里简单提一嘴啊简单提一嘴。
1
好,再看下一点hudi的log,也就是点log这种后缀的是avro的编码,对吧?他通过积攒buffer,并且以log block为单位写出。也就是说他并不是一条一条写的,就想说这一点而已,而是攒一批数据咱们去写一次log。那每一个呃log block一个文件,一个日志快吧啊,咱们叫log快吧。有一个魔法值magic number,大小size、上下文content,呃文本,还有一个footer等信息用于数据读、校验和过滤。当然这些事不需要我们去关心啊,这是他内部实现的一个机制,对吧?但是既然聊到就简单说一句

image-20240308173513479

1
2
3
那下面这张图就很明显了,这是两种格式的文件,一种是parquet,一种是log。那parquet文件重要特性就是会做一个什么呃,文件的原数据里面会有一个索引啊,可以用布隆,可以用其他的索引方式。那log就更复杂了,对吧?他记录了一堆东西。好,这个是文件布局。

好,我们总结一下啊呃其实总结就是这张图,还有上面这张图。第一个呢就是我们在文件系统啊是以什么方式存储的。一个表就是一个目录名对吧?一个分区也是个目录名,那跟呃表目录下除了分区目录,还有一个原数据目录。点呼顶啊,就就这就完了呗啊,那每个分区目录里面啊是数据文件,要么是点part要么是点log啊,就这两种就完事儿了呗啊,再往下走。呃,就是这里要注意hudi的一个多版本控制啊,就是每个分区路径下面它是以文件组的方式来组织的啊,有多个文件组,每个文件组有一个唯一的ID啊,记住这一点啊,那每个文件组里面可能有多个文件片,每一个文件片包含了一个parquet多个log啊,具体有几个不一定。当你进行合并啊或者先提交啊啊生成一个新的版本啊,那最新最全的数据就在新版本里面嘛。啊那老的版本也是个文件片,新的版本也是一个文件片。啊,另外呢有一个清理清理器对应的清理策略,会去清理旧的老的文件片啊,这就完事了嘛。

索引

原理

image-20240309142816952

1
2
3
那刚才也聊到了一个关于索引,对吧?那现在咱我们就来好好聊聊这个索引的一个东西。那一开始也介绍了hudi啊通过索引机制提供高效的upsert。那接下来问题就来,我是怎么来高效的啊,其实前面也简单讲过了啊,它具体是给定一个东西叫hoodiekey对吧?一个key那它是由什么组成呢?一个叫record key记录键对吧?Record就代表你数据的一条一条的记录,就叫record key啊。然后加上什么呢?分区路径partitionpath。大家注意这两个东西组起来叫hooodie key,它与文件ID那么刚才聊到了文件ID是什么?一个文件组有一个唯一的文件ID啊,记住了。好,那也就hooodie key跟文件ID建立了一个唯一的映射。这种映射关系第一次写入文件后保持不变。大家注意第一次写入文件后保持不变这句话什么意思?也就是说这就代表这个映射关系是固定的,是唯一的啊,就这个意思啊,所以呢一个文件组包含了一批数据的所有版本记录啊,我这个前面也聊了对吧?啊,有点拗口。但是我们具体解读就是什么呢?一个文件组里面有多个文件片,每一个文件片都是不同的,是呃不同的版本,对不不对啊,

另外这个索引可以用来干嘛?用来区分消息是插入还是要更新,是不是?嗯,如果你是我找得到,也就是说我我新来的这条数据,呃,如果原先数据里边没有,那我就是要做insert嘛。那如果原先数据里面有了相同的key,那说明这条数据是要干嘛?是要更新嘛啊就这个意思,那索引是不是啊就很重要了啊,所以就很重要。

image-20240309143549664

1
2
3
那下面是官网给出的例子啊,这个索引有什么用啊?再进一步理解一下,hudi为了消除不必要的读写,引入了索引。有了索引之后,更新的数据可以快速被定位到对应的file group。因为我们前面也聊到了,就是它这个索引是不是hoodiekey跟file group那个文件ID的绑定,对吧?直接看图吧,白色的是基本文件,也就是parquet,黄黄色是更数数据,先别管什么parquet .log了,反正就是这是现现有的数据。呃,然后黄色是后面新过来的数据啊,可能是插入,可能是更新,对吧?那如果有了索引什么意思呢?我们一共有几个?123456788份这个更新的数据,每一个是25兆。啊,那这个时候呢呃比如说你如果没有索引,大家想一想,比如说就看这个就好了。这个25兆的文件我怎么知道我要往哪一个方去更新呢?你是不是所有的地方都得干嘛都得去扫描一遍,是不是扫描一下,你这样全量扫描,扫描一下你的这些数据是该往哪里去写,去往哪里更新,效率特别低。但是如果有了索引,这25兆是不是就知道哎他要更新的东西在哪,哎,就是这一百兆对吧?另外这是另外的25兆的更新数据,他是不是就知道他对应的呃数据在这100兆里面。我举个例子啊,这100兆里面有123,那这25兆里面就有一的更新,二的更新,对吧?那这一百兆是456,那这这黄色的这个里面是五要更新对吧?那你看嘛它要更新的是5。他是不是找到无所在的基本文件就行了,他不用所有的基本文件都去扫一遍,能理解这意思吧?也就是说他知道他的目标在哪,这样的话他我们的合并开销是多少啊啊你看这一边呢是100呃加2个25,这个是不150对吧?这是四个文件组嘛,呃那这样的话我们合起来是不是开销就是600 150乘以4嘛,对不对?这个很好理解对不对?这个是大家都知道你要的对象在哪里,对吧?目标很明确,那你就开销少好另外一种没有缩音的时候,这是原先来四个基本文件啊,四个基本文件在这摆着啊,有400兆的数据,然后呢有八次的更新,每一次二十五兆。那现在问题就来了,这25兆他知道要去找谁吗?他不知道吧。所以你这一百兆的是不是要每一个基本文件是不是都要跟所有的更新数据做一次匹配啊?对不对?那这个时候你看它开销是多少,每100兆就得跟这些25兆有八个,这边合起来是200对吧?他是不是要扫描200兆的数据,哎,然后把属于自己更新的部分摘过来。所以它的一次开销是多少?100加200,他自己100嘛,所有的增呃更新是200嘛,加起来是不是300对吧?好,那下一个基本文件,100兆的数据它也一样,它是不是也要在这200兆里面找到属于自己的更新数据,它的开销是不是也是300兆?好,那这四个基本文件这么一下呢,它的开销是多少?1200兆,那你看这个差距是不是差了一倍啊?

对吧?这个就是索引的好处,让你不会漫无目的。那如果用生活中的例子来讲,这就索引的作用是不是?好,这是它的一个原理啊。
索引选项
1
2
3
4
5
6
7
8
9
10
那理解完索引的基本原理,接下来我们聊聊细节了,呃,那是从索引的作用来讲,那么接下来忽底它支持哪一些索引的类型,说白了也就是索引的实现方案,实现方式

那么简单总结来讲可以分为四大类啊,四大类第一类呢,就是我们前面讲到了布隆索引,它就是用了布隆过滤器来判断,诶,我对应的这一条数据在不在,那这条数据怎么判断,是不是有个hoodie key啊,那怎么判断这个key在不在呢?就是我们指定了一个数据键,记录键,还有分区路径跟file group ID文件ID建立一个唯一映射啊,就判断这个文件组里面这条数据在不在呗,啊那判断在不在,布隆是很常用的一种啊,它也是默认的配置啊,那我们前面讲呢,它的优点是什么?效率高,而且我不用依赖外部的系统,数据与索引可以保持一致性,缺点就是布隆这种过滤布容过滤这种算法,它本身的一个假阳性的问题,说白了就准确率,也就是说你说在它还真不一定在,因为你可能说错了啊,因为他有一个哈希冲突的问题啊。那是你说不在,他就一定不在,能理解我意思吧?呃,当然它正常来讲用它也够了啊,也够了啊,就是兼顾效率啊

另外一个是简单索引,简单索引呢,它其实是将我们更新还有删除操作的,这个新数据跟老数据进行join,那么大家看到这个就知道这个操作特别重了,它实现起来就是思路上最简单,但是它的缺点也超级明显,性能太差,你想想那如果数据一多了呢,你这个join那还得了啊。对吧,那那那那不行啊。

那还有一种叫hbase索引。那什么意思呢?他也不是说hbase实现什么算法,而是什么呢?将索引的这个数据存放在hbase里,那说明什么?说明你的所有数据太多了啊,太多了,那我们在插入数据的时候,我们是不是要插入分区下面的某一个文件组啊,那这个时候你在插入的过程中,呃,福迪呢,他会向hbase发起这个请求,读取请求,就查一下这张索引表,看一下,呃,这个数据在不在,也就是说它的区别就在于索引的存储位置啊,存储位置。我直我直接将全量的索引都保存在hbase,你就现查hbase呗,就这个意思啊,对于小批次的key查询效率是比较高的,这是它的优点,但是缺点也特别明显,什么明显呢?你这是要借助外部系统。那你增加了运维的压力,再者说如果hbase你用的不好,或者说你配置的不对啊,像什么内存啊,GC啊,还有它的线程啊,各种东西你没配好,也就是说你hbase并没有用好,这个时候它的QPS上不去,反而成为你的瓶颈。对吧,所以你用hbase index的前提是,嗯,你对hbase比较了解啊,你也知道怎么去让hbase发挥出它应该有的性能。好马配好鞍嘛,能理解我意思吧

再有一个是忽底后期版本就开始支持flink了,那这个时候他因为前面这些是针对于Spark而言的用户,hudi最早它就支持的一个Spark引擎啊,主要支持Spark引擎,那么后期版本才出了一个支持flink,那这个时候依赖于flink呢,它单独对于索引存储可以存在哪呢?呃,可以存储在flink那个。Think算子的一个状态里面去啊,可以存到flink的状态,因为flink本身是呃,有状态的计算嘛,对吧,它用了flink的状态作为底层的索引存储。每个数据在写入之前都会计算目标的一个bucket的ID啊,它的优点呢,就不同于布隆啊,它避免了每次重复的文件的一个查找。那缺点呢,嗯,缺点我觉得可能的缺点就是对于因为你你这个索引也是存在这个flink状态里的嘛,那如果你的这个索引数据特别大。那flink的状态是不是变得特别大,那进一步是不是就会影响flink这个check point是吧,另外一方面会影响咱们flink的资源使用,当然你可以对flink进行大状态的调优,你可以使用RODB,你可以开启增量检查点啊等等这些,那就看你对flink熟不熟了,是不是啊。
啊,所以我这边注意写的一点啊,flink只有一种是什么state based index。其他的index是Spark可选的配置啊,所以你要注意你用的是什么啊,这个是我们hudi索引,这个索引里面的可选的不同方案不同类型。
全局索引与非全局索引
1
2
3
4
5
6
7
在hudi的索引当中啊,它又区分了全局索引还有非全局索引。那所谓的全局索引呢就是整张表而言建立的索引。也就是说即使你做了分区,那么我是整个所有的分区一起来看,那每一个key啊也就是那个所谓的record key啊,数据键它是唯一的,不会有重复的。即使你的数据是在不同的分区,那我也不允许你有重复的这个键啊,那这样就能够确保给定的一个键只有一个对应的记录整张表的范围内。但是这也有一个很明显的缺点呢,如果我这张表特别大,那大的你受得了吗?对吧?那你随着你咱们表的数据量越来越大,你去做一个更新删除操作的时候,你这个性能就越差,对吧?因为你要全表的去匹配,那这样的话即使你做了索引,但这样效率也高不了。所以这种全局索引呢更适合用于小表啊,就是表的数据量并不大,那其实用全局索引呃比较理想。

那第二类就是非全局,所谓的非全局,那它的范围是什么呢?分区啊对它在一个分区里面能够保证这个key的唯一性。也就是说一个分区里面这个key只能有一个,那不同的分区那里key相同我管不了。对吧?那它是依靠它写入器为同一条记录,就是同一条数据。它的更新也好,你的删除也好,都提供相同的一个分区路径,因为你这条数据是固定的嘛。呃,或者说咱们前面不是讲了一个hoodiekey嘛啊那因为它默认的实现其实就是非全局的。所谓的非全局是咱们的record key,哎,就是什么呢?那个record key再拼接上一个什么呢?partionpath对吧?啊,我的记录键在拼接上一个分区路径,这样它不就唯一了吗?对吧?那这样的话呃你对相同一条数据的更新也好,删除也好,它都能定位到这条数据所在的分区。那这样就可以大幅的提高一个效率。那这样的话大即使是大表啊,相对于全局索引来讲,呃,它的性能会更加的好一点,这个非常好理解啊。

那从索引的维护成本跟写入性能考虑,呃,大家都知道这个全局索引难度更大啊,性能更差。如果你数据量大的话啊,那么好,既然聊完这个,那我们再对比一下前面聊到的这几个索引类型。在这当中哪一些可以设置全局的,哪一些只能是局部的呢?啊,那么大家注意了,对于hbase这个索引来讲,他就是全局索引啊,它没有非全局啊,它就只有一种全局索引。

那么对于布隆也好,还有简单索引也好,这两种索引类型你可以选择是全局索引或者是非全局索引。这个可以通过参数来配置。那当然我前面也提到这个主要应用于Spark引擎,是不是啊?那具体的配置方式呢就是这样啊,就是一个参数啊,hoodie.index.type=GLOBAL_BLOOM, GLOBAL_SIMPLE,这个是一个全局跟非全局。那如果数据量足够大,那一般来讲还是用非全局,所以会好一点啊,这也是一个默认的啊,大家注意这个是默认的。这是一个。
索引选择策略
对事实表的延迟更新
1
2
3
4
5
6
7
那么接下来就是我们这么多个索引怎么来选择呢?或者说什么样的场景更适合什么样的索引。这个那么下面给到大家的是官方的一个资料啊,这个描述也好,这个动图也好,都是官方做的那我们一起来看一下,第一个场景是对事实表的延迟更新,什么意思呢?呃,熟悉数仓建模理论呢,我们都知道我们通常会去构建一些事实表还有维度表。对吧那对应呢我们会去构造各种模型,像什么新型模型、雪花模型、星座模型,这是基于维度建模理论。总而言之呢,我们通常在数仓里面都会有一个事实表和维度表。那事实表其实就是对应我们的业务过程。那比如说你如果你是电商场景,那你应该就有大量的交易。比如说订单的数据,支付的数据啊,还有比如说像什么加购物车的数据,这种都是属于咱们的事实数据。那这一些呢,我们通常首先会在数据库当中存储大量的这些事实的交易数据,对吧?那除了电商场景,还有其他的像共享出行的什么行程表,股票买卖记录表啊,这都是不同业务场景下的一些事实表啊,电商的订单表。呃,那事实表有个特点就是什么呢?啊,随着你的业务在不断的就用户在使用啊,不断的操作,不断的去执行一些业务过程。那么你事实表的数据应该是一直在不断的增长。就比如说订单好了,你每天都有人下单,那你这个订单的数据应该是越来越大越来越大。啊。另外呢这些事实数据还可能发生什么呢?更新更新。而且更新的话我们还要考虑一个事情,它更新的数据大概呢是很久以前的,还是说最近的呢?那么大家可以想一想,比如说电商的订单表就好了,一笔订单的状态要发生改变对吧?啊,比如说咱们退单呢,或者从下单变为支付状态啊,或者再再到最后的呃签收状态等等。这些看你业务怎么定义啊。呃这些是不是针对于最近的这些订单才有可能啊,比如说你距间隔了一个月以上,那么你这个订单的信息是不是基本上不会再变了。因为从业务上来讲,商家也不会再允许你超过一个月再去说做一些退单,或者说我下完单一个月不不付款,一个月后再付款,这种应该是不会出现,对吧?所以呢这种更新事实数据的更新应该是发生在什么较新的记录上。也就是说它的更新数据分布是有特点的啊,都是最近的。那么对于比较老的一些数据呃,它的更新就比较少了啊,因为这一笔订单,这笔交易早就关闭了啊,不会再变更了啊。

嗯总而言之呢,就这种场景,大部分的更新会发生在最新的几个时间分区上,而小部分呢在旧的分区啊,那这个是官方画的一个图啊,这是一个数据库啊,nosql的存储也可以啊。那么这边呢是根据什么分区呢?你看订单创建日期啊,比如说咱们到呃年月日这样子好。那更多的是在什么最近的这一段时间,那更早之前的这些数据基本上很少做一个变更啊。好了,对于这样的这种场景,我们适合用什么布隆索引?因为它可以帮我们去裁剪,因为它里面可以比如说范围裁剪,我举个举个例子啊,它可以通过min max对吧?呃,我取一个最小最小最大区间啊,命中了,那就在这个区间里面找就好了,对吧?我把小于最小的,大于最大的直接就裁剪掉了。好,就简单举个例子啊,那布隆过滤天生就是用来判断呃用来判断在与不在,是吧?非常高效。那如果我们生成了这个key,也就是我们回头指定生成的这个record key啊,大家注意咱们反复聊到key键这个概念就是指的record key啊,可以某种顺序排列。也就是说它是有序的那这样的话他就能够更多的去筛选,尽可能的筛选出来啊,就将不必要那些更多的就过滤掉了,那这样性能就更高了啊,那hudi会用所有文件的键域来构造区间数。键域就是键的范围,比如说从哪到哪啊键域啊,构造一个区间数,这样能够更高效啊。

总而言之,言而总之这一段话就是什么呢?适合用布隆布隆,可以做一个呃区间裁剪啊,更高效。嗯,那如果我们为了更加高效的使用布隆过滤器进行一个比对的话嗯。这边hudi缓存的什么输入记录,也就是说做了什么事的缓存。并且使用了自定义分区器和统计规律来解决什么数据的倾斜啊偏斜。有时候你看这边说布隆过滤器的伪证率,其实就是什么常说的假阳性率啊,过高会增加数据的打乱操作啊。

那这个时候呢也就是说如果假阳性率特别高的话,为什么假阳性率会高啊啊大家应该知道布隆它的准确率或者说误差率是不是呃通过一个公式可以算出来。那这个公式是不是跟很多呃因素有关系。第一,数据集的大小,也就是说你整个数据量有多少。第2个可能你给到了这个bit mp空间有多少。还有呢你这个系数,还有哈希函数的个数啊等等,这些都有关系。那么呃其中一点就是数据量对吧?啊,数据量越大,那么你这个假阳性率的可能性是不是就越大?那个公式忘了自己可以去研究一下啊,那这个时候我们就希望呃布隆过滤器的一些参数啊,一些一些变量能够动态的调整。可以啊,我们只需要什么设置这个参数就可以了。你看啊hudi布隆首先指定为布隆索引的过滤类型,指定为动态的就可以指定位动态。它可以根据咱们总的数据量来调整布隆相关的一些参数,而从而达到我们希望的那个假阳性率啊。对于叫假阳性会更好理解啊。这能理解吧?啊,或者既然说到这儿,我就简单给大家看一眼吧。呃因为有的人可能还不太熟啊,不太熟啊啊比如说布隆,你搜一下过滤器。呃,我看一下有没有原理的介绍啊,看一下10分钟理解啊,随便找一篇啊,我只想找到它的公式,原理我就不讲了啊,原理不讲了。这篇没有给我们那个数学公式啊。啊,随便点啊。呃,这个也算吧,但我想找一篇更全的。哎,你看啊呃,那我们看也就是说跟公式有关系啊。看这一篇。哎,详解布隆过滤器的原理,使用场景,注意试一下。我看一下这个的。好,那我们看一下啊,也就是说这些公式大家自己看一看就行了。它有我们的数据量啊,其中一个参数,数据量一个是K值,一个是M值,一个是bit map的大小。这一节能够计算出一个一个它的阳性率啊,或者说。
1
2
那这个时候我们就希望呃布隆过滤器的一些参数啊,一些一些变量能够动态的调整。可以啊,我们只需要什么设置这个参数就可以了hoodie.bloom.index.filter.type=DYNAMIC_V0。你看啊hudi布隆首先指定为布隆索引的过滤类型,指定为动态的,就可以指定为动态。它可以根据咱们总的数据量来调整布隆相关的一些参数,而从而达到我们希望的那个假阳性率。啊,或者既然说到这儿,我就简单给大家看一眼吧。呃因为有的人可能还不太熟啊,不太熟啊啊比如说布隆,你搜一下过滤器。呃,我看一下有没有原理的介绍啊,看一下10分钟理解啊,随便找一篇啊,我只想找到它的公式,原理我就不讲了啊,原理不讲了。这篇没有给我们那个数学公式啊。啊,随便点啊。呃,这个也算吧,但我想找一篇更全的。哎,你看啊呃,那我们看也就是说跟公式有关系啊。看这一篇。哎,详解布隆过滤器的原理、使用场景、注意事项。我看一下这个的。好,那我们看一下啊,也就是说这些公式大家自己看一看就行了。它有我们的数据量啊,其中一个参数,数据量一个是K值,一个是M值,一个是bit map的大小。这一节能够计算出一个一个它的阳性率啊,或者说准确率,或者也可以算为什么误报率。
这是第一个场景事实表啊,总结起来是什么?事实表这种场景呃更新主要集中在最近的日期,那么我们用布隆就可以了。
对事件表的去重
1
2
3
4
5
好,第二种场景是对于事件表的去重大家注意是事件啊,不是事实事件是什么呢?比如说前端买点产生的这些呃事件流。比如说呃点击流,用户的点击买点产生的数据,还有咱们物联网,比如说车联网一些传感器产生的这个数据啊,我们通常称为事件流嘛对吧?还有广告点击这些啊广告曝光。好,那么这一些呢我们通常会将这个数据先采集进kafka对吧,这种消息队列。那么而且这种的数据量一般呢是咱们业务数据库当中呃10到100倍,也就是说事件事件的数据往往是大于事实的数据,对吧?也就是说数据库里的数据跟我埋点产生的数据哪个更多呢?一般来讲是买点还有传感器产生的这种啊会更多一点。而且他有一个特点,他要不要更新呢?这种事件的数据一般而言都是追加啊,不存在更新。大部分而言啊,那即使有更新,那可能也是最近的几个。那这个时候呃涉及到一个数据重放啊,可能是一个采集的一致性这些问题可能有一个重复的问题。那这个时候去重也是一个很常见的一个需求吧,对吧?这个大家做过的话应该都很熟悉啊,不管你是什么业务场景。

那接下来呢就是咱们对于这个呃你想要低消耗的对大数据量进行去重,这个还是说还是比较难度的啊。要么你就是挺浪费资源,那其实如果咱们可以用间值存储来实现去重啊,或者说这个量比,说白了就一句话,量比较大怎么办?那可以如果你对hbase比较熟悉,并且呢你们公司当中,你们环境当中有人可以维护运维这个hbase,并且能调的用的比较好,那你可以考虑一下用hbase啊。因为它在呃底层的h base索引当中,它不是简单的将索引数据就存进去了。它也利用了h base像LSM tree这种特性啊,做了很多很多的一个处理跟优化啊,但是呢这个事件数据还是不断膨胀的,一直在膨胀。那即使您要去hbase代价也是比较大,不是说不行,是代价比较大。呃,但这种时候呢有范围裁剪功能的布隆,所以才是比较好的,你又可以裁剪过滤。那这个时候我们这个record key可以怎么做呢?可以用事件时间加上呃标记这一条事件的一个ID来组成这个键就好了event_ts+event_id。

那这样有什么特点?你看这个时间戳它是不是一个递增,哎,对吧?那这样的话就是一直在增长的一个单调递增的一个键。好,总而言之呢,如果是这种布隆也更合适啊。那么我们大家看完两个场景都发现哎布隆更合适啊,所以其实布隆就是默认的,默认就是用了布隆啊。
对维度表的随机更删
1
2
3
4
5
6
7
8
9
10
11
12
13
那么对维度表的随机更新和删除,什么叫维度表啊?像什么地区啊、时间呢呃渠道啊对吧?这一些就是我们所谓的维度。比如说你电商场景是不是还有呃商品维度对吧,是哪一个品类的啊等等这一些。包括什么用户维度表对吧?记录了用户信息的这些表啊。用户维度信息也就是说你在建模的时候肯定要去构建一些维度表。那维度表有个特点啊,它的更新和删除。我举个例子,一张用户表啊,比如说你有张用户表,呃,有123有3名用户啊。用户一是去年创建的啊注册的啊用户二是今年呃上半年注册的用户,三是现在注册的。那这个时候呢呃很久以前注册的一他有没有可能更改信息啊?很有可能吧,他可以更改他的呃绑定邮箱,更改他的绑定手机号,可以更改昵称,可以更改地址,是不是都可以改,对吧?也就是说你并不是说你的更新都集中在最近这一段时间,而是不一定啊,所以我们叫随机啊随机。

好,那这种场景的话你用来去裁剪就不合适了。因为比如说我有这么一个呃这么一个范围吧,我这个表示时间顺序,好吧,表示时间顺序。如果你是裁剪的话应该是什么呢?呃,筛选出一段,那在这一段里边再去匹配,这样效率高。那我现在维度表这种场景是哎我指不定在我不会集中在某一段进行那个更新和删除。我可能在这里可能在这里,那你就没法把我提前拆剪掉了。那这样的话你用布隆的话就不能够得到一个很好的效益啊,就性能可能就啊没有发挥不出来了。也就是说这个是随机写入。那我们的更新操作会触及表里大多数文件,大多数分区,对吧?啊,这时候布布隆就不太好使啊,即使使用了范围比较也还是检查所有文件啊,那这种场景怎么办呢?

呃,简单来讲,既然是可能所有文件都可能涉及到,那你就用简单索引就好了啊,就不要做呃不要做那些花里胡哨的招了啊不要给自己呃搬起石头砸自己的脚啊,没必要啊。他不因为呢简单索引他不会提前裁剪啊不提前裁剪,而直接是将所有文件跟所需字段连接啊,就是join嘛,我们前面聊聊过嘛,对吧?呃,但是这种场景你也可以考虑hbase啊,因为是你只要是对hbase能够拿得下,能hold得住,那样hbase也是可以的。而且hbase对于这种随机读写也是很擅长的,是不是?

好了,另外呢有一个小事情啊,小事情就是什么呢?呃,我们用全局索引的是时候啊,有可能涉及到分区路径需要更新,哎,你可能不好理解对吧?呃,如果你是按照年月日这种日期来作为分区字段,那一般这个分区产生就不会变了是吧?比如说你是2022年9月1号这个分区,那基本上这个分区值就不会变了。但是如果你用的不是日期呢?啊举个例子啊,如果你是按照城市来分区的一张用户表,对吧?比如说哎北京这个城市是一个分区,深圳是一个分区对吧?那北京比如说有用户ABC呃,深圳这个分区里面有用户D的信息啊,你是这么来做了对吧?那就有可能出现什么呢?用户A他离开了北京,他去了深圳。那这个时候。是不是对于A这条数据来讲,它的分区值是不是变了,它的分区信息是不是变了,对吧?那这个怎么办呢?

这个时候咱们可以开启这个参数,hoodie.bloom.index.update.partition.path=true或hoodie.simple.index.update.partition.path=true。那分下面还有一个参数,那其实这两个分别对应的一个是用了布隆,另外一个用的是简单索引。啊,你用的什么索引,你就设置哪个参数啊。那这个时候用MOR的表会更好啊,当然这个表后面再来聊啊,下面就会聊到啊,这是一个小事情啊,就是分区路径会更新的这种特殊场景啊,可以开启参数好了。

综上所述啊,我们简单聊下来就是什么呢?大部分场景事实表、事件表、事件流都可以用布隆就好了。另外呢如果hbase hold得住,那就用hbase。

第呃第二点呢,如果是随机的更新这种场景,那就是用简单索引啊或者用hbase。好了,这都是一个取舍,没有说谁绝对好,谁绝对差啊。也就是说更新频率,更新的范围比较集中,用bloom够更好,呃,更新比较分散,那就用简单索引或者hbase好了,就这样子。

表类型

COW表
1
2
3
前面聊了这么多概念,那么接下来这个概念是大家必须要了解的。就是表类型在hudi当中它有两种表,一种叫copy on write,简单来讲就是写实拷贝。另外一种表呢叫做merge on read,叫读实合并啊,那么大家记住啊,这是物体特有的两种表类型。

那我们先来看第一种类型叫COW啊,咱们简称COW啊,copy on copy on write. 那么它的特点是它的数据文件当中,它只有一个基本的列文件列存的,也就是这种所谓.parquet格式。他没有点log啊,这个特点大家要记住啊,因为点log是增量提交的时候才有的啊,这个时候没有。那么对于每一个新批次的写入,它都会创建相应的数据文件的新版本啊,这句话大家好好品一下。对于每一个新的批次,也就是说我写入一批新的数据了,那这个时候都会创建相应数据文件的新版本。哎,重点是什么?新版本新的一个文件片,也就是我举个例子啊,原先在一张COW的表中,它只有一个parquet文件。比如说啊。它里面比如说存储了id为一二三这么三条数据啊,那现在呢我们要去追加写一个四跟五这么一批数据啊,4,5这2条新的数据要写进来了,那这个时候他会做什么呢?呃,他就会将原先老的,也就原先的parquet文件,再加新增加的这些变更数据,将他们合并起来。然后呢就会变成一份新的12345啊,这样子能理解这个意思吧?

image-20240310152555403

1
这也是为什么叫写时拷贝,拷贝的是什么呢?拷贝的是原先的parquet文件,将原先parquet文件都拷过来,再将新增的部分及变化的部分合并进去,再写入一个新的文件。那新的文件它也是一个parquet。那这个时候呢对应我们前面聊到的一个概念叫什么呢?文件片。那这个一二三这个比如说它就是一个文件片啊,比如说是一吧,我们称之为一fileslice1。那么写示拷贝完啊合并完这个新的parquet文件,那可能我们就可以认为它是一个文件片fileslice2,它们都是在同一个文件组里面是吧?嗯,这也是为什么我们前面说到这个文件片,可以理解为呃不同文件片是不同的数据版本。好,这简单理解啊呃新版本文件包含旧的文件记录集这个批次的记录,那最新的这个文件片就是包含了全量最新的数据了。

image-20240310152919086

image-20240310153114556

1
2
3
4
5
好,那下面看一个具体的例子啊,其实大家基本理解了啊,那当前呢是给到3个file group。有三个文件组。那么目前呢他们的版本都是第一个版本啊第一个版本。好,那这个时候我们进行一个数据的写入,新的写入。那你看啊场景是这样,在索引之后我们发现这些记录与啊文件组一、文件组二匹配了。也就是说什么呢?啊,我有一些数据是要属于更新操作的啊,那这些数据对应的原先数据在文件组一和文件组二里面啊,另另外呢有一些数据它是属于新的插入啊,不是更新啊啊那个时候候我会将将的插入入写到一个新的文组组,也就第四个组。比如说啊好,所以就变成下图这样子,文件组一、文件组二有些数据需要更新啊,那这个时候他们就会对呃旧的呃V一版本的数据,还有更新的数据进行合并啊,并且呢写入一个新的parket,生成了一个V2版本。那文件组2同样的道理,它也生成了一个V2版本,新的文件片,新的parquet,那文件组三呢没有变化啊,那还有一些是新插入的数据啊,就写入一个新的文件组。文件组四好,知道这个应该都好理解吧。

那基于这个大家就能想到了,呃,它是属于写入期间进行合并的。也就是说我一批数据插入的啊,我既要拷贝老的数据,又要将老的数据,将新写新过来的数据进行一个合并啊,写入一个新的文件。所以呢它写入的延迟相对会大一点。但是COW是最早封底设计的一种呃表一种表,它的优势就是它非常的简单。

另外有一个特点需要大家记入,它并不需要进行compassion。大家注意啊,如果是COW的表,它并没有compassion这个动作。因为它每一次都写入的时候就进行合并,合并成一个新的parquet文件嘛。啊所以呢它COW表就没有必要进行这种压缩合并操作啊,也是用起来比较方便和简单。好,那么这个是我们对COW表的一个理解啊,应该没什么难度。
MOR表
1
接下来我们看第二种类型的表,merge on read, 也就是所谓的读实合并。那么大家注意它包含什么样的文件呢?第一,它可能有parquet文件,大家注意我的描述啊,可能有parquet文件。另外它一定会有一个基于行存的增量日志文件,也就是avro格式的。那么它具体的文件名呢,就会看到点log这么一个后缀啊,这个我们前面也是简单给大家看过,对吧?有一些点log。好,那为什么叫MOR呢?那是因为它的合并在读取端,什么意思呢?哎,你看我现在是不是既有这个基本的列存文件parquet。另外呢还有每一次呃新增加的数据,比如说插入或者更新的数据,它这一批数据会记录在一个点log文件,对吧?那再来一个新批次,又这些数据又有插入有更新,它可能又在一个新的点log文件。也就是说在这张MOR表当中,他是不是可能有parquet,又有多个log,那你读的时候该怎么读啊?因为老的parquet文件,它里面可能包含了一些是过期的数据,对吧?啊,比如说我们原先这里有一条1A这么一条数据,然后后面我对它进行了呃更新。那我是不是有一条数据,比如说A变成B啊变成一B啊,这是更新后的数据。那它过来它不会对原先parquet文件进行处理,而是什么呢?将这一条更新的数据放在点log文件,对吧?所以这个时候如果你只读这个parquet那可能是不准的。所以你要综合parque和点log文件啊,才能得到最新的结果。

image-20240310160209348

1
2
3
4
5
6
7
8
9
10
11
12
13
这个MOR表读的时候parquet也好,log也好,它都会一起在读的时候进行合并。这也是为什么叫读时合并啊,只有在我进行读取操作的时候,我才会将parquet跟log进行一个合并。好。所以它的合并在读取端啊,它而且呢它在写入的时候不会进行合并或者说创建新的数据文件,对吧?这个就区别于COW表,它是写入的时候进行合并生成新的文件啊,那MOR不会也也就相当于说是反过来了

当完成了标记索引之后,呃,对于具有要更新记录的现有数据文件,是创建增量日志,就我刚才说的.log文件啊,那就像你看这一个文件组里面啊有parquet也有log。那么大家要注意的一件事情是什么?一定有parquet的吗?不一定。如果我这张hudi表呃是第一次有数据,来,大家注意听我描述啊。这张表第一次有数据写入,也就第一批数据来,这个时候你没有parquet文件。第一批数据也是会追加到点log文件里面去啊,这个时候你就看不到PAS回来。就像我前面给大家看的时候,是不是也只有一个点log是吧?

啊,当然呢后面它呃对于MOR表有这个所谓的compassion啊,有不同的策略,不同的条件。当你满足这个合并的条件之后,或者执行合并的时候,就会将现有的parquet和log进行一个合并成一个新的parquet文件。啊,这是compassion会做的事儿啊。读取端将实时合并基本文件,也就是parquet及各自的增量日志文件。啊,好了,每次的读取延迟都比较高,因为啊我们要查询时才进行一个合并操作。

另外就是我刚才提到的压缩机制,也就是所谓的compassion。这个就是无论你读与否啊,有没有就是即使你没有读这个compassion,如果触发的话,它也是会进行啊文件的合并的啊,它会将数据文件parquet,日志文件点log合并在一起,会创建新的parquet。

那关于这个compassion呢呃我们可以选择内联方式或者异步模式来运行,而且他提供了不同的压缩策略。那其中最常见的是基于什么?提交的数量。就像我刚才讲的,我每经过三次提交,每一次提交都会MOR表。每一次提交是不是都写入一个点log文件了,对吧?啊,比如说我有三次提交,那就有三个点log文件。呃,那这个时候我指定数量为三就compassion,那它就会触发自己的compassion,生成一个新的parquet。那么压缩完成之后,读取端只要读取最新的数据文件,而不用关心什么旧版的文件。这样是不是呃这种compassion机制是不是可以减轻咱们读实合并的一个性能问题,对吧?如果你从来不做compassion,那我每一次读都必然要跟旧版的parquet,跟新版的,跟各种各多个log文件进行一个线合命啊,那效率就低低了啊,所以这个compission也是很重要的一个事儿啊。

呃那下面一些细节呢就是什么呢?MOR表的写入行为根据索引会有一些区别啊。如果我们用的是布隆索引,它是无法对点log这种文件生成索引的啊,所以这个时候它会怎么做呢?它会将插入的消息写入parquet,将更新的消息写入这个点log。大家注意这是针对布隆索引,为什么呢?因为它对avro这种啊log file不能生成索引。

那如果咱们用的是flink,就是现象是不一样,这个大家要明白。如果用的是flink它是基于状态的索引,那每次写入它都是log的一个格式,呃,并且会不断的追加。那这个时候它就不会说我in sert的消息写入parquet,update写入log不是这样啊,它统一都写入什么log,追加到log。好,这个是一些区别啊。那具体来讲就是回头咱们演示的时候啊啊,如果咱们用sport引擎默认的布伦过滤就可以是这样子啊。银色的跟UP对分开写,那么如果是flink那就统一都写入到log啊。好,这个就是MOR表。那么大家可以想一想对吧,这两种表什么样的场景呃,适合用什么表?
COW与MOR的对比

image-20240311132159848

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
那接下来我们就来对比一下这两种表的优缺点啊区别在哪里啊。那我们看一个从数据的延迟来讲谁更高啊。COW更高一点啊,MOR的表反而会更低一点啊,数据的延迟。

那么查询的延迟呢就是我查这张表啊需要多久时间。COW为什么呀?因为我要查的时候,它是不是他是在写入的时候就进行合并了。我查的时候它是不已经合并完了啊,简单来理解是这样,所以我查的时候是不是延迟更低一点是吧?呃,那么对于MOR来讲,你的查询延迟就高了。因为你是读取查询的时候才进行一个合并啊,那你肯定就相比COW要慢一点了啊啊对

啊,好行,另外一个是更新的成本,IO成本啊,我要对表的数据进行一个更新,谁的成本更高呢?呃,对于COW来讲它更高。因为我们知道每一批数据来,它是不是要把旧的拍回来,跟新的数据进行一个合并,写入一个新文件。那如果呃我有一张表,有一个盘毁的文件吧,比如说这里有一万行数据,那现在我对其中的某新来的数据是对某一行数据,比如说id为三的进行更新。那这个时候他是不是同样得拷贝原先的一万条数据,并且将新的数据跟老数据合并,再写入一个新文件。这个代价是不是有点大啊,能理解这个意思吧?啊,这个代价就大了啊,另外一个呢就是咱们的这个MOR表,他代价比较低。为什么他不会动原先的parquet对吧?先不考虑这个compassion,呃,我不会动,我只会将新来的这些数据插入也好,更新也好,我都追加到那个点log文件,回头等compassion或者等你查询的时候,我先合并啊

好行parguate文件的大小,呃,COW它的是更小更低,但是它更新的IO成本更高。MOR文件会更大一点啊,但是它更新成本比较低。

写放大啊,那COW不用讲,它肯定是一个写放大的一个问题,对吧?我写的时候哎我要拷贝旧数据啊,这代价就比较重了。那么MOR表呢它写会不会放大呢?就呃相对来讲是低一点。当然说了这么多,大家应该有一个初步的印象,但是理解不会很深入吧。

那咱们来个大白话啊,COW比较适合什么?批还有流计算这两种场景来考虑,是不是更适合于批呀?因为我们知道它有一个什么写放大的问题。如果我原先的parquet文件有一假设啊有一亿行,当然不会这么做啊,我们不会让它那么大有一亿行现在呃我来的数据这个批次只有一条是更新的数据,那你是不是要拷贝原先一亿行更新的这一条进行合并,代价有点重吧。但是如果我来的是一批数据,这里面包含了比如说100万条,那也就是说我做这么一次啊,那还行,对吧?比你一条一条的去啊效率会更好一点。所以呃建议就是COW表呢,咱们还是用在批处理的一个场景会更好一点。

那MOR表呢就是呃批也好,流也好,其实也都行。但是如果是流式场景,咱们更推荐MOR表。因为你想想什么叫流啊流处理啊,流处理是不是数据源源不断的来,是吧?那如果不做处理,咱们是不是数据是一条一条来的呀,一条一条写入啊,对不对?那也就是说他写入会特别特别的频繁,比如处理写入很频繁,每次量又很小。你用COW的话,这个写放大问题就很严重,特别严重啊,没必要。所以如果是流失场景,特别是你用CDC去同步一些数据过来,呃,咱们还是推荐用MOR表。当然不绝对啊不绝对啊。如果你对虽然是流处理,但是你做了一个展批的处理,那其实也还行啊。

好,这个是咱们啊主要一个对比啊,心中有数啊。也就是说COW偏向于P或者呃只要有涨批吧,不管是批处理还是流处理,有涨批的这种更好一点。呃,MOR呢更适合流式的写入啊,流式的写入。好,这个取舍呢各大企业就一些大厂他们在用的时候啊,呃流式场景也有用COW的,也有用MOR的啊,就是说嗯你看吧,这是我的一呃一些建议。

查询类型(Query Types)

Snapshot Queries

image-20240311145011015

1
2
3
Hooody呢提供了三种不同的查询类型,我们一起来了解一下。第一种呢是快照查询。什么叫快照呢?简单来解就大家记住就是四个字叫全量最新。哎,我就要查询当前呃数据最新的一个状态,全量最新就记住这四个字就行了,这就是快照查询。

那区别于两种表类型有什么特点呢?COW表你就是直接查询最新版本的parquet文件就可以了,这个好理解吧,因为最新的这个parquet文件肯定是全量最新,对不对啊?那MOR表就不一样了,呃,MOR表需要做一个什么?呃,就是说即使要去合并读的时候要去合并,合并什么呢?最新的文件片里面当中的基本文件,也就是parquet。呃,还有呢一些点log里面的增量数据,增量文件,它会将他们合读取的时候进行合并,合并的时候展示给你啊,所以呢呃提供一个近实时的表,因为它合并还需要一些延迟嘛,是吧?所以MOR表你去查询的时候有一个读取延迟啊,就在因为它作为一个现合并啊,行,这个是快照查询。

image-20240311141444608

1
2
3
下面这张图简单搂一眼啊(COW快照查询),这什么意思呢?呃,大家注意看这边有四个什么呢文件组,这是file ID啊。它这个字体可能容易看错啊,这是file ID然后这是file ID1234表示四个文件组啊。另外呢就是呃这个是COW表为例啊,那我们看啊他在十点钟的时候啊,用这个淡蓝色表示进行了一次commit。那这个时候可能插入的呃只有两个文件组,文件组一还有文件组2,你看他们有蓝色的对吧?好,再之后呢在绿色的这个时刻,也就10点05分,呃,他又有一批新的数据commit了。那这个时候呢可能涉及到什么?呃,文件组一、文件组二里面数据的更新,还有呢一批数据的插入啊,也就是文件组三了啊,所以大家可以看到一二呢这里有更新,然后三呢是新插入。好,那么粉色的这个就10点10分的时候再一次commit一批数据,这个时候可能涉及到文件组一、文件组2的更新,对吧?啊,所以呢啊一二又更新了,然后呢又有一批数据新的插入啊,在文件组4。哎,在所以呢这个是一个不是一个动图啊,所以大家会看。那完事之后呢,我们可以看到你在10点05分的时候去查询它的全量最新快照是什么?你看在05分的时候,你进行一个快照查询,全量最新是什么?是不是文件组1二三里面的这三个parquet文件呢?对吧?啊,所以你看有文件组1二三里面呢,他们的版本都是5分的时候,这个时候文件组四还没有生成呢,还没有呢,能理解这个意思吧?

呃,另外呢在我们10点10分commit之后,这个时候你再去做一个快照查询询能达到什个效果呢?呃,首先文件组一、文件组二是不是都有一个最新的parquet文件,是吧?一二里都能查到。那四呢也是在10分的时候新插入的,新生成的也能看到。关键就在于三呢。对于三来讲,你可能觉得哎10分的commit跟他没关系是吧?虽然你没有数据的变更怎么样呢?没无所谓,但你现在是最新状态是什么,你也展示出来呗啊,所以你这里能看到什么呢?文件组三它的版本还是停留在10点05分,因为这就是它的最新版本了,能理解这个意思吧?这就是这张图的意思啊,慢慢去解读。
Incremental Queries

image-20240311144942588

1
2
3
看增量查询,它可以查询给定的提交commit或者增量提交delta commit。这个提交就对应咱们那个COW表的方式啊,delta commit就是对应MOR表的这种方式啊,呃给定了这个某一次commit以来新写入的数据,对吧?就比如说刚才这个例子,如果现在时间已经到了,经过了10点10分的commit,呃,你可以怎么样呢?你可以指定说哎我从10点05分之后或者从十点钟之后以来先提交,而不用去查询这个commit之前的一些数据的啊。

这个时候我们而且还可以开启一个变更流啊变更流来启用增量的数据管道。还记得咱们前面聊到护底的一个使用场景,是不是有一个增量管道啊,对吧?那也可以依赖于它的增量查询啊,说白了就是过滤一个某个commit之后新写入的数据啊,其他就不要。也就是说它不是全量最新啊,不是全量执新,而是指定commit之后新增加的这一部分数据。
Read Optimized Queries

image-20240311144927506

image-20240311150233853

1
2
3
还有一个叫读优化查询。大家看描述的话是commit还有compassion的最新快照。有的人就有疑问了,那这个跟快照查询不就一样吗?哎,大家注意咱们这边描述可不一样。普通的commit对应的是COW表啊,所以对于COW表来讲,快照查询跟读优化是一样的。但是对于MOR表来讲就不一样。这个delta commit,这个compassion也好,都是对于MOR而言的,对不对?读式合并,那delta commit呃就是增量提交。那我们知道呃增量提交完,然后你再去查这个快照查询一定是全量最新。但是compassion就不一定了。因为每次compassion之后,它是不是将老的parquet跟多个log进行一个合并,生成一个新的什么新的parquet是吧?那这个新生成的这个parquet一定是全量最新吗?不一定啊,为什么呢?我可能在compassion之后又进行了好几次的增量提交,又多了好几个.Log文件能理解吧?如果是读优化视图,它的区别在哪?它只会查询最新compparsion之后的这个parquet文件。也就是说在这一次parquet后面的这一些点log它查不到了啊,所以对于MOR来讲,它并不是全量最新能理解吧?

好吧,那看下一句话,他仅将最新的文件片,最新文件片嘛的什么呢列文件暴露给查询。这就我刚才讲了,如果是MOR表,我在某一次compassion之后,它是生成了一个新的parquet。但在这之后呢,它又多次提交,又有又有新的点log写过来了。这个时候你只查到parquet log你没有查到没有合并啊啊,这个是要注意的地方,这个是读优化啊。

image-20240311150847465

Read Optimized Queries是对Merge On Read表类型快照查询的优化
1
2
3
4
5
6
7
8
好,那下面有一个具体的对比啊,下面这张图就是对于MOR表而言,它的快照查询与读优化的一个对比。那么在呃从10点01分开始,差不多每1分钟进行了一次增量提交是吧?那么在10点05分的时候触发了一次compassion,大家注意compassion,那触发compassion之后有什么特点呢?就是会生成一个新的parquet文件,哎,那对应的是哪一个呢?哎,文件组一、文件组2、文件组3。在10点05分的时候,compassion之后生成的基本列文件啊用绿色表示。好,那我们接着往下看,在06分、07080910的时候分别进行的都是什么?增量提交增量提交每一次提交是不是生成一个点log文件,对吧?啊,所以你看对于文件组一来讲,它包含了呃06的点log、08点log、09点log。那对于文件组二呢,它有07的、有09的、有10的点log对吧?那对于文件组四三啊,压缩之后他没有新的数据来。文件组四一开始是没有的,但是在06分和10分的时候都是呃插入的数据嘛,相当于是对吧插入的新插入的数据。好,那这一块呢它就有一个点log,一个点log能理解吧?

好,那接下来就是什么呢?呃,我们在10分的时候进行查询,这个是什么?读优化的方式。你去查呃按照10点10分这个这一次commit为基准的话,你能查到什么呢?读优化视图,咱们说了他只查parquet文件,也就是说绿色的这三个能查到,所以你看是123,并且这是05分的时候,compassion那文件组四的这几个点log它就查不到了。查不到。
那如果是快照查询,它所有的都能查到啊,因为它是全量最新,能理解吧?这个图想表达的就是这么一个意思。

好,那这个读优化视图是专门针对MOR表的一个快照查询的优化。为什么要这么做呀?刚才也讲了,我读的不一定是全量最新,那既然如此还要他干嘛啊?其实就是为了查询效率。你想想如果我是都是快照查询MOR表,每一次查询我都要进行一个读时合并,代价还是有点重的,我的读取延迟还是比较大的那如果这个时候我能接受呃稍微的滞后,也就是说即使不是全量最新,那也是相对来讲比较新的吧。退而求其次嘛,但是我的效率能有一个质的提升啊,主要的目的在这里啊。

那你看呃数据延迟呢是比较低啊,那读优化呢是比较高,查询延迟呢快照比较高,因为它要现合并啊,那读优化呢比较低,它直接读part ket啊,他不读log。好,所谓的数据延迟这里怎么理解呢?也就是说我现在10点10分的数据已经写入了,对吧?比如说就就刚才这个地方呃,10点10分一次提交啊,往文件组4写入了点log文件,但是我却查不到。我现在10分钟去查,我查不到,那不就是有延迟吗?我可能在等下一次compassion这一些数据才可见,对吧?下一次compassion之后,这些呃合并之后这里才可见啊,这个就是所谓的数据延迟嘛啊不是立马就能查到嘛。

image-20240311151920329

不同表支持的查询类型
1
2
3
那么看看不同的表支持的查询类型啊。其实对于COW表来讲呢呃就简单的两种。一个是全量最新的快照,还有增量两种查询。

对于MOR表就除了这两个多了一个读优化

image-20240311153517398