为避免混乱,我做了两点取舍(都是为了"准确"):
-
不再用 FileStore 的 Journal 叙事(BlueStore 下不成立)。
-
对 BlueStore 内部写入,我按 Ceph 的真实分层表达:
- 数据面(data):裸块设备(block device)
- 元数据面(metadata):RocksDB(跑在 BlueFS 上)+ WAL
- 一致性语义 :数据写入 + 元数据原子提交(映射切换)
同时说明"为什么你会看到 WAL/日志字样"。
RBD 读写数据流(BlueStore 架构,完整版)
0. 总览:从"块"到"对象"再到"OSD 落盘"
RBD 是"块设备"语义,但 Ceph 后端是"对象存储"(RADOS)。因此任一 RBD I/O 都经历三次抽象转换:
- 应用/文件系统视角:写 LBA(块设备逻辑地址)
- *RBD 视角:切分为 rbd_data. 对象上的 offset/len(对象寻址)**
- OSD 视角:把对象写落到裸块设备 extents,并更新 RocksDB 元数据映射(BlueStore)
阶段一:客户端节点内部(从应用到 RBD 块设备)
1. 应用程序层
- 应用调用
read()/write(),数据在用户态 buffer(字节流)。
2. VFS + 本地文件系统(ext4/xfs)
write()进入内核,VFS 找到挂载点对应的文件系统。- 文件系统完成 inode/extent 等元数据处理,并把"文件偏移"变成"底层块设备的逻辑块地址(LBA)"。
- 数据通常先进入 Page Cache(脏页)。非同步写常在这里返回(除非 fsync/O_DIRECT 等)。
3. 通用块层(Generic Block Layer)
-
I/O 合并、调度,形成
struct bio:- 起始扇区(LBA)
- 长度
- page 指针
4. 进入 RBD 的两条客户端路径(你提到的 Path A / Path B)
路径 A:物理机 / 容器(内核 RBD,krbd)
- bio 进入
/dev/rbdX的内核块设备驱动(krbd)。 - krbd 把块请求映射为一个或多个对象操作(后续见阶段二)。
- 网络通信由内核 ceph 子系统(libceph)完成。
路径 B:虚拟机 / QEMU(用户态 librbd)
- QEMU 使用
librbd作为后端(例如 rbd: 协议)。 - I/O 在用户态直接进入 librbd,对象映射与网络由 librados/librbd 完成。
- 跳过内核的 krbd,但"块→对象→OSD"的逻辑完全相同。
你可以这样记:
krbd = 内核块设备实现 ,librbd = 用户态块设备实现 。两者最终都变成"对 rbd_data.* 对象的读写"。
阶段二:数据切分与位置计算(块 → 对象)
1. 对象切分(Striping / Object Mapping)
RBD 镜像按照固定参数切分为对象:
object_size:默认常见 4MB(可配置)stripe_unit/stripe_count:决定条带分布方式
很多常见场景 stripe_count=1,使映射更接近"按 object_size 切块"。
对一个块写请求(镜像内偏移 offset,长度 len):
object_index = offset / object_sizeobj_offset = offset % object_size- 对象名:
rbd_data.<image_id>.<object_index(16进制补齐)>
若一次写跨越 object 边界,则拆成多个对象写(每个对象内是一段连续 offset/len)。
这一层就是你之前读到的
Striper::file_to_extents()思想:
把连续逻辑范围切成"每对象最多一个连续 extent"的对象请求集合。
阶段三:对象定位与网络发送(Objecter + CRUSH)
1. Objecter 计算 PG
-
对象名 hash 后映射到 PG:
pgid = hash(object_id) % pg_num(概念化表达,实际还有更多参数与哈希规则)
PG 是数据放置与复制的基本单位。
2. CRUSH 计算 OSD 列表
-
Objecter 持有 OSDMap(集群拓扑与状态)。
-
使用 CRUSH 算法,将 PG 映射为一组 OSD(有序):
- Primary OSD(协调者)
- Replica OSDs(副本)
3. 发送请求
- 客户端(krbd+libceph 或 librbd/librados)直接连接 Primary OSD 发送 MOSDOp,不经过 MON 代理。
阶段四:OSD 内部处理与复制(Primary/Replica 协调)
1. Primary OSD 接收与排队
-
OSD 收到对象写请求,交由对应 PG 处理。
-
PG/ReplicatedBackend 决定:
- 写入顺序
- 复制策略
- 何时 ACK(与
min_size/size、配置、故障状态有关)
2. 并发复制
- Primary 将同一对象操作转发给所有 Replica OSD。
- Replica OSD 执行本地写入并返回 ACK 给 Primary。
3. 客户端 ACK
- Primary 收到满足策略要求的 ACK 后,向客户端返回成功。
- 注意:ACK 的严格语义与配置相关(例如是否要求落到稳定存储再 ack),但在 BlueStore 下"稳定存储"的判定不再是"Journal 写成功"。
阶段五:OSD 后端持久化(BlueStore:数据面 + 元数据面)
这是你原文中"Journal/XFS"最需要纠正的地方。BlueStore 下的正确结构是:
1. BlueStore 的三块组成
(1) 数据面:裸块设备(block device)
- 对象数据不存文件系统,不存在
rbd_data.*的"对象文件"。 - BlueStore 通过 Allocator 分配物理 extents(offset+len),将数据写入这些 extents。
(2) 元数据面:RocksDB(跑在 BlueFS 上)
-
对象的元数据(非常关键)存 RocksDB:
- object → extents 映射(extent map)
- 对象 size、attrs、omap(键值)
- checksum 记录
- deferred/free 等状态
-
RocksDB 需要一个"文件接口",但 BlueStore 不用 XFS,因此引入 BlueFS。
(3) BlueFS:为 RocksDB/WAL 提供"轻量文件系统"
-
BlueFS 是 BlueStore 内建的轻量 FS,专门承载:
- RocksDB 的 SST 文件
- RocksDB WAL
- 以及少量 BlueStore 自身元数据文件
-
BlueFS 本质上仍然是"在裸块设备上管理少量文件/extent",目标是高性能、低开销。
2. BlueStore 写入到底怎么"安全落盘"?
你需要记住一句话(这是 BlueStore 时代的核心):
数据先写到最终 extents,随后 RocksDB 原子提交新的映射;
一致性靠"映射切换",而不是 FileStore 的 Journal 回放。
把它展开成步骤:
-
Allocator 分配 extents(通常为新版本数据分配新空间;覆盖写也常是"写新空间+改映射")
-
数据写入块设备 extents(可能压缩/加密,并生成 checksum)
-
RocksDB 原子提交元数据(extent_map/size/checksum/omap 等)
-
崩溃恢复时:
- 若 DB 提交未完成:新数据不可见,旧映射仍生效
- 若 DB 提交完成:新映射生效,数据可按映射读取并校验
3. "为什么你会看到 WAL/日志字样"?(纠正你原文里的"Journal"残影)
在 BlueStore 下你可能会听到"WAL/日志",但它不是 FileStore Journal:
- RocksDB WAL:保证 DB 更新原子性/可重放(主要是元数据)
- BlueFS 也有自己的元数据一致性机制(为 DB 文件提供持久与一致)
这类"日志"承担的是:
数据库/元数据层的一致性
而不是:
对象数据先写 Journal 再 flush 到后端盘
读路径补全(对称理解)
客户端侧
- read() → VFS/FS → bio → krbd/librbd
- 做块→对象映射,形成对 rbd_data.* 的对象读
OSD/BlueStore 侧
- RocksDB 查 extent_map(逻辑 offset → 物理 extents)
- 从块设备读取对应 extents
- 校验 checksum(必要时触发修复/重读副本)
- 拼接返回对象数据 → 客户端再拼成块数据返回给应用
关键差异总结(你写报告时建议放一段)
1) krbd vs librbd(客户端差异)
- krbd:内核块设备 + 内核 libceph 发包
- librbd:用户态库 + librados 发包(QEMU 常用)
但二者的对象映射/CRUSH/复制语义一致。
2) FileStore vs BlueStore(后端差异)
- FileStore:Journal + XFS 对象文件
- BlueStore:裸块设备数据 extents + RocksDB(BlueFS) 元数据
BlueStore 没有"先写 Journal 再 flush 到 XFS"的流程。
你第二段文字里需要特别纠正/保留的点(已在整合版中正确处理)
✅ 保留并正确化的点
- Path A/Path B(krbd vs QEMU/librbd)------保留且强化为"客户端两实现"
- BlueFS 用于承载 RocksDB ------保留且明确职责
- "对齐大块 IO 直接写数据区"这一思路 ------保留为"数据面直接写 extents + 元数据更新"
- "小 IO 与 WAL/DB 关系"------改写为"元数据事务依赖 RocksDB WAL",避免误导为"数据先写 WAL"
❌ 彻底删除的旧点
- "OSD 先写 Journal(含数据本身)"
- "Journal flush 到后端文件系统"
- "对象最终以文件形式存在 XFS 目录"
可直接附带的简化流程图(文本版)
写路径
App write()
→ FS/PageCache → bio
→ krbd 或 librbd
→ (block→object mapping)
→ Objecter: PG + CRUSH
→ send to Primary OSD
→ Primary forward to Replicas
→ each OSD: BlueStore allocate extents → write data → RocksDB commit mapping
→ replicas ACK → primary ACK client
读路径
App read()
→ FS/PageCache miss → bio
→ krbd 或 librbd
→ (block→object mapping)
→ Objecter: PG + CRUSH
→ Primary OSD
→ BlueStore: RocksDB lookup mapping → read extents → checksum → assemble
→ return to client → assemble → app buffer
如果你希望我把这份"整合版"进一步升级成更像教材的版本,我建议下一步做两件事(我也可以直接给你产出):
- 在"阶段五 BlueStore"里加入 block.db / block.wal / block 三设备部署模式(非常常见的考试点/面试点)
- 增加"ACK 语义"一段:解释 applied / committed / on_applied_sync 在 OSD/BlueStore 中大概对应什么阶段(你之前贴的 bluestore.cc 正好能对上)
你希望我优先补哪一个?