一文读懂 OceanBase 数据库的启动恢复代码解析
作者简介:镜水,一个致力于无限进步的数据库学徒。
本文主要介绍 OceanBase 数据库启动时是如何将已持久化的日志和数据恢复到内存,重新形成各类信息(如租户信息、分区信息等)的内存映像,从而回到宕机前的状态。
在介绍具体的恢复流程之前,我们首先来了解一些与之相关的存储结构。
存储数据结构
MacroBlock
OceanBase 数据库将数据分为增量数据和基线数据,基线数据是几乎占满整个磁盘的一个超大文件,OceanBase 数据库以固定大小的宏块(MacroBlock,默认大小为 2 MB)为单位对磁盘上的基线数据进行管理,从而优化每日合并过程并高效利用磁盘空间,这类似于操作系统中内存的页式管理。宏块按照其在磁盘中的位置进行逻辑编号,起始编号为 0。
枚举类型标识了宏块有哪些种类:
enum MacroBlockType {
Free = 0, // 空闲宏块
SSTableData = 1, // 数据宏块,实际存放用户数据
PartitionMeta = 2, // 分区元数据宏块,通常以宏块链表的方式进行存储
// SchemaData = 3,
// Compressor = 4,
MacroMeta = 5, // 宏块元数据宏块,通常以宏块链表的方式进行存储
Reserved = 6,
MacroBlockSecondIndex = 7, // deprecated
SortTempData = 8,
LobData = 9,
LobIndex = 10,
TableMgrMeta = 11, // 新版本已废弃
TenantConfigMeta = 12, // 租户配置元数据宏块,通常以宏块链表的方式进行存储
BloomFilterData = 13,
MaxMacroType,
};
除此之外,类似于文件系统,宏块还有一种称为 的特殊种类。 存放了整个基线数据的关键信息,比如元数据入口点和日志回放点,固定为第 0 块宏块,通常还有若干个备份块。
除了 以外每个宏块都有一个 。
struct ObMacroBlockCommonHeader {
int32_t header_size_; // struct size
int32_t version_; // header version
int32_t magic_; // magic number
// each bits of the lower 8 bits represents:
// is_free, sstable meta, tablet meta, compressor name, macro block meta, 0, 0, 0
int32_t attr_; // MacroBlockType
union {
uint64_t data_version_; // sstable macro block: major version(48 bits) + minor version(16 bits)
/**
* For meta block, it is organized as a link block. The next block has a index point to
* its previous block. The entry block index is store in upper layer meta (super block)
* 0 or -1 means no previous block
*/
int64_t previous_block_index_;
};
union {
int64_t reserved_;
struct {
int32_t payload_size_; // not include the size of common header
int32_t payload_checksum_; // crc64 of payload
};
};
};
NOTE: 为前向指针,通常为链式结构的元数据宏块,它表明从新到旧的一个前向关系。例如,下表所示宏块链表的写入顺序为 12345,但由于前向链接,读取时的入口点为 5,从而导致读取顺序为 54321,因此实际使用数据时,通常需要一次正向(54321)读取获取 的数组后,再进行一次反向(12345)读取,从而得到正确的数据写入顺序。

在了解了数据块的基本单元 后,我们接着来介绍 以及 。
由于历史原因,目前对于以及的组织方式有新旧两个版本之分,下面我们将这两个版本分开介绍。
旧版本:
旧版本的 和所有的 都在同一个大文件(如果对 observer 的执行目录有所了解的话,这个大文件可以理解为 sstable 目录下的 )上。
可以简单理解为所有持久化数据的普遍元数据,其中包含了各种 的入口块(即宏块链表第一块)以及 SLog 的回放入口点(SLog 文件中的日志经过 checkpoint 后成为 ,这里的回放入口点即 SLog 中尚未经过 checkpoint 形成 meta 的日志偏移位置,如果将 看作 meta 的基线数据,那么SLog 需要回放的日志可以理解为 meta 的增量数据),一般是前两个 。
struct ObSuperBlockHeader {
static const int64_t OB_MAX_SUPER_BLOCK_SIZE = 64 * 1024;
int32_t super_block_size_; // not used any more
int32_t version_;
int32_t magic_; // magic number
int32_t attr_; // reserved, set 0
};
struct ObSuperBlockV2 {
struct MetaEntry {
static const int64_t META_ENTRY_VERSION = 1;
int64_t block_index_; // first entry meta macro block id
int64_t log_seq_; // replay log seq
int64_t file_id_; // ofs file id
int64_t file_size_; // ofs file size
};
struct SuperBlockContent {
static const int64_t SUPER_BLOCK_CONTENT_VERSION = 2;
int64_t create_timestamp_; // create timestamp
int64_t modify_timestamp_; // last modified timestamp
int64_t macro_block_size_;
int64_t total_macro_block_count_;
int64_t free_macro_block_count_;
int64_t total_file_size_;
// entry of macro block meta blocks,
common::ObLogCursor replay_start_point_; // SLog 回放入口点
MetaEntry macro_block_meta_; // macro block 元数据回放入口点
MetaEntry partition_meta_; // partition 元数据回放入口点
MetaEntry table_mgr_meta_; // table mgr 元数据回放入口点
MetaEntry tenant_config_meta_; // tenant config 元数据回放入口点
};
ObSuperBlockHeader header_;
SuperBlockContent content_;
}
由于 是以前向链表的形式连接的,每个 除了 以外还包含一个 :
struct ObLinkedMacroBlockHeader {
int32_t header_size_;
int32_t version_;
int32_t magic_;
int32_t attr_; // MacroBlockType
int64_t meta_data_offset_; // data offset base on current header
int64_t meta_data_count_; // 当前 block 有多少个元数据 item
// 如果是 0,表示该 item 超出 2 M,那么多个 block 构成多个完整 item
// 否则,1 个 block 包含多个完整 item
int64_t previous_block_index_; // last link block
int64_t total_previous_count_; // 之前所有 block 所包含的元数据 item 总数
int64_t user_data1_; // log_seq_num,not used
int64_t user_data2_; // 如果多个 block 组成多个 item,该值表示这多个 block 所包含的多个 item 的总数据序列化长度
}
宏块元数据保存了数据宏块中数据列排布、微块索引等重要元数据,从磁盘加载后常驻内存并在内存中维护。
所有宏块元数据(,也就是 item)由多个 组成,每个 block 可能有多个 item(完整的,由于一个 item 不会跨多个 block,可能存在 padding 空间),每个 item 代表了 从 0 开始的每一个 的元数据,也就是说每个 都有一个 。
每个 item 是 结构,解析出来的每个 item 在内存里存放在一个类似于 hash table 的结构里,通过 block id 进行 hash,解析代码见 。
表管理元数据保存了 SSTable 的相关元信息。
由多个组成,可能每个 block 有多个 item(完整的,这种情况下block可能存在 padding 空间),也可能多个 block 组成多个 item(比如,3个 block 组成 4 个 item,那么前两个 block 的 为 0,最后一个 block 的 为 4,第一个 block 的 代表了这 4 个 item 的总序列化大小,第二和第三个 bloc k的 则为 0),而一次对多个 item 的读取(一个 block,或者多个 block)实际对应着一条 log 记录,是同一个 。
每个 item 是 结构,包含了一个 sstable 的信息,解析代码见 。解析一条 log 中的多个 item 用的是 。
每个 有一个 :
struct TableKey {
ObITable::TableType table_type_;
common::ObPartitionKey pkey_;
uint64_t table_id_; // combine bits // include tenant_id (24 bits)
common::ObVersionRange trans_version_range_;
common::ObVersion version_; // only used for major merge
common::ObLogTsRange log_ts_range_;
}
分区元数据由多个 组成,可能每个 block 有多个 item(完整的),也可能多个 block 组成多个 item(组成方式和 相同),一次对多个 item 的读取同样对应着同一条 log 记录,解析一条 log 中的多个 item 用的是 。
每个 item 是 结构,包含了 partition 的全部基线信息,包括 sstable 信息。
解析代码见 ,解析后每个 partition 的 sstable 会存放在其自身的 。
NOTE:PartitionGroup(简称 PG)和 Partition 的概念可能会增加代码阅读复杂度,可以简单理解一个 PG 里面只有一个 Partition,只有特殊情况 PG 才会包含多个 Partition,看代码时可以简单把 PG 看作 Partition。
租户配置元数据由多个 组成,每个 block 可能有多个 item(完整的),每个 item是一个容纳了所有租户配置项的数组(结构)。
每个item在载入时,会原来的配置项数组,相当于覆盖更新。
解析代码见 。
新版本:
旧版本 和所有的 都在同一个文件上,没有为不同的 做区分。而新版本则不一样,每个 有单独的 和 ,其中每个 的 (第二级 )是一个 (第一级 meta)item,和第一级 在同一个文件,而每个 的 在 其单独的文件中,文件 id 以及元数据的入口点保存在 自己的 中。
简单来说,通过第一级 能够找到第一级 meta()的入口点,而通过第一级 meta 能够找到每个 对应的单独文件以及对应的二级 ,然后通过二级 上的入口点能够得到每个 的单独文件 id 以及文件上的二级 。
这是第一级 ,针对所有的 。
struct ObSuperBlockMetaEntry {
blocksstable::MacroBlockId macro_block_id_; // first entry meta macro block id
};
struct ObSuperBlockHeaderV2 {
int32_t version_;
int32_t magic_; // magic number
};
struct ObServerSuperBlock {
struct ServerSuperBlockContent {
static const int64_t SUPER_BLOCK_CONTENT_VERSION = 1;
int64_t create_timestamp_; // create timestamp
int64_t modify_timestamp_; // last modified timestamp
int64_t macro_block_size_;
int64_t total_macro_block_count_;
int64_t total_file_size_;
common::ObLogCursor replay_start_point_; // SLog 回放入口点
ObSuperBlockMetaEntry super_block_meta_; // tenant spuer block 元数据回放入口点
ObSuperBlockMetaEntry tenant_config_meta_; // tenant config 元数据回放入口点
};
ObSuperBlockHeaderV2 header_;
ServerSuperBlockContent content_;
};
除了 以外还包含一个 :
struct ObLinkedMacroBlockHeaderV2 {
int32_t header_size_;
int32_t version_;
int32_t magic_;
int32_t attr_;
int32_t item_count_; // 该 block 有多少个 item
// 如果是 0,表示该 item 超出 2M,那么多个 block 构成多个完整 item
// 否则,1 个block 包含多个完整 item
int32_t fragment_offset_;
// record previous macro block's MacroBlockId // 分别是 MacroBlockId 的四个部分
int64_t previous_block_first_id_;
int64_t previous_block_second_id_;
int64_t previous_block_third_id_;
int64_t previous_block_fourth_id_;
}
新版本的每个 meta item 都包含一个 :
struct ObPGMetaItemHeader {
int16_t type_;
int16_t reserved_;
int32_t size_; // item 的数据长度,不带 ObPGMetaItemHeader 的长度
};
新版本租户配置元数据和旧版本含义一致,解析代码见。
每个 item 是 结构,每个 item 也是调用 载入。
这是第一级 meta,由多个 构成,可能每个 block 有多个i tem(完整的),也可能多个 block 组成多个 item。
新版本和旧版本对 item 的拆分方式不太一样,旧版本的拆分方式可以见 的介绍,新版本则为拆分封装了更规范的函数( 等),通过 来判断一个 block 是否包含完整的 item,不再通过 来判断多个 item 的总长度(该成员新版本已不存在),而是通过 来 判断每个 item 的长度,如果一个 item 跨多个 block,则根据 item 的长度和每个block的有效数据长度()来拼接得到整个 item 的有效数据。
解析代码见 ,每个 item 由依次获取,每个 item 是 结构,包含了一个 , 包含了一个 单独文件的 id 以及第二级 ,由此可以得到每个 对应的单独文件以及二级元数据的入口点。
struct ObTenantFileKey {
uint64_t tenant_id_;
int64_t file_id_; // 每个 tenant 单独文件的文件 id, 二级 meta block 存储在该文件上
};
struct ObTenantFileSuperBlock {
blocksstable::ObSuperBlockMetaEntry macro_meta_entry_; // 二级 meta, macro block meta
blocksstable::ObSuperBlockMetaEntry pg_meta_entry_; // 二级 meta, pg meta
ObTenantFileStatus status_;
bool is_sys_table_file_;
};
struct ObTenantFileInfo {
ObTenantFileKey tenant_key_;
ObTenantFileSuperBlock tenant_file_super_block_; // 一级 meta, 每个 tenant 单独的 super block
PG_MAP pg_map_;
};
宏块元数据是第二级 meta,针对的是某个 ,由多个 组成,每个 block 可能有多个item(完整的),每个 item 是 结构,其中包含的 不再是旧版本的 结构,而是 。
解析出来的所有 item 存放在一个 hashmap 里,key 是 结构,解析代码见 。
struct ObPGMacroBlockMetaCheckpointEntry {
int64_t disk_no_;
blocksstable::MacroBlockId macro_block_id_;
ObITable::TableKey table_key_;
blocksstable::ObMacroBlockMetaV2& meta_;
};
struct ObMacroBlockKey {
// 引入 table key 可以在检查时判断该 sstable 是否还存在,不存在那么 macro block 自然也失效了
ObITable::TableKey table_key_;
// 这里的 block id 是每个 sstable 里宏块的逻辑 id,对于每一个 sstable 都是从头开始
blocksstable::MacroBlockId macro_block_id_;
};
分区组元数据是第二级 meta,针对的是某个 ,但和旧版本的 含义基本一致,依然由多个 组成,可能每个 block 有多个 item(完整的),也可能多个 block 组成多个 item,解析代码见 。
每个 item 是 结构,包含了 partition 的全部基线信息,包括 sstable 信息。每个 item 载入内存的过程依然是调用 。
SLog
SLog是meta的增量数据,相关介绍可以阅读 SLog结构介绍 文档,这里我们简单介绍几种具体的SLog日志类型。
该日志对应于旧版本的,新版本不再使用。
只有一种日志类型:
:某个macro block的元数据发生了变更。每个log entry是结构,包含block_id和,意味着整个新元数据都在log里。
struct ObMacroBlockMetaLogEntry {
int64_t data_file_id_;
int64_t disk_no_;
int64_t block_index_;
ObMacroBlockMeta& meta_ptr_;
};
该日志对应于旧版本的 ,新版本不再使用。
包含三种日志:、 和 。
:不支持。
:增加了一个 sstable。每个 log entry 是 结构,包含了 table_key 和 。
struct ObCompleteSSTableLogEntry {
ObOldSSTable& sstable_;
};
:删除了一个 sstable。每个 log entry 是 结构,包含了 table_key。
struct ObDeleteSSTableLogEntry {
ObITable::TableKey table_key_;
};
该日志新旧版本都使用。
只有一种日志类型:。
:租户配置发生变更。每个 log entry 是 结构,包含 ,replay 时会将原有 重置并全部更新,意味着即使只有一个租户配置变化,log 里还是保存了所有租户的配置。
struct ObUpdateTenantConfigLogEntry {
share::TenantUnits& units_;
};
该日志新旧版本都使用,并且旧版本的部分日志类型迁移到了该日志下。
由于日志种类比较多,这里只介绍部分日志类型,感兴趣的同学可以进一步阅读代码()。
:增加 partition 或者 pg。每个 log entry 是 结构,只包含了/ 等标识信息。
struct ObChangePartitionLogEntry {
common::ObPartitionKey partition_key_;
common::ObReplicaType replica_type_;
common::ObPGKey pg_key_;
uint64_t log_id_;
};
:某 partition 增加了 sstable。每个 log entry 是 结构,包含 和 。replay 时会将 添加到 里的 。
struct ObAddSSTableLogEntry {
common::ObPGKey pg_key_;
ObSSTable& sstable_;
};
:某个 macro block 的元数据发生了变更。该日志与旧版本的 类型日志一致。每个 log entry 是 结构,包含 block id 和 。
struct ObPGMacroBlockMetaLogEntry {
ObPGKey pg_key_;
ObITable::TableKey table_key_;
int64_t data_file_id_;
int64_t disk_no_;
blocksstable::MacroBlockId macro_block_id_;
blocksstable::ObMacroBlockMetaV2& meta_;
};
恢复流程
启动恢复过程的调用栈如下:
ObStoreFile::open(bool)
>> ObLocalFileSystem::start() // 开源上是ObLocalFileSystem,可能还有其他FileSystem
>>> ObPartitionService::start()
>>>> ObServer::start()
>>>>> main(int, char **)
主要分为三个流程:
1.加载元数据的快照点,即将分区和 sstable 的信息还原进内存
2.回放 slog,将分区和 sstable 的信息更新到最新状态
3.从分区的元信息中获取 clog 的回放位点,开始回放 clog 日志生成 memory table
其中前两步调用 完成,这里只介绍这一过程,建议参照代码阅读。
:
1.根据 的版本走不同的分支
2.v2 版本(旧版本),先读出 ,然后调用
3.v3 版本(新版本),直接调用
:
1.首先 读取所有的 (///),并使用对应元数据的类解析函数 出相应的信息恢复到内存(比如 能够解析出每个分区的基础信息以及包含的 sstable 信息,将以某种结构保存到内存),然后将所有 的 存到同一个数组,进而在内存维护每个 meta block 的引用状态。
2.接着 恢复 SLog,首先根据 中的 位置从 SLog 读取每一条 log,记录 begin 和 commit(),然后 进行日志回放,同样从 位置开始读 SLog,log 的 trans id 如果是已经 commited 的(上一步记录了),那么 redo 该日志,即调用该 log type 对应的 replay 函数。
:
1.首先读出 (第一级 )
2.从 获取 SLog 回放入口点 ,从该位置读取每一条 log,记录 begin 和 commit()
3. 读取所有 的 (这些 相当于第一级 ,第二级的 ,和 在同一个文件上,入口点在 上 ,也是多个 前向链接而成):首先 从 入口点读取所有的(),并反向预取出第一个 的数据保存在内存,接着将反向读取每一个 ;然后依次解析每个 meta item,将每个和其对应的file以map的形式保存在内存()
4.读取所有 :首先 从 入口点读取所有的 (),并反向预取出第一个 的数据保存在内存,接着将反向读取每一个 ;然后依次解析每个 meta item ,不断重新覆盖 ()
5.回放第一级 meta(tenant 的 和)的 SLog,调用 进行日志回放,从位置开始读 SLog,log 的 trans id 如果是已经 commit 的(第 2 步记录了),那么 redo,调用相应的 replay 函数(只会对上述两种 meta 的 SLog 触发 replay)
6. 根据第 3 步得到的 map 依次回放每个 的二级元数据,每个 的回放调用:首先从 的 能够得到 ,从而获得 和 的 block 入口点,然后首先通过 读取 并保存在一个从 得到的 中,接着通过 读取pg meta,每个item包含一个 pg 的信息,使用 将其解析并载入到内存
7. 回放第二级 meta(每个 tenant 的 以及 )的 SLog,调用 进行日志回放,从 位置开始读 SLog,log 的 trans id 如果是已经 commit 的(第 2 步记录了),那么 redo,调用相应的 replay 函数(只会对上述两种 meta 的 SLog 触发 replay)
8.最后 将所有 的 存到同一个数组,进而在内存维护每个 meta block 的引用状态。
回顾
在了解了宏块、各种宏块类型、SLog 以及从 Meta 和 SLog 恢复内存数据的过程后,将会更容易理解整个 OBServer 的存储结构。整个存储结构从上到下大致可以分为这样几层:

如果您有任何疑问,可以通过以下方式与我们进行交流:
微信群:扫码添加小助手,将拉你进群哟~

钉钉群:33254054
