本附录描述了 HFile 格式的演变。
由于我们将讨论对 HFile 格式的更改,因此简要概述原始(HFile 版本 1)格式。
版本 1 格式的 HFile 结构如下:
版本 1 中的块索引非常简单。对于每个条目,它包含:
偏移(长)
未压缩的大小(int)
键(使用 Bytes.writeByteArray 编写的序列化字节数组)
密钥长度为可变长度整数(VInt)
关键字节
块索引中的条目数存储在固定文件尾部中,并且必须传递给读取块索引的方法。版本 1 中块索引的一个限制是它不提供块的压缩大小,这对于解压缩来说是必要的。因此,HFile 读取器必须从块之间的偏移差异推断出该压缩大小。我们在版本 2 中修复了这个限制,我们存储了磁盘块大小而不是未压缩的大小,并从块头获得未压缩的大小。
注意:此功能是在 HBase 0.92 中引入的
我们发现有必要在遇到高内存使用率和由区域服务器中的大 Bloom 过滤器和块索引引起的慢启动时间后修改 HFile 格式。 Bloom 过滤器每个 HFile 可以达到 100 MB,在 20 个区域聚合时可以增加 2 GB。在同一组区域中,块索引的总大小可以增加到 6 GB。在加载所有块索引数据之前,不会将区域视为已打开。 Large Bloom 过滤器会产生不同的性能问题:第一个需要 Bloom 过滤器查找的 get 请求将导致加载整个 Bloom 过滤器位阵列的延迟。
为了加速区域服务器启动,我们打破 Bloom 过滤器并将索引阻塞到多个块中,并在它们填满时写出这些块,这也减少了 HFile writer 的内存占用。在 Bloom 过滤器的情况下,“填充块”意味着累积足够的密钥以有效地利用固定大小的位阵列,并且在块索引的情况下,我们累积所需大小的“索引块”。布隆过滤器块和索引块(我们称之为“内联块”)散布在数据块中,作为副作用,我们不再依赖块偏移之间的差异来确定数据块长度,就像在版本 1 中完成的那样。
HFile 是一种低级文件格式,它不应该处理特定于应用程序的细节,例如在 StoreFile 级别处理的 Bloom 过滤器。因此,我们在 HFile“内联”块中调用 Bloom 过滤器块。我们还为 HFile 提供了一个用于编写这些内联块的接口。
旨在减少区域服务器启动时间的另一种格式修改是使用连续的“加载打开”部分,该部分必须在打开 HFile 时加载到存储器中。目前,当 HFile 打开时,会有单独的搜索操作来读取预告片,数据/元索引和文件信息。要读取布隆过滤器,对其“数据”和“元”部分还有两个搜索操作。在版本 2 中,我们寻求一次阅读预告片并再次寻找从连续块中打开文件所需的其他内容。
引入上述功能的 HBase 版本同时读取版本 1 和 2 HFiles,但仅写入版本 2 HFiles。版本 2 HFile 的结构如下:
在版本 2 中,数据部分中的每个块都包含以下字段:
8 字节:块类型,相当于版本 1 的“魔术记录”的字节序列。支持的块类型是:
数据 - 数据块
LEAF_INDEX - 多级块索引中的叶级索引块
BLOOM_CHUNK - Bloom 过滤器块
META - 元块(不再用于版本 2 中的 Bloom 过滤器)
INTERMEDIATE_INDEX - 多级块索引中的中间级索引块
ROOT_INDEX - 多级块索引中的根级索引块
FILE_INFO - “文件信息”块,元数据的小键值映射
BLOOM_META - 加载时打开部分中的 Bloom 过滤器元数据块
TRAILER - 固定大小的文件预告片。与上述相反,这不是 HFile v2 块,而是固定大小(对于每个 HFile 版本)数据结构
INDEX_V1 - 此块类型仅用于传统的 HFile v1 块
块数据的压缩大小,不包括头(int)。
扫描 HFile 数据时可用于跳过当前数据块。
块的数据未压缩大小,不包括头(int)
如果压缩算法为 NONE,则等于压缩大小
相同类型的前一个块的文件偏移量(长)
可用于寻找以前的数据/索引块
压缩数据(如果压缩算法为 NONE,则为未压缩数据)。
以上格式的块用于以下 HFile 部分:
扫描块部分
该部分之所以如此命名是因为它包含了顺序扫描 HFile 时需要读取的所有数据块。还包含 Leaf 索引块和 Bloom 块块。
非扫描块部分
此部分仍包含统一格式的 v2 块,但在执行顺序扫描时不必读取它。本节包含“元”块和中级索引块。
我们在版本 2 中支持“meta”块的方式与版本 1 中支持的方式相同,即使我们不再将 Bloom 过滤器数据存储在这些块中。
HFile 版本 2 中有三种类型的块索引,以两种不同的格式(root 和 non-root)存储:
数据索引 - 版本 2 多级块索引,包括:
版本 2 根索引,存储在文件的数据块索引部分
(可选)版本 2 中间级别,以非根格式存储在文件的数据索引部分中。如果存在叶级块,则只能存在中间级别
(可选)版本 2 叶级别,以非根格式存储,与数据块内联
元索引 - 仅版本 2 根索引格式,存储在文件的元索引部分中
Bloom 索引 - 仅版本 2 根索引格式,作为 Bloom 过滤器元数据的一部分存储在''load-on-open'部分中。
此格式适用于:
版本 2 数据索引的根级别
版本 2 中的整个 meta 和 Bloom 索引始终是单级的。
版本 2 根索引块是以下格式的条目序列,类似于版本 1 块索引的条目,但存储磁盘大小而不是未压缩大小。
Offset (long)
该偏移可以指向数据块或更深层索引块。
磁盘大小(int)
键(使用 Bytes.writeByteArray 存储的序列化字节数组)
钥匙(VInt)
Key bytes
单级版本 2 块索引仅包含单个根索引块。要读取版本 2 的根索引块,需要知道条目数。对于数据索引和元索引,条目数存储在预告片中,对于 Bloom 索引,它存储在复合布隆过滤器元数据中。
对于多级块索引,除了上面描述的数据结构之外,我们还将以下字段存储在 HFile 的 load-on-open 部分的根索引块中:
中叶索引块偏移
中间叶块磁盘大小(表示包含对文件“中间”数据块的引用的叶索引块)
中间叶级块中的中间键(在下面定义)的索引。
这些附加字段用于有效地检索 HFile 分裂中使用的 HFile 的中间密钥,我们将其定义为具有零基索引(n-1)/ 2 的块的第一个密钥,如果总数为 HFile 中的块是 n。此定义与 HFile 版本 1 中确定中间密钥的方式一致,并且通常是合理的,因为块的平均大小可能相同,但我们没有对单个键/值对大小的任何估计。
在编写版本 2 HFile 时,每个叶级索引块指向的数据块总数将被跟踪。当我们完成写入并确定叶级块的总数时,很清楚哪个叶级块包含中键,并且计算上面列出的字段。当读取 HFile 并请求中键时,我们检索中间叶索引块(可能来自块缓存)并从该叶块内的适当位置获取中键值。
此格式适用于版本 2 多级数据块索引的中间级别和叶索引块。每个非根索引块的结构如下。
numEntries:条目数(int)。
entryOffsets:块中条目偏移的“二级索引”,便于快速二进制搜索键(numEntries + 1
int 值)。最后一个值是此索引块中所有条目的总长度。例如,在条目大小为 60,80,50 的非根索引块中,“二级索引”将包含以下 int 数组:{0, 60, 140, 190}
。
参赛作品。每个条目包含:
文件中此条目引用的块的偏移量(长整数)
引用块的磁盘大小(int)
键。长度可以从 entryOffsets 计算。
与版本 1 相比,在版本 2 中,HFile Bloom 过滤器元数据存储在 HFile 的 open-on-open 部分中,以便快速启动。
复合布隆过滤器。
Bloom 过滤器版本= 3(int)。曾经有一个 DynamicByteBloomFilter 类,其 Bloom 过滤器版本号为 2
所有复合 Bloom 过滤器块的总字节大小(长)
散列函数数(int)
哈希函数的类型(int)
插入 Bloom 过滤器的总密钥数(长)
布隆过滤器中的最大键数(长)
块数(int)
用于 Bloom 过滤器键的比较器类,使用 Bytes.writeByteArray 存储的 UTF> 8 编码字符串
版本 2 根块索引格式中的 Bloom 块索引
文件信息块是从字节数组到字节数组的序列化映射,包括以下键。 StoreFile 级逻辑为此添加了更多密钥。
| hfile.LASTKEY |文件的最后一个键(字节数组)| | hfile.AVG_KEY_LEN |文件中的平均密钥长度(int)| | hfile.AVG_VALUE_LEN |文件(int)|中的平均值长度
在版本 2 中,我们没有更改文件格式,但是我们将文件信息移动到文件的最后一部分,可以在打开 HFile 时将其作为一个块加载。
此外,我们不再将比较器存储在版本 2 文件信息中。相反,我们将其存储在固定文件预告片中。这是因为我们需要在解析 HFile 的 open-on-open 部分时知道比较器。
下表显示了版本 1 和版本 2 中固定文件预告片之间的常见字段和不同字段。请注意,预告片的大小因版本而异,因此仅在一个版本中“固定”。但是,版本始终存储为文件中的最后四个字节整数。
版本 1 | 版本 2 |
---|---|
文件信息偏移(长) | |
数据索引偏移量(长) | loadOnOpenOffset(long)/打开文件时需要加载的部分的偏移量./ |
数据索引条目数(int) | |
metaIndexOffset(long)/版本 1 读者不使用此字段,因此我们将其从版本 2 中删除。 | uncompressedDataIndexSize(long)/整个数据块索引的未压缩总大小,包括根级别,中级别和叶级别块./ |
元索引条目数(int) | |
未压缩字节总数(长) | |
numEntries(int) | numEntries(长) |
压缩编解码器:0 = LZO,1 = GZ,2 = NONE(int) | Compression codec: 0 = LZO, 1 = GZ, 2 = NONE (int) |
数据块索引中的级别数(int) | |
firstDataBlockOffset(long)/第一个数据块的偏移量。扫描时使用./ | |
lastDataBlockEnd(long)/最后一个键/值数据块之后的第一个字节的偏移量。扫描时我们不需要超出此偏移量./ | |
版本:1(int) | 版本:2(int) |
注意:此优化是在 HBase 0.95+中引入的
HFiles 包含许多包含一系列已排序单元格的块。每个单元都有一个键。为了在读取 Cells 时保存 IO,HFile 还有一个索引,它将 Cell 的开始键映射到特定块开头的偏移量。在此优化之前,HBase 将使用每个数据块中第一个单元的键作为索引键。
在 HBASE-7845 中,我们生成一个新的键,其按字典顺序大于前一个块的最后一个键,并按字典顺序等于或小于当前块的起始键。虽然实际的密钥可能很长,但这个“假密钥”或“虚拟密钥”可以短得多。例如,如果前一个块的停止键是“快速棕色狐狸”,则当前块的开始键是“谁”,我们可以在我们的 hfile 索引中使用“r”作为我们的虚拟键。
这有两个好处:
拥有较短的密钥会减少 hfile 索引的大小,(允许我们在内存中保留更多的索引),以及
当目标密钥位于“虚拟密钥”和目标块中第一个元素的密钥之间时,使用更靠近前一个块的结束键的东西允许我们避免可能的额外 IO。
此优化(由 getShortMidpointKey 方法实现)的灵感来自 LevelDB 的 ByteWiseComparatorImpl :: FindShortestSeparator()和 FindShortSuccessor()。
注意:此功能是在 HBase 0.98 中引入的
HFile 的第 3 版进行了更改,以简化静态加密和单元级元数据的加密管理(这又是单元级 ACL 和单元级可见性标签所必需的)。有关更多信息,请参阅 hbase.encryption.server , hbase.tags , hbase.accesscontrol.configuration 和 hbase.visibility.labels 。
引入上述功能的 HBase 版本在版本 1,2 和 3 中读取 HFile,但仅写入版本 3 HFile。版本 3 HFile 的结构与版本 2 HFile 相同。有关更多信息,请参阅 hfilev2.overview 。
版本 3 将另外两条信息添加到文件信息块中的保留键。
| hfile.MAX_TAGS_LEN |存储此 hfile(int)中任何单个单元格的序列化标记所需的最大字节数| hfile.TAGS_COMPRESSED |该 hfile 的块编码器是否压缩标签? (布尔)。仅当 hfile.MAX_TAGS_LEN 也存在时才应存在。 |
在读取版本 3 HFile 时,MAX_TAGS_LEN
的存在用于确定如何对数据块内的单元进行反序列化。因此,消费者必须在读取任何数据块之前读取文件的信息块。
在编写版本 3 HFile 时,HBase 在将 memstore 刷新到底层文件系统时将始终包含[COD0]。
压缩现有文件时,如果所选的所有文件本身不包含任何带标记的单元格,则默认编写器将省略MAX_TAGS_LEN
。
有关压缩文件选择算法的详细信息,请参见压缩。
内的 HFILE,HBase 的细胞被存储在数据块作为键值来的序列(见 hfilev1.overview 或拉斯乔治的很好的介绍 HBase 的寄存)。在版本 3 中,这些 KeyValue 可选地包含一组 0 个或更多标记:
版本 1& 2,版本 3 没有 MAX_TAGS_LEN | 版本 3,MAX_TAGS_LEN |
---|---|
密钥长度(4 个字节) | |
值长度(4 个字节) | |
关键字节(变量) | |
值字节(变量) | |
标签长度(2 个字节) | |
标签字节(变量) |
如果给定 HFile 的 info 块包含MAX_TAGS_LEN
的条目,则每个单元格将包含该单元格标签的长度,即使该长度为零。实际标签存储为标签长度(2 个字节),标签类型(1 个字节),标签字节(变量)的序列。单个标记的字节格式取决于标记类型。
请注意,对 info 块内容的依赖意味着在读取任何数据块之前,必须首先处理文件的 info 块。它还意味着在写入数据块之前,您必须知道文件的信息块是否包含MAX_TAGS_LEN
。
使用 HFile 版本 3 编写的固定文件预告片始终使用协议缓冲区进行序列化。此外,它还为名为 encryption_key 的版本 2 协议缓冲区添加了一个可选字段。如果 HBase 配置为加密 HFile,则此字段将存储此特定 HFile 的数据加密密钥,使用 AES 使用当前群集主密钥加密。有关更多信息,请参阅 hbase.encryption.server 。