
OceanBase 源码解读(九):存储层代码解读之「宏块存储格式」
此前,带你读源码第八篇《事务日志的提交和回放》,为大家介绍了日志模块的设计理念和日志的一生。本期“源码解读”由数据库技术专家公祺为大家带来“存储层代码解读之「宏块存储格式」”。
“宏块”是处于 SSTable 和微块之间的数据结构,OceanBase 中的宏块为 2MB 的定长数据块。
众所周知,OceanBase 中微块是读 IO 最小单元,这是因为微块读处在用户请求的关键路径上,为保证快速响应用户的请求,微块不能过大,所以微块的默认大小一般不超过 16KB;而宏块作为写 IO 的最小单元,它的读写不在用户请求的关键路径上,所以就有了 2MB 的宏块,目的是为了最大限度的发挥磁盘的吞吐性能,能快速的做压缩、迁移复制、坏块检查等操作。
宏块的简单结构可以参考下图,详细的宏块格式介绍见下一节:
* 注:本文所有的说明及代码都是基于 v3.1.0_CE_BP1 版本的 OceanBase 开源代码。
宏块的格式
目前 OceanBase 支持的宏块有很多种,具体可以 enum MacroBlockType 的定义,总共有十几种,但是常用的数据宏块主要有三种,如下:
● SSTableData:常规的存放数据的宏块;
● LobData:Large Object Data,用来存放数据较大的行数据;
● BloomFilterData:带有 bloomfilter 的宏块。
本文主要介绍第 1 种常规的数据宏块,关于 LobData、BloomFilterData,后面找时间再单独说明。
一般来说,宏块的整体格式是一种比较经典的存储结构(header + payload + trailer + padding):
● header 中记录元数据:对应 OceanBase 宏块的 header ;
● payload 存放的是具体的数据:OceanBase 宏块的 payload 为微块列表;
● trailer 中记录的是数据的 index:OceanBase 宏块的 trailer 为微块的 index 信息,即为微块在宏块中的偏移量;
● padding 是为了做对齐的:OceanBase 宏块为2MB,不足部分需要做 padding。
后面我们将针对不同的部分,一一介绍其结构的存储格式。
一、宏块的 header
宏块的头部记录的自然就是宏块的元数据,它由多个部分组成,如下图所示:
宏块头部的各个部分存储的是不同的元数据,具体含义如下:
● common header:宏块的版本、类型、大小、checksum 等信息,见 ObMacroBlockCommonHeader ;
● macro block header:记录了宏块数据大小、table_id、partition_id、微块的数量、列数、行数、checksum、加密信息、以及相关 offset 信息,具体可以参考 struct ● ObSSTableMacroBlockHeader;
● column id list:列的 id 列表,OceanBase 数据库表每一列都一个唯一 id;
● column type list:每列的类型信息,包括:类型、编码字符集等;
● column order list:每列的顺序,可以是 ASC 或 DESC ,宏块中所有微块中的行数据都是按照这个顺序存储;
● column checksum list:每列数据的 checksum 信息,用来做列的数据校验。
微块头部的存储格式可以参考下面的代码:
宏块的头部结构的设计具有这些特点:
第一,简洁高效的序列化(和反序列化)实现:header 大小基本上是固定的,仅依赖列的个数,换句话说只要固定列数,这个宏块的 header 大小就固定了,这给内存分配和序列化带来了很大的便利;
第二,有很好的扩展性:主要体现在使用了version、宏块类型、预留字段等方面;
第三,不同纬度的数据校验:有字节级别的 payload_checksum_,也有业务级别的 column_checksum。
二、宏块的 payload
宏块的 payload 就是多个微块的数据,点击文末“阅读原文”可以查看微块的存储格式的详细介绍,当然也可以参考下面的代码来看微块的格式,本文不再做详细说明。
三、宏块的 trailer
宏块的 trailer 记录的主要是每个微块的 index 信息,但是其实不只是微块的 index 信息,包含了这些信息:
● 微块在宏块中 offset 数组:偏移量的个数为微块数+1,前后两个 offset 的差值是前一个微块的长度;
● 每个微块的最大的 rowkey 信息( endkey ),包括:endkey 的偏移量和 endkey 的数据。
另外,如果是多版本的宏块,trailer 中还包括了两个和多版本相关的信息:
● can_mark_deletion:用来标记这个微块是否可以标记删除;
● delta:用来记录微块中真正有效的行数,不包括被标记删除的行数。
为什么要单独记录微块的 index 信息(offset、length、endkey),最主要的原因就是能快速检索指定 rowkey 所在的微块,并能快速的将微块单独读出来,而不需要读取整个宏块。
宏块的 trailer 代码如下:
最终宏块的 trailer 会在 ObMacroBlock::flush 中序列化,序列化的实现可以参看 ObMacroBlock::build_index;
有了 trailer 中微块的 index ,那么就有两种方式来读取宏块中的各个微块数据:
● 顺序读取:主要用在压缩等场景,顺序读取各个微块数据,进行合并、迁移等处理;
● 随机读取:主要用在处理用户请求时,根据 rowkey 快速读取对应的微块数据。
四、宏块的 padding
2MB 的 OceanBase 宏块由于多种原因导致并不能写满,比如:数据量不够,以及特意预留的 10% 的空间(用于后续的 insert 等,避免过多的宏块分裂)等,这个时候就需要做 padding 补齐 2MB。本质上 padding 是空间浪费,但是为了性能以及简化设计,padding 还是有必要的。
没有 padding 的宏块应该如何做,不外乎有两种:
● 使用不固定大小的宏块:这时我们需要记录宏块的 offset 、length 等元信息,对宏块的定位就需要多一个操作,性能会有一定的损失,同时也会提高设计的复杂度;
● 每个宏块都满载 2MB:仅仅一个 insert 操作,可能会导致满载的宏块分裂成两个。
OceanBase 宏块 padding 并不是显式的实现,每个宏块大小2MB是固定的,header 中记录了宏块真正数据的大小,其余的都是 padding,OceanBase 并没有对 padding 部分的数据进行补零等操作。
宏块的操作
底层对数据块的读写主要靠继承 class ObStorageFile 来实现,如下代码是该类对外的接口说明:
一、宏块的写
OceanBase 主要在压缩、数据迁移复制等情况下才会有涉及宏块的写入操作,用户发起的写入操作会直接写 WAL ,不会直接触发宏块的写入操作。宏块和写相关的基础操作都在 class ObMacroBlock 中实现,主要的对外接口如下:
class ObMacroBlock 仅实现了一些宏块的基础写接口,关于 SSTable 的多个宏块的按序写入是靠 class ObMacroBlockWriter 实现的,具体见下面的代码说明:
在 class ObMacroBlockWriter 之上,又封装了一层 class ObMacroBlockBuilder 专门用来做合并,关于这个类的实现,本文先不做过多说明,后续会在与“合并”有关的博文中详细介绍,敬请关注后续推送。
二、宏块的读
对于用户请求,一般不会直接读取整个宏块,而是先读宏块的 index,再根据请求的过滤条件,最后精准的将某个微块读到内存,关于微块的精确读取,可以参考 struct ObMicroBlockDataHandle 的代码,搜索一下上下的调用链路,即可了解其逻辑。OceanBase 中,在多种情况下,也会做完整宏块的读取,包括:
● 在做 compaction 时,会使用 ObMicroBlockIterator ,读取完整的宏块数据,可以参考 class ObMicroBlockIterator 代码,关于合并的逻辑,后续会有专门的博文来介绍;
● 在做数据迁移的时候也会涉及到完整宏块的读取,详细逻辑可以参考 class ObMigratePrepareTask 的实现;
● ObMacroBlockWriter 将宏块数据写成功后,会根据 MICRO_BLOCK_MERGE_VERIFY_LEVEL 的情况,有可能见宏块数据读出来做检查;
● 在异步构建 bloomfilter 的时候,也会将宏块数据按序读出来,可以参考代码:ObBloomFilterBuildTask::build_bloom_filter、class ObSSTableRowWholeScanner;
● 在做坏块检查时,也会有读取全部宏块的情况,具体逻辑参考下面的代码说明:
综上,完整宏块的读取主要发生在后台异步任务中,这是由于相比于 16KB 的微块,读取 2MB 宏块的开销较大,在处理用户的请求时,一般都是指定微块进行读取。
三、宏块的申请和释放
OceanBase 中的基线数据存放在一个预分配好的大文件(ob_dir/store/sstable/block_file)中,里面的大部分区域存放的是 2MB 的宏块,通过元数据可以区分出有效的宏块数组和未使用的宏块数组,宏块的申请和释放就是基于这两个数组来做的。
具体的代码可以先参考这两个函数:ObStoreFile::alloc_block、ObStoreFile::free_block。后续会在“宏块 GC 原理”的博文中详细介绍这部分逻辑。
宏块存储格式的 demo
下面是一个真实的宏块 demo,参考本文,大家能更好地理解宏块的存储格式:
相信大家阅读了 OceanBase 有关微块、宏块存储格式的源码,会对 OceanBase 这样的设计初衷有更深的理解:大体来说,微块是为了更低的延时,宏块是为了更大的吞吐,分别应用在不同的场景上。
后续我们会继续解读 OceanBase 存储层的相关代码,与大家一起学习交流存储技术,敬请关注!
文章转载自公众号:OceanBase
