本文基于 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 的 WALWrite-Ahead Log可以写到更快的小盘NVMeSST 文件写到中等速度的盘溢出部分再写到慢盘——三级存储分层按速度和成本分配。而用户数据对象的 payload则完全绕过 RocksDB通过 AIO 直接写到主数据裸盘block设备只有数据在裸盘的物理位置这条元数据才进 RocksDB。二、元数据的分类9 个 Key PrefixBlueStore 把 RocksDB 的 key 按前缀prefix分成 9 个 namespace每个 prefix 存不同类型的元数据。源码定义在BlueStore.cc第 70~80 行conststring PREFIX_SUPERS;// field - valueconststring PREFIX_STATT;// field - value(int64 array)conststring PREFIX_COLLC;// collection name - cnode_tconststring PREFIX_OBJO;// object name - onode_tconststring PREFIX_OMAPM;// u64 keyname - valueconststring PREFIX_PGMETA_OMAPP;// u64 keyname - value(for meta coll)conststring PREFIX_DEFERREDL;// id - deferred_transaction_tconststring PREFIX_ALLOCB;// u64 offset - u64 length (freelist)conststring PREFIX_ALLOC_BITMAPb;// (see BitmapFreelistManager)conststring PREFIX_SHARED_BLOBX;// u64 offset - shared_blob_t下面逐个详细解析。三、SuperS—— 全局配置信息key: S field_name value: 全局字段值这是 BlueStore 的 superblock存 OSD 级别的全局配置信息FSIDCeph 集群 IDOSD UUID该 OSD 的唯一标识last_seq最后的事务序列号类似传统文件系统的 superblock在 OSD 启动时读取用于验证设备归属和恢复状态。四、StatT—— 统计信息key: T field_name (如 bluestore_statfs) value: int64 数组存 OSD 级别的空间统计信息store_statfs_t包括已用空间大小可用空间大小对象总数这些数据会定期更新对外通过ceph osd df命令展示。本质上是这个 OSD 的磁盘空间用了多少、还剩多少的底层记录。五、CollectionC—— 集合元数据key: C collection_name value: bluestore_cnode_t { bits }Collection 是 PGPlacement Group的等价概念。cnode_t的结构极其精简bluestore_types.h第 52~64 行structbluestore_cnode_t{uint32_tbits;/// how many bits of coll pgid are significant};只有一个字段bitsPGID 的有效哈希位数。它控制了 PG 的分裂粒度——当 PG 数量增长时bits 增大一个 Collection 可以分裂成更细的子集合。Collection 虽然元数据很小但它是对象组织的容器所有对象都挂在一个 Collection 下面读写对象时先定位 Collection。六、OnodeO—— 对象元数据最核心的部分key: O collection_hash object_hash value: bluestore_onode_t这是 BlueStore 中最重要的元数据类型类似文件系统的 inode。源码定义bluestore_types.h第 898~970 行structbluestore_onode_t{uint64_tnid0;/// numeric id (locally unique)uint64_tsize0;/// object sizestd::mapstring,buffer::ptrattrs;/// xattrsstructshard_info{uint32_toffset0;/// logical offset for start of sharduint32_tbytes0;/// encoded bytes};vectorshard_infoextent_map_shards;/// extent map shards (if any)uint32_texpected_object_size0;uint32_texpected_write_size0;uint32_talloc_hint_flags0;uint8_tflags0;// FLAG_OMAP 1, FLAG_PGMETA_OMAP 2};逐字段解释字段含义nid对象数字 IDOSD 内唯一用于 omap key 的前缀size对象的逻辑大小字节attrs对象的扩展属性xattr——键值对存用户/系统属性。类似文件系统的 xattrextent_map_shardsextent map 分片索引。小对象 inline 在 onode 里大对象分片存储expected_object_size分配提示上层期望的对象大小RBD/CephFS 传来expected_write_size分配提示期望的写大小alloc_hint_flags分配提示标志如SEQUENTIAL、RANDOMflagsFLAG_OMAP 表示有 omap 数据注意onode 的 key 是collection_hash object_hash的组合不是对象名本身。这样做可以高效地按 Collection 扫描所有对象RocksDB 的 key 是有序的。内存中Onode 还关联一个ExtentMapBlueStore.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_length0;/// 逻辑长度uint32_tcompressed_length0;/// 压缩后长度uint32_tflags0;/// FLAG_COMPRESSED/CSUM/SHARED/HAS_UNUSEDuint8_tcsum_typeChecksummer::CSUM_NONE;/// 校验和类型uint8_tcsum_chunk_order0;/// chunk 大小 1 orderbufferptr csum_data;/// 校验值数组真正的 CRC 数据unused_t unused0;/// 未使用区域位图};关键字段详解extentsPExtentVector—— 对象的 layout 信息structbluestore_pextent_t:publicbluestore_interval_tuint64_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 多大比如 order12 → chunk4096 字节csum_data是一个紧凑的数组每csum_value_size字节存一个 chunk 的校验值。这就是你问的CRC 信息——它也存在 RocksDB 里不在裸盘数据块旁边flags —— Blob 的特征标记Flag值含义FLAG_COMPRESSED2Blob 数据被压缩了compressed_length有效FLAG_CSUM4Blob 有校验和csum_*字段有效FLAG_HAS_UNUSED8Blob 有未写入区域unused位图有效FLAG_SHARED16Blob 被多个对象共享去重场景见 SharedBlobunused —— 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 来嵌入元数据头。八、OMAPM—— 对象的 Key-Value 附表key: M onode_nid user_key value: user_valueOMAP 是 BlueStore 提供的对象级 key-value 存储接口。它不同于 xattrxattr 数量少、值小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 OMAPP—— PG 级别的元数据key: P onode_nid key value: valuePGPlacement Group级别的状态信息使用特殊的 omap prefixP存储与普通对象的 omapM分开。存的是PG infoPG 的状态、last_update、last_complete 等PG logPG 的操作日志用于恢复和一致性检查PG meta 对象如meta、osdmap.NNN是 OSD 内部的系统对象不在用户数据空间里。十、Deferred TransactionL—— 延迟写事务key: L sequence_id value: bluestore_deferred_transaction_t源码定义bluestore_types.h第 1005~1021 行structbluestore_deferred_transaction_t{uint64_tseq0;listbluestore_deferred_op_tops;interval_setuint64_treleased;/// allocations to release after tx};什么时候会产生 deferred transactionBlueStore 的写分两种路径_do_write_big大写对齐到 min_alloc_size直接在裸盘上分配新空间写入_do_write_small小写不对齐的小块覆盖写需要做 read-modify-write 或 deferred小块覆盖写无法原地更新因为 min_alloc_size 的限制BlueStore 选择把小块写记录为 deferred transaction先存到 RocksDB后续异步执行到裸盘上。这样做的好处是小写不需要立即等裸盘 IO 完成先记录意图后续批量执行。十一、AllocationB/b—— 裸盘空间管理key: B offset (BlockFreelistManager) key: b bitmap_data (BitmapFreelistManager) value: length / bitmapBlueStore 不依赖宿主机文件系统自己管理裸盘空间的分配和回收。空闲列表有两种实现BlockFreelistManagerprefix Bkey 是 offsetvalue 是 length。简单但空间效率低BitmapFreelistManagerprefix b用 bitmap 紧凑编码空间效率高是新版本的默认实现这个 prefix 记录的是裸盘上 offset X 处有 length Y 的空闲空间。当需要为新数据分配空间时从空闲列表中找到合适的区间当数据被删除时释放的区间重新加回空闲列表。所有分配和释放操作都在 RocksDB transaction 中完成保证数据写入 空间标记的原子性。十二、Shared BlobX—— 去重引用计数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 的对象到 OSDBlueStore 的内部流程如下1. 收到写请求 → 创建 TransContext事务上下文 2. 分配裸盘空间 - 从 Allocation 空闲列表中找到空闲区间 - 记录分配意图到 RocksDB transaction 3. 写裸盘数据AIO - 把 4MB 数据直接写到主数据裸盘的 offset0x10000000 - 等待 AIO 完成 4. 更新 RocksDB 元数据同一个 transaction - PREFIX_OBJ(O)创建/更新 onode设置 size4MB - 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 Mapinline 或 shard → 得到每个逻辑偏移对应的 Blob 3. 从 Blob 的 PExtentVector 得到裸盘物理位置 → pextent {offset0x10000000, length4MB} 4. 从裸盘直接 AIO 读数据 → 读 offset 0x100000004MB 5. 校验 CRC可选 → 从 Blob.csum_data 取每个 chunk 的校验值 → 与读到的数据计算值比对可以看到读路径非常高效——先查 RocksDB 拿 layout通常在内存 cache 中然后一次 AIO 读裸盘。CRC 校验用 RocksDB 里的值不需要额外读裸盘。十五、元数据全景总结Prefix存储内容作用类比SSuperblockFSID, UUID, seq文件系统 superblockTStat空间统计df的底层数据CCollectionPG 分裂 bits目录的 inodeOOnode对象大小、xattr、flags文件的 inodeOshardExtent Map → Bloblayout CRC 压缩文件的 extent tree block mapMOMAP对象的 KV 附表文件的 xattr海量版PPG Meta OMAPPG info/log文件系统的 journalLDeferred Transaction延迟写文件系统的延迟写日志B/bAllocation空闲空间管理文件系统的 free block bitmapXShared 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_tCollection 元数据os/bluestore/bluestore_types.h第 70~109 行pextent_t物理位置os/bluestore/bluestore_types.h第 429~862 行blob_tlayout 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_tos/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小写可能 deferredos/bluestore/BlueFS.h第 100~103 行BDEV_WAL/DB/SLOW 三级设备定义