OceanBase源码解读:存储引擎Tablet单元 ob_tablet.cpp

Tablet 是 OceanBase 存储引擎的核心管理单元,位于 Log Stream(LS)之下、SSTable 与 Memtable 之上。一个 Tablet 对应一张表某个分区的全部数据,管理该分区的存储元数据、SSTable 列表、Memtable 句柄以及宏块引用计数。ob_tablet.cpp 共 9278 行,是整个 storage/tablet 目录下体量最大的文件,涵盖了 Tablet 从创建、迁移、合并、序列化到引用计数管理的完整生命周期。本文聚焦其中 8 个核心函数,揭示 Tablet 的架构设计精髓。

核心数据结构

ObTablet 类的关键成员分为三大类:元数据、存储层地址包装、运行时缓存。

  • tablet_meta_(ObTabletMeta):存储 tablet_id、ls_id、SCN、快照版本、兼容模式、分裂信息、上报状态等元信息
  • table_store_addr_(MetaObjAddr<ObTabletTableStore>):表存储地址包装,指向 SSTable 列表 + Memtable
  • storage_schema_addr_(MetaObjAddr<ObStorageSchema>):存储层 Schema 地址
  • macro_info_addr_(MetaObjAddr<ObTabletMacroInfo>):V3+ 引入的宏块聚合信息地址
  • table_store_cache_(ObTabletTableStoreCache):热数据缓存,直接持有 major/minor SSTable 指针
  • pointer_hdl_ / log_handler_:与 LS 的关联句柄和日志句柄

设计上,Tablet 使用”地址包装 + 延迟加载”模式:大对象(table_store、storage_schema)不直接内嵌在 Tablet 内存中,而是通过 MetaDiskAddr 记录其在宏块中的位置,需要时才 IO 加载。这保证了 Tablet 元数据本身保持紧凑(约 2KB),可以快速序列化和缓存。

关键流程

1. 首次创建:init_for_first_time_creation

建表或建分区时首次创建 Tablet,从零构建全部存储元数据。这是 Tablet 生命周期中最重要的入口之一:

int ObTablet::init_for_first_time_creation(
    common::ObArenaAllocator &allocator,
    const share::ObLSID &ls_id,
    const common::ObTabletID &tablet_id,
    const share::SCN &create_scn,
    const int64_t snapshot_version,
    const ObCreateTabletSchema &create_tablet_schema,
    const bool need_create_empty_major_sstable,
    ...)
{
  // 1. 参数校验
  // 2. init_shared_params — 设置 ls_id / tablet_id / compat_mode
  // 3. tablet_meta_.init — 初始化元数据
  // 4. pull_memtables — 从 LS 拉取 memtable handle
  // 5. 分配并初始化 storage_schema
  // 6. 可选:create_empty_sstable — 为新表创建空 major SSTable
  // 7. ALLOC_AND_INIT table_store — 构建表存储
  // 8. table_store_cache_.init — 初始化热缓存
  // 9. build_read_info — 构建行键读取信息
  // 10. init_aggregated_info — 构建宏块聚合信息(V3+)
  // 11. inner_inc_macro_ref_cnt — 递增宏块引用计数
  /* NOTICE: 步骤 11 后禁止 OB_FAIL,否则宏块引用计数泄漏 */
  is_inited_ = true;
}

函数采用典型的 OceanBase “错误码瀑布”风格:每个 else if (OB_FAIL(…)) 块对应一个初始化步骤,任何一步失败都会跳到收尾的 reset()。特别注意步骤 11(递增宏块引用计数)之后有明确注释:禁止在此处再出现 OB_FAIL,否则会导致宏块引用计数泄漏——这是存储层 GC 安全的硬约束。

2. 合并路径:init_for_merge

Compaction(Mini/Minor/Major Merge)完成后,用新的 SSTable 替换旧的,生成一个全新 Tablet 对象。设计上每次 merge 都创建新对象而非原地修改,保证原子性和回滚安全。

int ObTablet::init_for_merge(
    common::ObArenaAllocator &allocator,
    const ObUpdateTableStoreParam &param,
    const ObTablet &old_tablet)
{
  // 从旧 tablet 继承 meta,更新 snapshot_version / multi_version_start
  // 取新旧 schema 版本的最小值(防 replay/reboot 时丢 schema)
  int64_t input_max = MIN(MAX(param.storage_schema_->schema_version_,
      old_schema->schema_version_), max_sync_schema_version);
  // 用旧 table_store + 新 param 构建新表存储
  ALLOC_AND_INIT(allocator, table_store_addr_, (*this), param, (*old_table_store));
  // 更新缓存 + SCN + medium 合并进度
  // 可选:init_report_info — 更新上报状态
}

一个关键设计是取新旧 Schema 版本的最小值——这防止了 replay 或 reboot 时由于旧 SSTable 与新 Schema 不匹配导致的校验错误。

3. 迁移路径:init_with_migrate_param

物理备库同步、副本迁移、Transfer(分区迁移到其他 LS)时构建新 Tablet。函数内部分两条路径:

  • 空壳路径(is_empty_shell):无数据 tablet,仅初始化 meta 和空 table_store,不分配 storage_schema 和 macro_info(全设为 none_addr)。远端未同步数据时的占位符。
  • 正常路径:有实际数据。旧版本(< PARAM_VERSION_V3)需额外构造 mds mini sstable 注入 table_store,保证迁移后 MDS 数据不丢失。Transfer 场景则需从 LS leader 获取 storage_schema 并转换为 CS 副本格式。

4. SSTable 批量替换:init_for_sstable_replace

HA 主备同步、Transfer Replace、Tablet Split 三大场景下批量替换整个 table_store。与 init_for_merge 的区别在于使用 ObBatchUpdateTableStoreParam(批量参数),支持一次替换多个 SSTable:

// 分裂和 HA 两条分支
if (is_tablet_split) {
  table_store_addr_.ptr_->build_split_new_table_store(allocator, *this, param, *old_store);
} else {
  table_store_addr_.ptr_->build_ha_new_table_store(allocator, *this, param, *old_store);
}
// transfer_replace 场景特殊处理
if (param.is_transfer_replace_) {
  OB_FAIL(handle_transfer_replace_(param));
}
// 分裂场景:split_info.data_incomplete 在 major merge 时清除
if (is_tablet_split && is_major_merge_type(param.tablet_split_param_.merge_type_)) {
  split_info.set_data_incomplete(false);
}

分裂时 data_incomplete 标记在 major merge 完成后清除,保证分裂后数据完整性。ERRSIM 分支(ERRSIM_REPLACE_SWAP_BEFORE)用于测试 Transfer 替换的故障注入。

5. 序列化:serialize / self_serialize

Tablet 持久化到宏块时的序列化逻辑,采用”先写 payload 后回填 header”策略:

int ObTablet::serialize(...) const {
  // 1. 构造 block_header(version / length / checksum / inline_meta 描述)
  // 2. self_serialize 写入 tablet 自身数据
  // 3. 遍历 meta_arr,序列化内联 macro_info
  // 4. CRC64 校验和计算:block_header.checksum_ = ob_crc64(buf+pos, self_size)
  // 5. 回填 block_header 到 buf 开头
}

// self_serialize 写入顺序(V4):
// version → length → tablet_meta_ → table_store_addr →
// storage_schema_addr → rowkey_read_info(可选) → macro_info_addr

block_header 放最后回填,因为 length 和 checksum 在 payload 写完后才能确定。inline_meta 机制允许将 macro_info 内联到 tablet 序列化数据中,减少一次独立 IO。CRC64 校验保证反序列化时能检测数据损坏。

6. 反序列化:load_deserialize / load_deserialize_v3

从宏块读 Tablet 到内存时,首先读取 block_header 版本号,按 V1/V2/V3 路由到不同加载函数。V3 是当前主力格式:

int ObTablet::load_deserialize_v3(...) {
  // 1. 解析 block_header
  header.deserialize(buf, len, pos);
  // 2. CRC 校验
  crc = ob_crc64(buf + new_pos, header.length_);
  if (header.checksum_ != crc) { ret = OB_CHECKSUM_ERROR; }
  // 3. 调用 V2 加载 tablet 自身数据
  load_deserialize_v2(allocator, buf, len, new_pos, prepare_memtable);
  // 4. 反序列化 macro_info_addr + 可选内联 macro_info
  // 5. 计算内联 macro_info 在宏块内的精确偏移地址
}

版本化路由保证升级兼容:旧格式 V1 Tablet 可以在新版本中正确加载。V3 的 CRC 校验在加载时检测数据损坏,避免使用损坏的 Tablet。has_next_tablet_ 标记支持链式 Tablet 递归反序列化。

7. 宏块引用计数:inc_macro_ref_cnt / inner_inc_macro_ref_cnt

引用计数是 OceanBase 存储层 GC 安全的核心机制。每个 Tablet 持有其所有 SSTable 所在宏块的引用,加载时 inc_ref,淘汰时 dec_ref,引用归零后宏块才能被 GC 回收。

int ObTablet::inner_inc_macro_ref_cnt() {
  if (macro_info_addr_.is_none_object() || is_empty_shell()) {
    // 路径 A:无聚合信息,逐个从 addr 解析宏块 ID
    inc_ref_without_aggregated_info();
  } else {
    // 路径 B:有聚合 macro_info,批量迭代 inc_ref
    inc_ref_with_aggregated_info();
  }
  hold_ref_cnt_ = true;  // 标记已持有引用
}

// 路径 B 内部:
// load_macro_info → info_iter.init → inc_addr_ref_cnt(tablet_addr)
// → inc_ref_with_macro_iter(info_iter) — 遍历所有宏块调用 OB_STORAGE_OBJECT_MGR.inc_ref
// 失败时回滚:dec_addr_ref_cnt + dec_ref_with_macro_iter

V3+ 聚合 macro_info 将所有宏块 ID 集中存储,一次 IO 即可获取全部引用列表,显著加速冷启动时 Tablet 加载。失败回滚机制保证原子性:部分 inc_ref 成功后如果后续步骤失败,已加的引用必须回退。

小结

ob_tablet.cpp 展现了 OceanBase 存储引擎在 Tablet 管理层面的几个核心设计决策:

  • 地址包装 + 延迟加载:Tablet 自身保持紧凑(约 2KB),大对象按需 IO,兼顾启动速度和内存效率
  • 每次 merge 创建新对象:而非原地修改,保证原子性和回滚安全
  • 宏块引用计数:Tablet 持有宏块引用,引用归零才能 GC,防止活跃数据被误回收
  • 版本化序列化/反序列化:V1→V2→V3 三级兼容,支持集群滚动升级
  • 聚合信息机制:V3+ 将宏块 ID 集中存储,减少 IO 次数,加速引用计数操作
  • 错误码瀑布风格:每步失败即跳出,保证资源不泄漏,配合 reset() 兜底

这些设计共同支撑了 OceanBase 在大规模分布式环境下对存储数据的安全、高效管理,是理解整个存储引擎读写路径的基础。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注