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 ¶m,
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 ¶m,
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 ¶m,
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 级数据规模下的高效存储与访问能力。