Ceph BlueStore 元数据全景:一个 OSD 的 RocksDB 里到底存了什么?
本文基于 Ceph 源码(
os/bluestore/bluestore_types.h、os/bluestore/BlueStore.h、os/bluestore/BlueStore.cc、os/bluestore/BlueFS.h)深入分析 BlueStore 的元数据存储架构,完整回答"一个 OSD 的 BlueStore 上到底存了哪些元数据"这一问题。
一、BlueStore 的整体架构
每个 OSD 进程内部运行一个 BlueStore 实例,BlueStore 是 Ceph 从 FileStore 演进到的新型存储引擎。核心设计思想:元数据全部进 RocksDB,用户数据直接写裸盘。
OSD 进程 ├── BlueStore (ObjectStore 实现) │ ├── bdev[BDEV_WAL] → block.wal (WAL 专用裸盘分区) │ ├── bdev[BDEV_DB] → block.db (RocksDB 专用裸盘分区) │ ├── bdev[BDEV_SLOW] → block.slow (可选,RocksDB 溢出空间) │ ├── bdev[data] → block (主数据裸盘分区) │ ├── BlueFS (简易文件系统,管理 WAL/DB/Slow 设备上的文件) │ └── RocksDB (通过 BlueRocksEnv 读写 BlueFS 上的文件) │ └── 存储所有元数据 └── 主数据盘 block (直接裸盘 IO,不走 BlueFS/RocksDB)BlueFS 是一个极简的文件系统,专门为 RocksDB 服务。它管理block.wal、block.db、block.slow三个裸盘分区上的文件读写。RocksDB 通过自定义的BlueRocksEnv(实现了 rocksdb::Env 接口)来访问 BlueFS 的文件,而不是操作宿主机的 ext4/xfs 文件系统。
这样做的好处是:RocksDB 的 WAL(Write-Ahead Log)可以写到更快的小盘(NVMe),SST 文件写到中等速度的盘,溢出部分再写到慢盘——三级存储分层,按速度和成本分配。
而用户数据(对象的 payload)则完全绕过 RocksDB,通过 AIO 直接写到主数据裸盘(block设备),只有"数据在裸盘的物理位置"这条元数据才进 RocksDB。
二、元数据的分类:9 个 Key Prefix
BlueStore 把 RocksDB 的 key 按前缀(prefix)分成 9 个 namespace,每个 prefix 存不同类型的元数据。源码定义在BlueStore.cc第 70~80 行:
conststring PREFIX_SUPER="S";// field -> valueconststring PREFIX_STAT="T";// field -> value(int64 array)conststring PREFIX_COLL="C";// collection name -> cnode_tconststring PREFIX_OBJ="O";// object name -> onode_tconststring PREFIX_OMAP="M";// u64 + keyname -> valueconststring PREFIX_PGMETA_OMAP="P";// u64 + keyname -> value(for meta coll)conststring PREFIX_DEFERRED="L";// id -> deferred_transaction_tconststring PREFIX_ALLOC="B";// u64 offset -> u64 length (freelist)conststring PREFIX_ALLOC_BITMAP="b";// (see BitmapFreelistManager)conststring PREFIX_SHARED_BLOB="X";// u64 offset -> shared_blob_t下面逐个详细解析。
三、Super(S)—— 全局配置信息
key: "S" + field_name value: 全局字段值这是 BlueStore 的 superblock,存 OSD 级别的全局配置信息:
- FSID:Ceph 集群 ID
- OSD UUID:该 OSD 的唯一标识
- last_seq:最后的事务序列号
类似传统文件系统的 superblock,在 OSD 启动时读取,用于验证设备归属和恢复状态。
四、Stat(T)—— 统计信息
key: "T" + field_name (如 "bluestore_statfs") value: int64 数组存 OSD 级别的空间统计信息(store_statfs_t),包括:
- 已用空间大小
- 可用空间大小
- 对象总数
这些数据会定期更新,对外通过ceph osd df命令展示。本质上是"这个 OSD 的磁盘空间用了多少、还剩多少"的底层记录。
五、Collection(C)—— 集合元数据
key: "C" + collection_name value: bluestore_cnode_t { bits }Collection 是 PG(Placement Group)的等价概念。cnode_t的结构极其精简(bluestore_types.h第 52~64 行):
structbluestore_cnode_t{uint32_tbits;///< how many bits of coll pgid are significant};只有一个字段bits:PGID 的有效哈希位数。它控制了 PG 的分裂粒度——当 PG 数量增长时,bits 增大,一个 Collection 可以分裂成更细的子集合。
Collection 虽然元数据很小,但它是对象组织的"容器":所有对象都挂在一个 Collection 下面,读写对象时先定位 Collection。
六、Onode(O)—— 对象元数据(最核心的部分)
key: "O" + collection_hash + object_hash value: bluestore_onode_t这是 BlueStore 中最重要的元数据类型,类似文件系统的 inode。源码定义(bluestore_types.h第 898~970 行):
structbluestore_onode_t{uint64_tnid=0;///< numeric id (locally unique)uint64_tsize=0;///< object sizestd::map<string,buffer::ptr>attrs;///< xattrsstructshard_info{uint32_toffset=0;///< logical offset for start of sharduint32_tbytes=0;///< encoded bytes};vector<shard_info>extent_map_shards;///< extent map shards (if any)uint32_texpected_object_size=0;uint32_texpected_write_size=0;uint32_talloc_hint_flags=0;uint8_tflags=0;// FLAG_OMAP = 1, FLAG_PGMETA_OMAP = 2};逐字段解释:
| 字段 | 含义 |
|---|---|
nid | 对象数字 ID,OSD 内唯一,用于 omap key 的前缀 |
size | 对象的逻辑大小(字节) |
attrs | 对象的扩展属性(xattr)——键值对,存用户/系统属性。类似文件系统的 xattr |
extent_map_shards | extent map 分片索引。小对象 inline 在 onode 里,大对象分片存储 |
expected_object_size | 分配提示:上层期望的对象大小(RBD/CephFS 传来) |
expected_write_size | 分配提示:期望的写大小 |
alloc_hint_flags | 分配提示标志(如SEQUENTIAL、RANDOM) |
flags | FLAG_OMAP 表示有 omap 数据 |
注意:onode 的 key 是collection_hash + object_hash的组合,不是对象名本身。这样做可以高效地按 Collection 扫描所有对象(RocksDB 的 key 是有序的)。
内存中,Onode 还关联一个ExtentMap(BlueStore.h 第 1054 行):
structOnode{ghobject_t oid;///< 对象名(内存中才有)string key;///< RocksDB keybluestore_onode_t onode;///< 持久化的元数据boolexists;///< 对象是否逻辑存在ExtentMap extent_map;///< 数据布局映射(内存中展开)};七、Extent Map + Blob —— 数据布局与校验和(layout 和 CRC)
这是本文的核心问题——“对象的 layout 信息和 CRC 信息在哪里?”
7.1 Extent Map 的概念
Extent Map 是 onode 下面的子结构,描述"对象的逻辑字节范围 → 物理存储位置"的映射关系。类似文件系统的 extent 映射(ext4 的 extent tree、XFS 的 extent map)。
对象逻辑空间:offset 0 ... size-1 ↓ Extent Map 映射 裸盘物理空间:pextent {offset, length}对于小对象,Extent Map 直接 inline 在 onode 的 value 里(一条 KV);对于大对象,Extent Map 被分成多个 shard,每个 shard 是一条独立的 RocksDB KV。
7.2 Blob 的结构(layout + CRC 都在这里)
每个 Extent 引用一个 Blob。bluestore_blob_t是 layout 和 CRC 的共同载体(bluestore_types.h第 429~862 行):
structbluestore_blob_t{PExtentVector extents;///< 裸盘上的物理位置!这就是 layoutuint32_tlogical_length=0;///< 逻辑长度uint32_tcompressed_length=0;///< 压缩后长度uint32_tflags=0;///< FLAG_COMPRESSED/CSUM/SHARED/HAS_UNUSEDuint8_tcsum_type=Checksummer::CSUM_NONE;///< 校验和类型uint8_tcsum_chunk_order=0;///< chunk 大小 = 1 << orderbufferptr csum_data;///< 校验值数组(真正的 CRC 数据)unused_t unused=0;///< 未使用区域位图};关键字段详解:
extents(PExtentVector)—— 对象的 layout 信息
structbluestore_pextent_t:publicbluestore_interval_t<uint64_t,uint32_t>{uint64_toffset;// 裸盘物理偏移uint32_tlength;// 物理长度};每个 pextent 就是一个{offset, length}对,告诉你"这块数据写在主数据裸盘的哪个位置、多长"。一个 Blob 可以有多个 pextent(比如数据被分散写到多个不连续的位置)。
这就是你问的"对象的 layout 信息"——它存在 RocksDB 里,不在裸盘上。
csum_data —— 对象的 CRC 信息
uint8_tcsum_type;// 校验和类型:CRC32、XXHASH32 等uint8_tcsum_chunk_order;// chunk 大小 = 2^order 字节bufferptr csum_data;// 校验值数组CRC 也是按 chunk 计算的,不是整个 Blob 一个校验值。csum_chunk_order决定了每个 chunk 多大(比如 order=12 → chunk=4096 字节),csum_data是一个紧凑的数组,每csum_value_size字节存一个 chunk 的校验值。
这就是你问的"CRC 信息"——它也存在 RocksDB 里,不在裸盘数据块旁边!
flags —— Blob 的特征标记
| Flag | 值 | 含义 |
|---|---|---|
| FLAG_COMPRESSED | 2 | Blob 数据被压缩了,compressed_length有效 |
| FLAG_CSUM | 4 | Blob 有校验和,csum_*字段有效 |
| FLAG_HAS_UNUSED | 8 | Blob 有未写入区域(unused位图有效) |
| FLAG_SHARED | 16 | Blob 被多个对象共享(去重场景,见 SharedBlob) |
unused —— Blob 内的空洞位图
当一个 Blob 被分配了空间但部分区域还没写入数据时,unused位图标记哪些 chunk 是"空洞"(从未写入)。这在稀疏写场景下非常有用——可以避免对空洞区域做不必要的读-改-写。
7.3 为什么要把 layout 和 CRC 都放 RocksDB?
这是一个深思熟虑的设计决策。如果像传统文件系统那样把 CRC 写到数据块旁边(每个数据块自带 header),会带来几个问题:
原子性:RocksDB 提供事务性写入。layout 更新 + CRC 更新 + 空间分配记录更新可以在同一个 RocksDB transaction 中完成。如果 CRC 写到裸盘上,layout 更新写到 RocksDB 里,两者无法在同一事务中提交,可能导致"layout 是新的、CRC 是旧的"这种不一致。
灵活性:CRC 和 layout 放在 KV 存储里,可以独立更新。比如做压缩时,只需要修改 Blob 的
compressed_length和csum_data,不需要重写数据块。读路径优化:读数据时,先从 RocksDB 查 layout 和 CRC,然后直接从裸盘读数据。如果 CRC 在裸盘上,就需要先读数据块头部来获取 CRC,再读数据本身——多了一次 IO。
数据块对齐:裸盘数据块保持纯净(只有用户数据),不做 read-modify-write 来嵌入元数据头。
八、OMAP(M)—— 对象的 Key-Value 附表
key: "M" + onode_nid + user_key value: user_valueOMAP 是 BlueStore 提供的对象级 key-value 存储接口。它不同于 xattr(xattr 数量少、值小),OMAP 支持海量条目、值可以很大。
典型使用场景:
- RBD(块设备):RBD image 的元数据几乎全部存在 omap 里——image header、snap info、parent reference 等
- CephFS:目录的文件列表也存在 omap 中(
dirname → inode_number映射) - RGW(对象网关):bucket index 存在 omap 中
omap 的 key 使用 onode 的nid作为前缀,这样同一个对象的所有 omap 条目在 RocksDB 中是连续排列的,范围扫描效率很高。
九、PG Meta OMAP(P)—— PG 级别的元数据
key: "P" + onode_nid + key value: valuePG(Placement Group)级别的状态信息使用特殊的 omap prefixP存储,与普通对象的 omapM分开。存的是:
- PG info:PG 的状态、last_update、last_complete 等
- PG log:PG 的操作日志(用于恢复和一致性检查)
PG meta 对象(如meta、osdmap.NNN)是 OSD 内部的"系统对象",不在用户数据空间里。
十、Deferred Transaction(L)—— 延迟写事务
key: "L" + sequence_id value: bluestore_deferred_transaction_t源码定义(bluestore_types.h第 1005~1021 行):
structbluestore_deferred_transaction_t{uint64_tseq=0;list<bluestore_deferred_op_t>ops;interval_set<uint64_t>released;///< allocations to release after tx};什么时候会产生 deferred transaction?
BlueStore 的写分两种路径:
_do_write_big:大写(对齐到 min_alloc_size),直接在裸盘上分配新空间写入_do_write_small:小写(不对齐的小块覆盖写),需要做 read-modify-write 或 deferred
小块覆盖写无法原地更新(因为 min_alloc_size 的限制),BlueStore 选择把小块写记录为 deferred transaction,先存到 RocksDB,后续异步执行到裸盘上。这样做的好处是:小写不需要立即等裸盘 IO 完成,先记录意图,后续批量执行。
十一、Allocation(B/b)—— 裸盘空间管理
key: "B" + offset (BlockFreelistManager) key: "b" + bitmap_data (BitmapFreelistManager) value: length / bitmapBlueStore 不依赖宿主机文件系统,自己管理裸盘空间的分配和回收。空闲列表有两种实现:
- BlockFreelistManager(prefix B):key 是 offset,value 是 length。简单但空间效率低
- BitmapFreelistManager(prefix b):用 bitmap 紧凑编码,空间效率高,是新版本的默认实现
这个 prefix 记录的是"裸盘上 offset X 处有 length Y 的空闲空间"。当需要为新数据分配空间时,从空闲列表中找到合适的区间;当数据被删除时,释放的区间重新加回空闲列表。
所有分配和释放操作都在 RocksDB transaction 中完成,保证"数据写入 + 空间标记"的原子性。
十二、Shared Blob(X)—— 去重引用计数
key: "X" + sbid (shared blob id) value: bluestore_shared_blob_t源码定义(bluestore_types.h第 869~892 行):
structbluestore_shared_blob_t{uint64_tsbid;///> shared blob idbluestore_extent_ref_map_t ref_map;///< shared blob extents};当 Ceph 启用了去重(deduplication)功能时,多个对象可能引用同一块物理数据。此时 Blob 的FLAG_SHARED被设置,物理数据不再属于单个对象,而是被共享。
SharedBlob.ref_map记录每个逻辑偏移有多少对象在引用它——引用计数。当引用计数降为 0 时,物理空间才能被释放。
十三、完整的元数据流转:写一个对象的全过程
假设客户端写一个 4MB 的对象到 OSD,BlueStore 的内部流程如下:
1. 收到写请求 → 创建 TransContext(事务上下文) 2. 分配裸盘空间: - 从 Allocation 空闲列表中找到空闲区间 - 记录分配意图到 RocksDB transaction 3. 写裸盘数据(AIO): - 把 4MB 数据直接写到主数据裸盘的 offset=0x10000000 - 等待 AIO 完成 4. 更新 RocksDB 元数据(同一个 transaction): - PREFIX_OBJ(O):创建/更新 onode,设置 size=4MB - Extent Map:创建 Blob,记录 pextent={0x10000000, 4MB} - Blob.csum:计算每个 chunk 的 CRC,存入 csum_data - PREFIX_ALLOC(B/b):标记已分配空间 - PREFIX_STAT(T):更新统计信息 5. 提交 RocksDB transaction → 写 BlueFS WAL → SST compact → 完成整个过程的关键保证:步骤 4 中所有 RocksDB 操作在同一个 transaction 中完成。这意味着要么全部成功(onode 更新、layout 更新、CRC 更新、空间分配标记),要么全部失败(回滚)。裸盘上的数据(步骤 3)如果写了但 RocksDB transaction 没提交,那只是"垃圾数据"——因为没有任何元数据指向它,下次空间分配会覆盖它。
十四、读一个对象的过程
1. 从 RocksDB 查 PREFIX_OBJ(O):找到 onode → 得到 size、attrs、extent_map_shards 索引 2. 从 RocksDB 查 Extent Map(inline 或 shard): → 得到每个逻辑偏移对应的 Blob 3. 从 Blob 的 PExtentVector 得到裸盘物理位置: → pextent = {offset=0x10000000, length=4MB} 4. 从裸盘直接 AIO 读数据: → 读 offset 0x10000000,4MB 5. 校验 CRC(可选): → 从 Blob.csum_data 取每个 chunk 的校验值 → 与读到的数据计算值比对可以看到:读路径非常高效——先查 RocksDB 拿 layout(通常在内存 cache 中),然后一次 AIO 读裸盘。CRC 校验用 RocksDB 里的值,不需要额外读裸盘。
十五、元数据全景总结
| Prefix | 存储内容 | 作用类比 |
|---|---|---|
| S | Superblock(FSID, UUID, seq) | 文件系统 superblock |
| T | Stat(空间统计) | df的底层数据 |
| C | Collection(PG 分裂 bits) | 目录的 inode |
| O | Onode(对象大小、xattr、flags) | 文件的 inode |
| O+shard | Extent Map → Blob(layout + CRC + 压缩) | 文件的 extent tree + block map |
| M | OMAP(对象的 KV 附表) | 文件的 xattr(海量版) |
| P | PG Meta OMAP(PG info/log) | 文件系统的 journal |
| L | Deferred Transaction(延迟写) | 文件系统的延迟写日志 |
| B/b | Allocation(空闲空间管理) | 文件系统的 free block bitmap |
| X | Shared Blob(去重引用计数) | 文件系统的 shared block refcount |
核心设计原则可以一句话概括:
RocksDB 是 BlueStore 的"大脑"(存所有元数据和决策记录),裸盘是 BlueStore 的"身体"(只存纯粹的用户数据)。大脑和身体通过 Blob 的 PExtentVector 连接——大脑告诉身体"数据应该放在哪个位置",身体只负责存取数据本身。
这种架构的好处是:
- 原子性:所有元数据变更在 RocksDB 事务中完成
- 一致性:layout、CRC、空间分配记录始终同步
- 性能:数据直写裸盘不走 KV 引擎,元数据在 RocksDB 内存缓存中
- 灵活性:CRC、压缩、去重等特性都可以在元数据层独立演进,不影响数据格式
附录:源码文件索引
| 文件 | 关键内容 |
|---|---|
os/bluestore/BlueStore.cc第 70~80 行 | 9 个 KV prefix 定义 |
os/bluestore/bluestore_types.h第 52~64 行 | cnode_t(Collection 元数据) |
os/bluestore/bluestore_types.h第 70~109 行 | pextent_t(物理位置) |
os/bluestore/bluestore_types.h第 429~862 行 | blob_t(layout + CRC + 压缩 + 空洞) |
os/bluestore/bluestore_types.h第 869~892 行 | shared_blob_t(去重引用计数) |
os/bluestore/bluestore_types.h第 898~970 行 | onode_t(对象元数据) |
os/bluestore/bluestore_types.h第 1005~1021 行 | deferred_transaction_t |
os/bluestore/BlueStore.h第 1038~1090 行 | 内存中的Onode结构 |
os/bluestore/BlueStore.h第 505 行 | 内存中的Blob结构 |
os/bluestore/BlueStore.h第 393 行 | 内存中的SharedBlob结构 |
os/bluestore/BlueStore.cc第 12823 行 | _do_write_big(大写,直接写裸盘) |
os/bluestore/BlueStore.cc第 12462 行 | _do_write_small(小写,可能 deferred) |
os/bluestore/BlueFS.h第 100~103 行 | BDEV_WAL/DB/SLOW 三级设备定义 |
