共计 4946 个字符,预计需要花费 13 分钟才能阅读完成。
作者:闻乃松
- MinMax
每个 Iceberg 文件的头部元数据信息中记录了以后文件每个列的最大最小值,比方下图中的 parquet 文件数据记录蕴含两列:year 和 uid,file1.parquet 中列 year 的最大和最小值别离是 2019 和 2018,列 uid 的最大和最小值别离是 23000 和 12000。
当咱们进行查问
select * from event where year=2019 and uid=20000
因为这些元数据信息在数据写入文件时最终收集,因而在查问时候,很容易利用这些统计信息过滤掉不符合条件的数据文件。比方示例中,依据 year=2020 和 uid=20000 查问到符合条件的数据文件为 file1.parquet,另外两个数据文件间接过滤掉了,缩小了不必须的文件读取。
为了获取更好的过滤成果,MinMax 通常进行全局排序,然而适宜排序字段较少的状况,比方 1 个字段,当排序字段多于 1 个,根据索引的最左匹配规定,只有查问字段笼罩所有所有索引字段能力取得最好的查问成果,否则过滤成果将大打折扣,为此须要更适合的索引,比方 Z -Order。
- Z-Order
因为 MinMax 索引包含多个字段时,不能保证数据的汇集性,而利用 Z -Order 索引可能取得比 MinMax 均匀更好的数据汇集性。Z-Order 原理是把人造没有有序性的多维数据以某种形式映射成一维数据进行比拟。映射后的一维数据,可能保障各个原始维度依照同种水平去保障其汇集性。如下图所示:
对 X,Y 这两个维度进行比特位的穿插组值,造成了 Interleave Index 进而得出一个新的值,这个值被称作 Z -Value。从图中,能够看到针对 X,Y 这两个字段的数据,生成的 z -value 会呈现出一个 Z 形嵌套。依照这样的一个构造,在依照 Z -Value 排序时,可能同时保障 X,Y 两个字段的汇集性。
实现 Z -Order 的一个前提是须要保证数据以保序的形式映射成一个正整型,但参加排序的字段类型很多,如 String、Long、DateTime、Double 等,如何将这些不同类型的值映射为正整型就是个问题。实际中,尽管能够将 String 类型取固定的前几位字符转为二进制来进行映射,但也带来了信息损失。另外,即便是正整型数据,因为其数据分布不同,可能导致映射的后果不合乎 Z -Order 曲线的嵌套散布。比方,X 的取值是 0,1,2,3,4,5,6,7,Y 的取值是 8,16,24,32 这种,计算出来的 z -value 排序成果实际上和数据依照 order by y,x 的成果是一样的。也就是说这种排序并没有带来额定的益处,对于 X 的汇集性无奈保障。
为了获取更好的过滤成果,Z-Order 也须要进行全局排序,然而 Z -Order 排序字段越多,排序成果也会越差。倡议 2 - 4 个。
- Bloom Flilter
Bloom Flilter 是一种空间节约型的概率数据结构,通过一个长度为 M 的位数组来存储元素,能够增加元素但不能删除元素。每个元素应用 k 个哈希函数生成 k 个数值,大小位于区间 [0,数组长度 -1] 中,增加元素到 Bloom Flilter 时,将相应地位置为 1。当查问是否存在相应元素时,只须要判断 k 个地位的值是否全为 1,如果不全是 1,阐明不存在该元素,如果全是 1,则不肯定阐明存在该元素。k 是一个远小于 m 的常数,m 是跟增加到 Bloom Flilter 的元素个数成正比。两者的具体取值由 Bloom Flilter 的误判(false positive)比例决定。比方下图示例中,Bloom Flilter 长度 m 为 11,哈希函数个数 k 为 2,增加两个元素 A 和 B,当初查问元素 A、C、D,因为元素 A 哈希映射的两个地位都为 1,且确实是 A 的哈希映射后果,所以能检索到 A 存在。元素 C 因为不满足所有哈希地位都为 1,所以能够判定 C 不存在(True Negative)。然而因为 D 的哈希映射地位并非是 D 的哈希映射后果,即便其对应的哈希地位都为 1,也不能判定 D 的存在,这对 D 来讲,就是误判(false positive)。
Bloom Filter 利用大量哈希位来存储和定位元素,无论存储还是查问,其工夫复杂度都是常量级:O(k),只跟哈希函数的个数无关。其代价是有肯定的抵触概率,数组长度同增加的元素数量成正比,当数组长度越长,哈希函数越多,抵触概率越小,反之,抵触概率越高。因而,在应用中,须要确定数组长度和抵触概率。Bloom Flilter 应用内存保护,且不存储元素自身,相比其余数据结构,能取得较高的空间和查问效率。毛病是只能确定元素是否存在,不能确定元素的具体位置,且不反对范畴查问和删除操作。
- BitMap
位图索引 (bitmap indices) 是一种专为多个键的简略查问而设计的。bitmap 索引将每个被索引的列的值作为 KEY,应用每个 BIT 示意一行,当这行中蕴含这个值时,设置为 1,否则设置为 0。利用位图索引的前提是记录必须被按程序编号,个别从 0 开始。给出编号 n,必须可能很容易的找到对应的记录,如果记录被寄存在间断的块,能够将编号 n 转换成块编号 + 块内偏移的示意以疾速定位记录地位。
位图索引用一个位来对应一条记录,这便是记录须要被编号的起因。instructor_info 表如上图,性别的值有男、女两种,支出等级则划分为 5 级,既有 5 种值。在给性别属性建设位图索引时,就会别离为 male 和 female 建设,对于 male 位图来说,如果一条记录的性别为 male,则位图上对应的位会置 1,female、支出等级位图也采纳雷同的做法。
位图索引的劣势体现在依据多个键的查问的时候,比方查问:
where gender=’f’and income_level=’L2′
只需将 gender=’f’的位图索引和 income_level=’L2’ 的位图索引取位与运算即可:
除此之外,范畴查问也是进行数据统计时候常见操作,基于位图索引的位或运算很容易实现范畴查问,比方上面的查问:
where gender=’f’and income_level>=’L2’
咱们只须要将 income_level=’L1’ 和 income_level=’L2’ 的位图索引位或运算,而后再跟 gender=’f’的位图索引位与运算即可:
从下面示例中可见,bitmap 索引就是用位图示意的索引,对列的每个键值建设一个位图。所以绝对于 b -tree 索引,占用的存储空间十分小,创立和应用十分快。相比 BloomFilter 索引,bitmap 索引不仅反对等值过滤,还反对范畴过滤。通过良好编码的位图索引,还可能取得比 BloomFilter 索引更少的存储空间和更精准的匹配。但 bitmap 索引应用也有限度,比方适宜建在值反复度高的列上,倡议在 100 到 100,000 之间,如:职业、地市等。反复度过高则比照其余类型索引没有显著劣势;反复度过低,则空间效率和性能会大大降低。对于常常更新的列,也不适宜应用 bitmap 索引。
- 小文件合并与去重
流式数据入湖随同着大量小文件的产生,依据文件产生的更新形式分为可追加的形式和非追加的形式两种:
可追加的形式:以只读事件日志的形式写入,如 IoT 事件
非追加的形式:以可更新的形式写入,如 CDC 事件
在高频的流解决场景,每天都可能产生成千盈百的新文件,基于 Flink 实时计算引擎,事务提交的距离越短,产生的文件大小越小,数量越多。在有些数据湖零碎的实现中,即便没有可提交的数据,也可能会生成空文件(但存在文件元数据)。对于非追加的形式,有两种解决形式:
-COW(Copy-on-Write):每次更新事件,会以该事件所在文件为正本,创立新的文件,最新的数据由以后最新的正本文件数据组成。COW 会导致”写放大“和并发提交抵触问题,适宜于写少读多的场景。
-MOR(Merge-on-Read):每次创立增量更新的文件,最新的数据是由前一次提交的快照数据和以后增量更新数据组成。MOR 会导致查问效率低,适宜于写多读少的场景。MOR 通常实现为一个持续性地合并并提交增量更新的后盾过程。
以后三大数据湖技术中,Delta Lake 和 Iceberg 仅反对 Copy-on-Write,因而它们不适宜写负载重的场合,而 Hudi 同时反对 Copy-on-Write 和 Merge-on-Read。Iceberg 实现了 一种 Copy-on-Write 变体:每个快照只存储增量数据,最新全量数据由最新的快照通过援用的 Manifest 蕴含的所有数据文件组成。为了尽可能保障写入的效率,Iceberg 将非追加的事件转换为可追加的 Insert 事件和 Delete 事件,别离存储在不同类型的数据文件中,这实际上带来了一些的开销:
每条更新事件都会波及到两个文件(追加数据文件和删除数据文件),相比可追加存储的形式,多了一份数据文件
因为删除数据文件中须要记录事件在追加数据文件中的地位,须要依据事件查找所在的原始数据文件,在没有数据索引的状况下,该过程会全数据集搜寻
这些文件随后被写入对象存储系统,如 AWS S3、阿里云 OSS 等,而后再通过查问引擎,如 Athena、Trino 等查问数据。大量的小文件将重大拖慢零碎响应速度,因为读取每个文件,零碎都要实现由上面三个根本步骤组成的动作:
- 关上文件
- 查找元数据
- 敞开文件
尽管关上一个文件可能只须要数毫秒的工夫,然而当文件数量规模足够大,这个工夫开销就会达到无法忍受的水平。当应用云上对象存储服务时,思考到拜访频次限度和调用估算,读取大量的文件有是无奈承受的。解决小文件的形式就是通过压缩,将小文件合并成大文件,这是目前做无效的形式,通过这种形式让计算引擎破费、更多工夫在读取数据内容,而不是将工夫花在频繁地关上文件和敞开文件下面。实现文件合并常见的形式是定时启动 Spark 或者 Hadoop 离线作业合并小文件,该过程通常较长,具体时长视合并文件的大小而定。企业应用通常启动独立的集群运行,比方基于自建集群服务或者 EMR 云服务。对 Iceberg 来说,读取一个文件的数据,首先要从快照中获取 Manifenst 文件,而后从 Manifenst 获取数据文件,最初从数据文件中读取数据,而 Iceberg 的数据文件分为一般数据文件和删除数据文件两种类型,删除数据文件存储的删除形式有基于文件门路(position-delete)删除和基于等值删除(equality-delete)两种,前者实用于在以后快照周期内重复减少和删除雷同主键记录的场景,后者实用于跨快照周期,防止扫描历史数据的场景。特地是基于等值删除的场景,因为 Iceberg 短少主键索引,为了防止更新记录去查问历史数据带来的写入开销,间接在删除文件中记录等值删除,这尽管带来了更新较快的劣势,但给前期的数据合并带来了大坑:在合并文件时,不可避免的对所有数据记录查问是否存在删除的状况,一旦数据集过大,很容易引起合并效率低甚至内存溢出的危险。咱们通过流程图来看数据合并的过程:
如图所示,合并数据文件时候,首先获取待合并的数据文件列表,而后迭代读取每个数据文件并将相关联的删除数据文件的数据加载到内存中的删除数据汇合(delete set)。迭代读取数据文件的过程,相似数据库中一个大表和一个小表进行 join 的过程,大表是流式表,小表是构建表,遍历数据文件的每一条数据时,在删除文件数据汇合中查看是否存在,如果存在,以后数据记录就不会被保留。要晓得每个数据文件可能关联多个删除数据文件,这些文件都是压缩存储的,比方 parquet 或 avro,一旦删除数据集加载到内存,只有当数据文件迭代完结之后才会开释,因而很可能导致内存溢出。另外,以后的 Iceberg 实现只合并了一般数据文件,对删除数据文件并没有合并,在更新数据频繁的状况下,删除数据文件数量也很可观,不得不对其合并,这个也是在实践中不得不思考的问题。
这里介绍了在合并文件过程常见的问题,实际上还有很多细节问题须要思考,这里简略汇总下,做个小结:
- 确定何时进行文件合并,思考因素能够是文件数量、文件大小。如果存在分区,还要思考到分区的变更。
- 为节约存储空间和费用,确保删除未合并的文件。
- 尽可能调大合并文件的大小,同时解压后内存可能容得下。
- 尽可能防止文件合并作业和流作业提交时的锁争用和抵触。