OceanBase 宏块、微块与表的关系

读 OceanBase 存储层源码,绕不开两个最基本的结构:宏块(Macro Block)与微块(Micro Block)。宏块是 2MB 的物理存储与 IO 单位,微块是宏块内部默认 16KB 的压缩与索引单位,一条 SQL 的数据最终就躺在这两者的层层嵌套里。本文聚焦两个最常被问到的问题:宏块和微块里会不会混进不同表的数据?两者到底谁是容器、谁是内容?所有结论都直接来自 v4.3 源码。

一、各自的身份与尺寸

宏块 Macro Block 微块 Micro Block
尺寸 固定 2MB(OB_DEFAULT_MACRO_BLOCK_SIZE = 2 << 20,ob_define.h:1965) 默认 16KB(OB_DEFAULT_SSTABLE_BLOCK_SIZE,建表可用 block_size 指定 4KB~1MB)
角色 空间分配单位、磁盘 IO 单位、缓存单位 压缩/编码单位、索引定位单位
数量关系 1 宏块 : N 微块(N 不固定);1 微块 : M 行(M 不固定)

注意两者不是 1:1 关系:一个 2MB 宏块能装多少微块取决于压缩比和行宽,一个 16KB 微块装多少行取决于行长,所以只能给出”多对一”的方向,没有固定倍数。

二、最关键的硬约束:微块不跨宏块

微块的尺寸上限不是拍脑袋定的,而是在源码里由宏块尺寸倒算出来的(ob_data_store_desc.cpp):

micro_block_size_limit_ = macro_block_size_
    - ObMacroBlockCommonHeader::get_serialize_size()
    - ObSSTableMacroBlockHeader::get_fixed_header_size()
    - MIN_RESERVED_SIZE;   // 1KB

即微块尺寸上限 = 2MB 减去宏块两级头部再减 1KB 预留,目的就是保证任何微块都能完整放进一个宏块。写入侧的印证在 ob_macro_block_writer.cpp:

if (OB_FAIL(macro_blocks_[current_index_].write_micro_block(micro_block_desc, data_offset, nullptr))) {
  if (OB_BUF_NOT_ENOUGH == ret) {          // 当前宏块装不下这个微块
    try_switch_macro_block();              // 刷盘,结束当前宏块
    alloc_block();                         // 申请新宏块
    macro_blocks_[current_index_].write_micro_block(micro_block_desc, data_offset, nullptr);
  }
}

微块是整块原子写入的:装不下就换宏块,绝不会劈成两半分居两个宏块。

三、归属问题:微块不可能跨表,标准宏块单 tablet

微块头 ObMicroBlockHeader 里完全没有 table_id / tablet_id 字段——只有 magic、版本、列数、rowkey 列数、行数、编码类型、校验和。微块本质是”某个 tablet 内一段按 rowkey 有序的行”的压缩单元,归属由外层决定,所以同一个微块里的行必然是同一张表、同一个分区的数据。

标准宏块同样只属于一个 tablet:宏块头 ObSSTableMacroBlockHeader::FixedHeader 里 tablet_id_ 是单一取值,写入时取自写入描述符:

// ob_sstable_macro_block_header.cpp
fixed_header_.tablet_id_ = desc.get_tablet_id().id();

而 ObMacroBlockWriter::open(一个 ObDataStoreDesc) 是”一个 writer 绑定一个 tablet”,宏块内所有微块都来自同一个 writer。

标准宏块与小 SSTable 共享宏块的表归属对照

四、例外:小 SSTable 共享宏块

翻源码时比较意外的点:当某个 tablet 的整个 SSTable 小于 1MB(SMALL_SSTABLE_THRESHOLD = 1 << 20)时,OceanBase 不会单独给它分配 2MB 宏块,而是走租户级单例 ObSharedMacroBlockMgr:它内部持有一个宏块句柄和 offset_ 游标,把每个小 SSTable 的整块内容(含自己的宏块头、微块、索引与元数据)当作一个 blob 按偏移依次追加进同一个物理宏块,写满 2MB 才换新块;每个 blob 的位置记录在 ObBlockInfo 的 nested_offset_ / nested_size_ 里。

因为容器是租户级共享的,一个物理宏块里确实可以并存不同 tablet、甚至不同表的行记录。但它们不是混在一起,而是彼此独立的多个”逻辑宏块”段,每段保留自己的 tablet_id 头和 rowkey 范围。读取时按偏移定位:

// ob_index_block_row_scanner.cpp
nested_offset_ = sstable.get_macro_offset();
// ob_index_block_row_struct.h
return row_header_->get_block_offset() + nested_offset_;

一句话:物理共享、逻辑隔离。

五、宏块的三种类型与内部布局

宏块按用途分三类(ObMacroDataSeq::BlockType):数据宏块装数据微块,索引宏块装索引微块(构成索引树),元数据宏块装宏块级元数据。数据量不足 1MB 时三类内容会合并进同一个宏块。单个数据宏块的内部布局为:物理头 + 逻辑头 + 列定义/列序/列校验 + N 个数据微块 + 索引微块 + 元数据块,头部里三组 offset/size(micro_block_data、idx_block、meta_block)正好对应这三个分区。

SSTable 的宏块组成与宏块内部布局

另一个细节:宏块实际可写空间是 90%(DEFAULT_RESERVE_PERCENT = 90),预留的 10% 给增量合并时微块复用等场景留余量。

六、这个划分解决了什么问题

  • 宏块管 IO 效率:2MB 定长 + DIO 对齐,顺序读、预读、块缓存都以它为粒度;
  • 微块管压缩和检索:同一宏块里不同微块可以选用不同编码/压缩方式;查询时靠索引里的 macro_id + block_offset_ 直接定位微块,解压一个微块就能拿到目标行,不必解压整个 2MB;
  • 宏块元数据管裁剪:ObDataMacroBlockMeta 记录每个宏块的末行 rowkey 与统计信息,查询时可以整块跳过,多版本合并时也靠它决定微块能否直接复用。

读取路径串起来就是:索引树 → 定位宏块 → 读 2MB 宏块 → 按 block_offset 取微块 → 解压解码 → 取行。

小结

宏块与微块是”容器与内容”的关系:宏块是 2MB 的物理容器与 IO 单位,微块是宏块内部默认 16KB 的压缩与索引单位;微块既不跨宏块,也不跨表;标准宏块只属于一个 tablet,唯一例外是小于 1MB 的小 SSTable 会被打包进租户级共享宏块——物理上多表同居一个块,逻辑上各段各管各的。

发表回复

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