OceanBase源码解读:SSTable持久化存储引擎 ob_sstable.cpp

SSTable(Sorted-String Table)是 OceanBase 存储引擎的持久化基座。当 MemTable 中的数据积累到一定量或触发合并条件时,内存数据会被刷盘为 SSTable。每个 Tablet 持有多个 SSTable,按版本从新到旧排列,读取时由 TableStore 统一调度。本文深入解析 ob_sstable.cpp 的核心实现,涵盖创建初始化、读写访问、序列化/反序列化及宏块引用计数四大关键路径。

一、核心数据结构

SSTable 的内存表示通过三层结构组织,每一层都有明确的职责边界:

  • ObSSTable(≤264 字节):SSTable 对象本体,通过 static_assert 强制约束大小。内含 addr_(磁盘/内存地址)、meta_cache_(热数据缓存)、meta_(指向完整元数据的指针)。由于每个 Tablet 持有数十个 SSTable,控制对象大小对降低内存占用至关重要。
  • ObSSTableMeta:完整元数据,包含 basic_meta(状态/行数/checksum/事务版本号)、macro_info(宏块布局信息,区分 data_block / other_block / linked_block 三类)、root_block(B+树索引根节点)。
  • ObSSTableMetaCache:元数据缓存层,缓存行数、宏块数、occupy_size、trans_version 等高频访问字段。后续查询直接走 cache 无需反序列化完整 meta,显著降低 IO 开销。

二、创建初始化流程

ObSSTable::init() 是 SSTable 的创建入口,采用 OceanBase 标志性的瀑布式错误处理风格,7 步串联完成:

int ObSSTable::init(const ObTabletCreateSSTableParam &param,
                    common::ObArenaAllocator *allocator)
{
  int ret = OB_SUCCESS;
  bool inc_success = false;
  // 1. 初始化 table_key(tablet_id + ls_id + version)
  if (OB_FAIL(ObITable::init(param.table_key()))) {
  // 2. 初始化完整元数据(分配 ObSSTableMeta + transform root block)
  } else if (OB_FAIL(init_sstable_meta(param, allocator))) {
  // 3. 设置内存地址(标记为临时 SSTable)
  } else if (FALSE_IT(addr_.set_mem_addr(0, sizeof(ObSSTable)))) {
  // 4. 增加宏块引用计数
  } else if (OB_FAIL(inc_macro_ref(inc_success))) {
  // 5. 清除 linked_block 引用(init 时无需保留外链块引用)
  } else if (FALSE_IT(meta_->macro_info_.dec_linked_block_ref_cnt())) {
  // 6. 标记为临时 SSTable(reset 时需 dec_macro_ref)
  } else if (FALSE_IT(is_tmp_sstable_ = true)) {
  // 7. 若参数标记为 ready_for_read,校验可读性
  } else if (param.is_ready_for_read()) {
    if (OB_FAIL(check_valid_for_reading())) { }
  }
  // 最终状态校验:必须为 READY_FOR_READ 或 WRITE_BUILDING
  if (OB_FAIL(ret)) { }
  else if (OB_UNLIKELY(!(SSTABLE_READY_FOR_READ == status
                       || SSTABLE_WRITE_BUILDING == status))) {
    reset();  // 状态不合法则回滚
  }
  return ret;
}

其中 init_sstable_meta() 负责分配并初始化 ObSSTableMeta,包括 root_block 的 extra buffer 转换(将索引根数据从序列化缓冲区提取到独立内存),以及 meta_cache_ 的初始化。关键设计:is_tmp_sstable_=true 标志表明这是一个刚创建但尚未持久化的临时 SSTable,reset() 时会执行 dec_macro_ref() 释放引用计数。

三、读写访问路径

3.1 范围扫描 scan()

scan() 根据查询标志和 SSTable 类型选择三种迭代器:

int ObSSTable::scan(const ObTableIterParam &param,
                     ObTableAccessContext &context,
                     const ObDatumRange &key_range,
                     ObStoreRowIterator *&row_iter)
{
  // ...
  if (context.query_flag_.is_whole_macro_scan()) {
    // 全宏块扫描:用于 compaction / 迁移,跳过索引直接遍历数据块
    ALLOCATE_TABLE_STORE_ROW_IETRATOR(context,
        ObSSTableRowWholeScanner, row_scanner);
  } else if (is_multi_version_minor_sstable()) {
    // Minor SSTable 多版本读:需要处理多版本行链
    ALLOCATE_TABLE_STORE_ROW_IETRATOR(context,
        ObSSTableMultiVersionRowScanner, row_scanner);
  } else {
    // 标准范围扫描:B+树索引驱动
    ALLOCATE_TABLE_STORE_ROW_IETRATOR(context,
        ObSSTableRowScanner, row_scanner);
  }
  // 初始化迭代器:传入 param + context + this + key_range
  row_scanner->init(param, context, this, &key_range);
  return ret;
}

三种迭代器的选择体现了 SSTable 的多场景适配:全宏块扫描用于后台合并任务(不需要索引定位),多版本扫描处理 Minor SSTable 中的多版本数据链,标准扫描则是 OLTP 查询的主路径。

3.2 点查询 get()

get() 的迭代器选择逻辑更简洁,核心在于多版本读判断:

int ObSSTable::get(const ObTableIterParam &param,
                     ObTableAccessContext &context,
                     const ObDatumRowkey &rowkey,
                     ObStoreRowIterator *&row_iter)
{
  // ...
  if (is_multi_version_minor_sstable()
      && (context.is_multi_version_read(get_upper_trans_version())
        || contain_uncommitted_row())) {
    // 多版本 Getter:处理未提交行和跨版本读取
    ALLOCATE_TABLE_STORE_ROW_IETRATOR(context,
        ObSSTableMultiVersionRowGetter, row_getter);
  } else {
    // 标准 Getter:B+树索引点查
    ALLOCATE_TABLE_STORE_ROW_IETRATOR(context,
        ObSSTableRowGetter, row_getter);
  }
  return ret;
}

多版本读判断通过 context.is_multi_version_read(get_upper_trans_version()) 检查读快照是否覆盖该 SSTable 的事务版本范围。如果 SSTable 包含未提交行(contain_uncommitted_row()),也必须走多版本路径以确保 MVCC 正确性。

四、序列化与反序列化

4.1 双模式序列化

SSTable 的序列化采用双模式设计,由 addr_ 的类型决定走哪条路径:

int ObSSTable::serialize(char *buf, int64_t buf_len, int64_t &pos) const
{
  // 先序列化 table_key(ObITable 基类)
  ObITable::serialize(buf, buf_len, pos);
  // 根据 addr_ 类型选择模式
  ObSSTable::StatusForSerialize status;
  if (!addr_.is_memory()) {
    // 磁盘地址:仅序列化 fixed_struct(table_key + 地址 + 状态)
    // 因为 meta 数据已在磁盘块中,反序列化时通过 get_meta 按地址加载
    status.set_with_fixed_struct();
  } else {
    // 内存地址:全量序列化(table_key + status + 完整 ObSSTableMeta)
    // 用于 Tablet merge 时复制内存态 SSTable
    status.set_with_meta();
  }
  OB_UNIS_ENCODE(status.pack_);
  if (status.with_fixed_struct())
    serialize_fixed_struct(buf, buf_len, pos);
  else if (status.with_meta())
    meta_->serialize(buf, buf_len, pos);
  return ret;
}

这个设计避免了重复存储:当 SSTable 已持久化到磁盘时,序列化输出仅包含地址和 table_key(几十字节),而完整元数据通过反序列化时从存储缓存按地址加载。

4.2 兼容性反序列化

反序列化需要处理新旧二进制格式的兼容性:

int ObSSTable::deserialize(ObArenaAllocator &allocator,
    const char *buf, int64_t data_len, int64_t &pos)
{
  // 先解码基类 table_key
  ObITable::deserialize(buf, data_len, pos);
  // 兼容检查:尝试读取 status 魔数
  int64_t compat_temp_pos = pos;
  OB_UNIS_DECODE(status.pack_);
  if (COMPAT_MAGIC == status.compat_magic_ && 0 == status.reserved_) {
    // 新格式:按 status 标志走 fixed_struct 或 meta 路径
  } else {
    // 老格式:回退 pos,强制走 with_meta 全量反序列化
    pos = compat_temp_pos;
    status.reset();
    status.set_with_meta();
  }
  // meta 路径:分配 meta → 反序列化 → 校验 → 修复 filled_tx_scn
  // → transform_root_block_extra_buf → check_valid_for_reading → init cache
  return ret;
}

兼容性处理的关键在于 COMPAT_MAGIC 魔数检测:新格式二进制在 status 字段中嵌入了魔数标志,老格式没有该字段,通过回退 pos 并强制走全量 meta 路径实现向后兼容。

五、宏块引用计数

SSTable 引用三类宏块,每类独立计数,确保垃圾回收安全:

  • data_block:存储实际行数据的宏块
  • other_block:BloomFilter 等元数据宏块
  • linked_block:外链宏块(跨 SSTable 共享的宏块)

inc_macro_ref() 逐块增加引用计数,并通过计数器记录成功数以支持失败回滚:

int ObSSTable::inc_macro_ref(bool &inc_success) const
{
  // 1. 遍历 data_block,逐块 inc_ref,记录 data_blk_cnt
  while (OB_SUCC(iter.get_next_macro_id(macro_id))) {
    OB_STORAGE_OBJECT_MGR.inc_ref(macro_id);
    ++data_blk_cnt;
  }
  // 2. 遍历 other_block,逐块 inc_ref,记录 other_blk_cnt
  // 3. 遍历 linked_block,逐块 inc_ref,记录 linked_blk_cnt
  // 4. add_used_size(更新共享宏块管理器统计)
  
  // 失败回滚:逆序 dec_ref 已增加的块
  if (OB_FAIL(ret) && meta_handle.is_valid()) {
    for (int64_t i = data_blk_cnt; i > 0; --i)
      OB_STORAGE_OBJECT_MGR.dec_ref(macro_id);
    // 同样回滚 other_blk_cnt 和 linked_blk_cnt
  }
  return ret;
}

原子性保证的核心在于失败回滚:如果 inc_ref 过程中某块失败,会逆序调用 dec_ref 回滚已增加的块,确保不会出现”引用计数增加了一半”的不一致状态。dec_macro_ref() 是逆操作,遍历同三类块逐个 dec_ref,使用 ignore_ret() 容忍 IO 重试(因为减引用是 GC 时的清理操作,不应因单块失败而中断)。

六、元数据访问 get_meta()

get_meta() 是所有元数据访问的统一入口,根据 SSTable 的加载状态走两条路径:

int ObSSTable::get_meta(ObSSTableMetaHandle &meta_handle,
    ObSafeArenaAllocator *allocator) const
{
  if (is_loaded()) {
    // 路径1:meta_ 已在内存,直接赋值,零 IO
    meta_handle.meta_ = meta_;
  } else {
    // 路径2:meta_ 为空,addr_ 指向磁盘块
    ObStorageMetaKey meta_key(MTL_ID(), addr_);
    bool bypass_cache = (nullptr != allocator);
    if (!bypass_cache) {
      // 默认:走存储元数据缓存
      meta_cache.get_meta(meta_type, meta_key, handle, nullptr);
    } else {
      // 绕过缓存:直接从磁盘读,用完即释放
      meta_cache.bypass_get_meta(meta_type, meta_key, *allocator, handle);
    }
    // 从缓存值中取出 ObSSTable 指针,取其 meta_ 成员
    value->get_sstable(sstable_ptr);
    meta_handle.meta_ = sstable_ptr->meta_;
  }
  return ret;
}

绕过缓存的场景主要用于合并等一次性操作,传入 arena allocator 后直接从磁盘读取元数据,不占用缓存空间。CO SSTable(Column-Oriented)和普通 SSTable 通过 meta_type 区分,缓存层分别管理。

小结

ob_sstable.cpp 实现了 SSTable 从创建到销毁的完整生命周期管理。其设计有几个值得注意的工程取舍:

  • 264B 大小约束:通过 static_assert 强制对象大小,因为 SSTable 被大量实例化,每字节都影响内存占用。
  • meta_cache 热数据缓存:将高频字段从完整 meta 中提取到独立 cache,避免每次访问都反序列化 B+树索引等大对象。
  • 序列化双模式:磁盘态仅序列化地址和 key(几十字节),内存态全量序列化,避免了持久化数据的重复存储。
  • 三类宏块独立引用计数:data/other/linked 分别计数,inc 失败时逆序回滚保证原子性,dec 时容忍重试确保 GC 不中断。

这些设计共同支撑了 OceanBase 在 PB 级数据规模下的高效存储与访问能力。

发表回复

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