本文基于 Ceph 源码os/bluestore/、osd/深度剖析 BlueStore 的对象存储模型、数据分层结构、CRC 校验机制以及为什么 CRC 被存在 RocksDB 而不是数据块头部。一、整体架构概览Ceph 的存储栈从上到下分为以下几层┌─────────────────────────────────────────────────┐ │ ClientRBD / CephFS / RGW │ ├─────────────────────────────────────────────────┤ │ RADOS 层 │ │ (副本管理、一致性、PG、Recovery、Scrub 等) │ ├─────────────────────────────────────────────────┤ │ ObjectStore 抽象层 │ │ (BlueStore / FileStore 接口) │ ├─────────────────────────────────────────────────┤ │ BlueStore │ │ ┌──────────────┐ ┌───────────────────────┐ │ │ │ RocksDB │ │ 裸块设备Raw Block │ │ │ │ (元数据CRC) │ │ (对象数据) │ │ │ │ BlueFS 管理 │ │ │ │ │ └──────────────┘ └───────────────────────┘ │ └─────────────────────────────────────────────────┘关键点RADOS 使用 BlueStore而不是 BlueStore 基于 RADOS。BlueStore 是 OSD 的本地存储后端通过ObjectStore抽象接口与上层 RADOS 解耦可以替换为 FileStore 等其他实现。二、RADOS 对象大小RADOS 对象没有固定大小但受配置项约束// common/options.ccOption(osd_max_object_size,Option::TYPE_SIZE,Option::LEVEL_ADVANCED).set_default(128_M)// 默认最大 128MB实际上上层应用通常会主动切分对象上层应用默认切分大小RBD块设备4MB / 对象CephFS文件系统4MB / 对象RGW对象存储按 multipart 切分单 part 通常 ≤ 5GB三、BlueStore 数据分层模型BlueStore 引入了一套精密的数据分层抽象理解这个层次是读懂 BlueStore 的关键。3.1 层次结构RADOS Object如 4MB │ ├── Onode对象元数据存 RocksDB │ ├── size, nid, attrs │ └── extent_map_shards指向 ExtentMap │ └── ExtentMap逻辑偏移 → Blob 映射 ├── Extent [0, 64K) → Blob 0 ├── Extent [64K, 128K) → Blob 1 ├── ... └── Extent [3968K, 4096K) → Blob 63 │ └── Blob物理数据单元元数据存 RocksDB数据存裸盘 ├── pextents: 物理磁盘偏移 长度 ├── csum_type: crc32c ├── csum_chunk_order: 12即 4KB └── csum_data: [chunk0_crc, chunk1_crc, ...]3.2 Blob数据的物理载体bluestore_blob_t是 BlueStore 最核心的数据结构// os/bluestore/bluestore_types.hstructbluestore_blob_t{PExtentVector extents;/// 物理磁盘上的位置offset lengthuint32_tlogical_length;/// 原始数据长度uint32_tcompressed_length;/// 压缩后长度如启用压缩uint8_tcsum_type;/// 校验类型crc32c / xxhash 等uint8_tcsum_chunk_order;/// 校验粒度1 order 字节bufferptr csum_data;/// 每个 chunk 的校验值数组// ...};Blob 的大小由bluestore_max_blob_size控制设备类型默认 max_blob_sizeHDD机械盘512 KBSSD固态盘64 KB因此一个4MB 的 RADOS 对象在 SSD 上会被拆成约64 个 64KB 的 BlobHDD 则约 8 个 512KB Blob。3.3 csum chunkBlob 内的校验粒度每个 Blob 内部数据被按csum_chunk_size 1 csum_chunk_order进一步分块每块存一个 CRC 值。bluestore_csum_type默认为crc32c。以一个 64KB Blob、4KB chunk 为例Blob (64KB) ├── chunk[0] [0, 4K) → crc32c 值4 bytes ├── chunk[1] [4K, 8K) → crc32c 值 ├── chunk[2] [8K, 12K) → crc32c 值 ... └── chunk[15] [60K, 64K) → crc32c 值csum_data就是这 16 个 crc32c 值的连续数组共 64 bytes。Blob 和 csum chunk 的关系Blob IO 分配单元对应磁盘上一段连续的物理空间csum chunk Blob 内部的校验粒度用于 bit-rot 检测二者是包含关系csum chunk 是 Blob 的内部细分不是同一个东西四、CRC 的两个层次Ceph 的 CRC 校验分为两个完全独立的层次层次一OSD 层 ——object_info_t中的 data_digest// osd/osd_types.hstructobject_info_t{// ...__u32 data_digest;/// 整个对象数据的 crc32copportunistic可选__u32 omap_digest;/// omap 数据的 crc32c// ...};由 PrimaryLogPG 在写操作完成后计算覆盖整个对象用于Scrub后台一致性检查和副本对比存储在对象的 xattrsnapinfo键中持久化到 BlueStore onode 的attrs字段层次二BlueStore 层 —— Blob 内的 csum_data由 BlueStore 在写入时计算粒度为每个 csum chunk通常 4KB用于读时验证每次读取数据时逐 chunk 验证 CRC防止静默数据损坏bit rot存储在 RocksDB 的 Blob 元数据中随对象 extent map 一起持久化维度data_digestOSD 层Blob csumBlueStore 层粒度整个对象每 4KB chunk用途Scrub、副本校验读时 bit-rot 检测存储位置onode attrs (RocksDB)Blob 元数据 (RocksDB)触发时机OSD 写完整对象后BlueStore 每次写 Blob五、Onode 存储在哪里Onode 存储在 RocksDB 中key 前缀为O// os/bluestore/BlueStore.ccconststring PREFIX_OBJO;// object name - onode_t读取时intrstore-db-get(PREFIX_OBJ,key.c_str(),key.size(),v);写入时txn-set(PREFIX_OBJ,o-key.c_str(),o-key.size(),bl);Onode 在内存中有 LRU Cache热对象不需要每次都查 RocksDB。BlueStore 的 RocksDB 使用BlueFSCeph 自己实现的轻量文件系统管理可以放在单独的 NVMe/SSD 设备上与对象数据盘分离实现元数据 IO 和数据 IO 的隔离。六、数据写入 IO 路径Client 写请求 │ ▼ Primary OSD (PrimaryLogPG) │ ① 生成 OpContext计算 data_digest如全量写 │ ② 调用 ObjectStore::Transaction ▼ BlueStore::queue_transactions() │ ▼ _txc_add_transaction() │ ③ 查找/创建 Onode从 RocksDB 或内存 cache │ ④ 调用 _write() ▼ _do_write() │ ⑤ 按 blob 边界切分写入区域 │ ⑥ 对每个 Blob 分配物理空间Allocator │ ⑦ 计算 csum_datacrc32c per 4KB chunk │ ⑧ 更新 ExtentMap逻辑偏移 → Blob 映射 ▼ _txc_write_nodes() │ ⑨ 将 onode含 csum_data序列化写入 RocksDB batch ▼ _txc_finalize_kv() │ ⑩ 提交 RocksDB WAL元数据原子落盘 │ ⑪ 异步将数据写入裸块设备aio ▼ 写完成回调 → 通知 Primary OSD → 副本确认 → 响应 Client关键设计数据写裸盘aio元数据写 RocksDB WAL两者通过TransContext协调原子提交。七、为什么 CRC 存在 RocksDB 而不是块头部这是 BlueStore 设计中最值得思考的问题之一。7.1 BlueStore 没有块头部——这是刻意的设计BlueStore直接写裸块设备不在数据前加任何 header。这与传统存储如 FileStore 走 ext4/xfs截然不同。如果在每个 4KB 数据块前加一个包含 CRC 的 header如 ZFS 的做法则读写不对齐数据 4KB header 若干字节导致物理 block 不对齐引入 read-modify-write写放大每次小 IO 都要额外写 header碎片化连续的逻辑数据在物理上变得不连续7.2 RocksDB 存 CRC 的优势写入流程对比 方案A块头部存 CRC 数据(4KB) Header(CRC) → 写盘 ↑ 如果不对齐先读旧块 → 修改 → 写回RMW 方案BRocksDB 存 CRCBlueStore 的做法 数据(4KB) ──────────────────────→ aio 直写裸盘对齐无 RMW CRC(4B) → RocksDB WAL batch ──→ 顺序写高效 └── 与 onode 原子提交维度块头部存 CRCRocksDB 存 CRC写对齐可能破坏对齐引入 RMW数据写完全对齐原子性数据和 CRC 分两次写存在窗口WAL 保证原子提交读 CRC 开销需要额外磁盘 IOonode 已在内存 cache元数据管理分散在数据盘各处集中在 RocksDB便于管理损坏定位头部损坏则无法读取数据数据和元数据独立可分别恢复7.3 原子性保证这是最关键的一点。BlueStore 通过TransContext将一次写操作的数据写盘和元数据含 CRC写 RocksDB绑定在一起利用 RocksDB 的 WAL 机制保证原子语义不会出现数据写盘了但 CRC 没更新读时误报校验错误不会出现CRC 更新了但数据没落盘数据实际是旧的7.4 读时验证效率读取一个 Blob 时BlueStore 的流程① onode 在内存 LRU cache → 直接拿到 blob 的 csum_data无磁盘 IO ② aio 读取数据到 buffer ③ 逐 chunk 计算 crc32c与 csum_data 中存储的值对比 ④ 不匹配 → 返回 -EIO上层触发副本恢复因为 CRC 随 onode 缓存在内存验证过程几乎零额外 IO 开销。八、完整架构图┌─────────────────────────────────────────────────────────────────┐ │ Ceph OSD 进程 │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ PrimaryLogPG │ │ │ │ object_info_t { data_digest (整对象 crc32c) } │ │ │ └───────────────────────┬─────────────────────────────────┘ │ │ │ ObjectStore::Transaction │ │ ┌───────────────────────▼─────────────────────────────────┐ │ │ │ BlueStore │ │ │ │ │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ │ │ 内存层LRU Cache │ │ │ │ │ │ OnodeCache: Onode → ExtentMap → Blob │ │ │ │ │ └──────────────────────┬───────────────────────────┘ │ │ │ │ │ │ │ │ │ ┌──────────────────────▼──────────┐ ┌──────────────┐ │ │ │ │ │ RocksDBBlueFS 管理 │ │ 裸块设备 │ │ │ │ │ │ │ │ │ │ │ │ │ │ PREFIX_OBJ O → │ │ Blob 数据 │ │ │ │ │ │ bluestore_onode_t │ │ 4KB对齐 │ │ │ │ │ │ ├── size, attrs │ │ │ │ │ │ │ │ └── extent_map_shards │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ PREFIX_OBJ O (extent shard)→ │ │ │ │ │ │ │ │ bluestore_blob_t │ │ │ │ │ │ │ │ ├── pextents (磁盘偏移) │◄─┼──────────────┘ │ │ │ │ │ ├── csum_type: crc32c │ │ │ │ │ │ │ └── csum_data: [crc...] │ │ │ │ │ │ │ │ │ │ │ │ │ │ PREFIX_OMAP M → omap 数据 │ │ │ │ │ │ └─────────────────────────────────┘ └─────────────────┘ │ │ └──────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────────┘九、总结概念本质存储位置RADOS Object逻辑对象最大 128MB实际多为 4MB-Onode对象元数据size、attrs、extent 映射RocksDBO前缀Blob物理数据分配单元SSD 64KBHDD 512KB裸块设备数据 RocksDB元数据csum chunkBlob 内校验粒度4KBRocksDB随 Blob 元数据存储data_digest整个对象的 crc32cOSD 层RocksDB onode attrsomap对象 key-value 扩展属性RocksDBM前缀BlueStore 将 CRC 存在 RocksDB 的根本原因裸盘直写无块头部开销避免 RMW 写放大RocksDB WAL 保证数据与 CRC 的原子一致性onode LRU cache 让读时 CRC 验证几乎零开销元数据集中管理便于 Scrub、修复和统一的故障恢复参考源码os/bluestore/bluestore_types.h—bluestore_blob_t,bluestore_onode_tos/bluestore/BlueStore.h—Onode,Blob,Extent,ExtentMapos/bluestore/BlueStore.cc—PREFIX_OBJ,get_onode,_do_writeosd/osd_types.h—object_info_t,data_digestcommon/options.cc—osd_max_object_size,bluestore_max_blob_size