Bootstrap

一文读懂 OceanBase 数据库的启动恢复代码解析

作者简介:镜水,一个致力于无限进步的数据库学徒。

作者简介:海芊,一个致力于当网红的 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

欢迎大家一起参与社区贡献,指南请参考看 

社区答疑:请点击