RBD(RADOS Block
Device)对外暴露一个块设备接口,对内是一组写入 RADOS
对象池的固定大小对象序列。理解它的核心问题不是「如何
rbd map」,而是:一次 4K 写在 image
上的偏移如何命中哪个 RADOS 对象,krbd 和 librbd
的一致性边界在哪,快照 COW
的触发条件与克隆父子链的代价是什么。
本文是「Ceph / RADOS 存储内核」系列第 12 篇(共 18 篇)。→ 系列目录
篇目 核心内容 第 11 篇 · EC 路径 ECBackend、整条带 vs RMW 第 12 篇 · RBD image→对象、krbd vs librbd、快照/克隆边界 第 13 篇 · CephFS / MDS 可恢复 MDS、caps、subtree thrashing
版本锚定:Ceph Squid v19.2.5(
docs.ceph.com/en/squid/rbd/;源码路径src/librbd/@ v19.2.5)。
一、image 与 RADOS 对象的映射关系
RBD image 格式 2(Format 2,自 Bobtail/0.56 起为默认)将一块连续的虚拟磁盘空间切成若干等大小的 RADOS 对象。格式 1 已弃用;本文默认格式 2。
对象大小与命名
image 的对象大小由 --order 参数决定,默认
22,即 \(2^{22} =
4\,\text{MiB}\)。创建时可通过
--object-size 显式指定字节数(须为 2
的幂次,范围 4KiB–32MiB)。
每个 Format 2 image 创建时分配唯一
image_id(短十六进制串)。rbd info
输出中的 block_name_prefix
即数据对象前缀,常规形态为
rbd_data.{image_id}(Squid 文档与
rbd man page 口径;见
docs.ceph.com/en/squid/rbd/)。数据对象完整名为:
{block_name_prefix}.{object_number:016x}
object_number 从 0 起计,固定 16
位小写十六进制、左侧补零。例如
block_name_prefix: rbd_data.15452f1b586be、对象大小
4MiB 时:
rbd_data.15452f1b586be.0000000000000000
rbd_data.15452f1b586be.0000000000000001
rbd_data.15452f1b586be.0000000000000002
元数据侧:rbd_header.{image_id} 以 RADOS
对象 + OMAP/xattr
持有大小、order、features、stripe 参数与 snap
列表;池内名→id 映射在 rbd_directory
OMAP。客户端从不靠「猜对象名」寻址——先读 header 拿到
block_name_prefix 与
order,再用偏移公式算序号。命名一旦写错(手工拼对象名、漏补零),会落在错误对象标识上,表现为「静默空洞」而非明确报错。运维核对应以
rbd info 的 block_name_prefix
为准,不要凭记忆拼前缀。生产排障时优先核对前缀与对象序号宽度,再怀疑归置组。
偏移到对象的换算
给定写入偏移 \(\text{off}\) 和 image 对象大小 \(S = 2^{\text{order}}\):
\[\text{obj\_seq} = \lfloor \text{off} / S \rfloor, \quad \text{obj\_off} = \text{off} \bmod S\]
若开启了 striping(stripe_unit < \(S\),stripe_count
> 1),换算稍复杂:先将 \(\text{off}\) 按
stripe_unit 切成 strip,再按
stripe_count
交叉分配到不同对象序号,然后对象内偏移再按
stripe_unit 对齐。Striping
的意义在于将顺序写分散到更多 RADOS 对象,从而利用更多 OSD
并行写,但代价是元数据更复杂,且
stripe_unit * stripe_count 必须 ≤
对象大小。实际部署中,若后端是 SSD 集群且 PG
数量足够,striping 带来的收益有限,多数情况保持默认(无额外
striping)。
image
元数据对象布局(来源:src/librbd/image/Types.h,src/librbd/
@ v19.2.5)
| RADOS 对象 | 存储内容 |
|---|---|
rbd_directory(OMAP) |
池内所有 image 名 → image_id 映射 |
rbd_header.{image_id} |
image 大小、order、features、stripe 参数、snap 列表 |
rbd_info |
池级别 RBD 配置 |
rbd_trash(OMAP) |
已移入垃圾箱的 image |
rbd_children(旧) |
Format 1 的 parent→child 索引(Format 2 已迁到 header) |
二、krbd:内核驱动路径
驱动架构
krbd(内核 RBD 驱动)位于 Linux 内核
drivers/block/rbd.c,自 3.8
引入,持续维护至今。它依赖
libceph(net/ceph/)建立 messenger
连接、处理 CRUSH 路由与 OSD Op 发送。
flowchart LR
blkq["block queue\n(/dev/rbdN)"] --> rbd["rbd.ko\n(drivers/block/rbd.c)"]
rbd --> libceph["libceph\n(net/ceph/)"]
libceph --> mon["MON\nmsgr2"]
libceph --> osd["OSD cluster\nmsgr2"]
用户通过
rbd device map {pool}/{image}[@snap] 将 image
挂为
/dev/rbdN,然后像普通块设备一样格式化、挂载。内核收到
bio 请求后,rbd.c
将其按对象边界拆分,对每个对象生成 OSD op(读 →
CEPH_OSD_OP_READ,写 →
CEPH_OSD_OP_WRITE),经 libceph 发送到对应
Primary OSD。
一致性与 Exclusive Lock
Exclusive
Lock(exclusive-lock feature)是 RBD
层的客户端互斥,不是 OSD 层的 advisory lock。Squid
文档(docs.ceph.com/en/squid/rbd/rbd-exclusive-locks/)钉死三点:
- 保护对象:保证同一时刻只有一个客户端在写
image,从而保护 object-map、journal、PWL cache
等内部结构不被并发改坏。新建 image 默认开启;可用
rbd_default_features或--image-shared覆盖。 - 协作转移(默认):写方先抢锁;若锁在别的客户端手上,请求对方停写、flush
缓存后释放。默认模式不会禁止多客户端「轮流写」——只是把写线性化。要禁止自动转移,需
rbd device map --exclusive(对应RBD_LOCK_MODE_EXCLUSIVE)。 - 非优雅退出 →
blocklist:持锁客户端硬杀/掉电时,新写方不能直接抢锁;它与
Monitor 协商,先把旧客户端
blocklist(fencing),等 OSDMap
生效后再破锁。缺少
osd blocklist能力(profile rbd含此 cap)时,破锁路径会失败。
Exclusive Lock 不兼容
rbd lock add / rbd lock rm 那套
advisory
lock。object-map、fast-diff、journaling
都依赖它;关掉 exclusive-lock
会连带拖垮这些特性的正确性与性能假设。
krbd map 另有 features 闸门:内核若未实现
journaling 等,rbd device map
直接失败。核对:
rbd info {pool}/{image}krbd 的适用场景
- 需要
/dev/块设备接口(例如无 QEMU 的宿主机卷)。 - 依赖 OS page cache,不走 librbd 写回缓存。
- 多客户端写必须接受 Exclusive Lock 的协作语义,或显式
--exclusive独占。
局限:与 librbd 的 live-migration / journaling 协议深度不同;高 features image 在旧内核上可能无法 map。
三、librbd:用户态库路径
架构
librbd(src/librbd/ @ v19.2.5)是一个 C/C++
库,直接调用 librados API 操作 RADOS
对象,不经过内核块层。QEMU 通过 block/rbd.c
调用 librbd,完成虚机块 I/O 的完整路径:
flowchart LR
vm["VM (KVM/QEMU)"] --> qrbd["QEMU block/rbd.c"]
qrbd --> librbd["librbd"]
librbd --> librados["librados"]
librados --> osd["OSD cluster"]
librbd 在用户态内部维护了多个并发 I/O
path(ImageRequestWQ、异步
AIO),支持深度队列,能更充分地利用 OSD 并行性。QEMU
侧还可叠加写回缓存(rbd_cache=true),在 guest
crash 安全性与吞吐之间权衡。
librbd 的关键状态机
librbd 内部用
ImageCtx(src/librbd/ImageCtx.h)持有
image 的打开状态,包括:
snap_lock:读写锁,保护 snap 列表与 snap_id 映射object_map(若 feature 开启):位图,标记哪些 data 对象已存在,避免对空白区域发 RADOS readexclusive_lock:若 feature 开启,对应ExclusiveLock状态机(src/librbd/exclusive_lock/)journal:若 feature 开启,journal 写入在 OSD write 之前进行,供 RBD mirror 复制
librbd 读写时判断逻辑: 1. 写:若开启 object map,先标记目标对象为”存在”,再发 RADOS write。 2. 读:若开启 object map 且对象标记为”不存在”(即该区域从未写过),直接返回全零,不发 RADOS read。
Object map 的代价是:每次写首次覆盖新对象时,需额外更新 object map bitmap(一次 RADOS OMAP op),并持有 object map 锁。大量随机小写在首次写入时略有额外延迟;若关闭 object map,每次读都要向 OSD 实际发请求(即使对象不存在,OSD 返回 ENOENT 后 librbd 填零)。
四、快照:COW 边界与失败模式
快照的 RADOS 模型(COW 落在 PrimaryLogPG)
RBD
快照创建本身几乎不复制数据:rbd snap create
只在 rbd_header.{image_id} 追加 snap
元数据,并刷新客户端 SnapContext(有序 snap_id
列表)。真正的复制发生在之后第一次覆盖写该对象时,由
PrimaryLogPG(src/osd/PrimaryLogPG.cc
@ v19.2.5)在 OSD 侧完成。
机制链:
- 客户端每次写带上当前
SnapContext。 - Primary 查该 OID 的 snapset:若 head 仍被某一 snap
引用、且本次写会破坏「snap 可读旧内容」不变量,则先
clone/ 拆分 snapset(OSD op 语义上的 copy-on-write),再写 head。 - 同一 snap 窗口内、同一 OID 的第二次写不再 clone——snapset 已记录 head 与 snap 分离。
sequenceDiagram
participant Client
participant Primary as PrimaryLogPG
participant Store as BlueStore
Note over Client: snap@s1 已存在,首次写 OID
Client->>Primary: WRITE + SnapContext[s1,HEAD]
Primary->>Primary: 检查 snapset:HEAD 仍被 s1 引用?
alt 是(该 OID 首次写)
Primary->>Store: clone / 保留 s1 可见副本
Primary->>Store: 写 HEAD 新数据
else 否(已 COW 过)
Primary->>Store: 直接覆盖 HEAD
end
Primary-->>Client: ACK(副本路径见第 6 篇)
COW 税:存在快照时,每个 data OID
的第一次覆盖写 ≈ 读旧 +
写新(再加副本路径),随机小写尾延迟最痛;顺序大写摊薄后不那么显眼。多
snap 叠层时,snapset
更胖,snaptrim(删除快照后的异步合并,见
ceph status 中的 snaptrim)会与前台争
I/O——这是轴四(第 16 篇)的常见诱因之一。
Squid 文档另钉:RBD 不知道 image 内文件系统;未 quiesce 的快照只保证 crash-consistent。删 snap 不立刻释放容量,靠 OSD 异步 snaptrim。
快照的失败场景
- snap 过多:SnapContext 变长 + snaptrim 排队;COW 与 trim 叠加时 commit latency 抬升。
- 有 snap 时缩容再扩容:image 层边界与 RADOS OID 生命周期易错位;生产上少做。
- protect / unprotect:clone 前必须
rbd snap protect;有子 clone 未 flatten/删除时unprotect失败(Squidrbd-snapshot文档硬条件)。
五、克隆:父子链与 flatten 代价
Clone 的 RADOS 语义
Clone(rbd clone {pool}/{parent_image}@{snap} {pool}/{child_image})创建一个子
image,其 rbd_header 中含 parent
字段指向父 image 的 pool_id、image_id、snap_id 与
overlap(父子共享的字节数)。子 image
初始无任何自己的数据对象。
读取子 image 中的一个对象时:
- librbd/krbd 先检查子 image 对象池是否有该对象。
- 若无,检查父 image 对应偏移是否在
overlap范围内。 - 若在,从父 image(按快照 epoch)读取对应对象。
- 若不在(子 image 已扩容超过父 image),返回全零。
写入子 image 时,同样触发 COW:将父 image 对象复制到子 image 对象池,再写入新数据。
Clone 链的深度与 flatten
多级 Clone 形成链:grandparent → parent_snap → child → grandchild。读路径沿 parent 指针向上找「有数据的 OID」;每跨一层多一次 librados 往返(或内核 libceph 往返)。写路径在子 image 触发对父 OID 的 COW,把父 snap 可见内容落到子池后再改。
rbd flatten {pool}/{child}(Squid
rbd-snapshot):把仍依赖父 snap
的共享块全部复制进子 image,切断 parent
引用。官方口径:耗时随父快照可见数据量增加;flatten
后占用 ≥ 分层 clone(分层时只付已写脏块)。期间子 image
仍可读写,但占带宽——与扩容 backfill 同类,易拖死前台(第 16
篇轴四)。
代价模型(机制推导,非本站实测):设对象大小 \(S\)、子 image 在 overlap 内未本地化对象数为 \(N\),则 flatten 量级 \(\approx N\cdot S\) 的读父 + 写子;随机已写脏块越多,\(N\) 越小。生产上常见策略:模板池只读 snap + 实例 clone;迁移/删父前强制 flatten;CI 用后即删的 clone 可跳过 flatten。
Trash 与 deep flatten
rbd trash move 把 image 记入
rbd_trash OMAP,deferment 到期后 GC 才删
OID。父进 trash 而子仍依赖时,父 OID 未 GC 前子可读;一旦
GC,未 flatten 的子会读失败——trash 不是「免费延期
flatten」。deep-flatten feature 允许在仍有 snap
的子上递归断开更深父链;无此 feature 时须先处理中间
snap。
六、features 标志与版本兼容性
RBD Format 2 通过 features
位掩码(src/include/rbd/features.h)控制可选功能。常见标志:
| Feature | 标志名 | 说明 |
|---|---|---|
layering |
RBD_FEATURE_LAYERING |
支持 clone/snapshot COW |
exclusive-lock |
RBD_FEATURE_EXCLUSIVE_LOCK |
多客户端写互斥 |
object-map |
RBD_FEATURE_OBJECT_MAP |
位图跳过空对象读 |
fast-diff |
RBD_FEATURE_FAST_DIFF |
基于 object map 快速 diff |
deep-flatten |
RBD_FEATURE_DEEP_FLATTEN |
flatten 时递归展开整条父链 |
journaling |
RBD_FEATURE_JOURNALING |
写前日志(RBD Mirror 依赖) |
data-pool |
RBD_FEATURE_DATA_POOL |
数据对象写入独立 EC 池 |
兼容性要点:exclusive-lock 依赖
layering;object-map 和
fast-diff 依赖
exclusive-lock;journaling 依赖
exclusive-lock。较旧内核 krbd 不支持部分
features,需用 rbd feature disable 降级。Squid
v19.2.5 的新建 image 默认开启特性集可通过
rbd_default_features 配置项调节。
七、开放问题与谱系
谱系:RBD 的设计随 Ceph 从 Argonaut
版本演进,Format 2 解决了 Format 1 无法支持多级 clone
与灵活特性扩展的问题。Object map 特性(Hammer
版本引入)显著降低了稀疏 image 的读放大,来源于
src/librbd/ObjectMap 与相关 PR。RBD
Mirror(异步副本跨集群,依赖 journaling
feature)是 Jewel 版本引入的灾备机制,利用 PG journal replay
而非 Ceph OSD 层 journal。
争论:librbd 客户端写回缓存(QEMU
rbd_cache=true)在历史上因 VM crash 时未 flush
而引发数据一致性问题,社区在 Hammer
之后收紧了默认策略(rbd_cache_writethrough_until_flush=true),但
writethrough 模式下性能损失在高 IOPS
随机写场景显著。这是「性能 vs 安全」权衡在用户态 cache
层面的典型争论,未形成共识默认值,建议由应用层(数据库、QEMU
配置)显式声明。
开放问题: 1. Crimson OSD 下 RBD librbd 路径与 SeaStar 的 async 语义如何对接——当前 librbd 仍假设线程池模型,Crimson 的单线程 actor 与 librbd 并发模型存在边界问题(参见本系列第 18 篇)。 2. Object map 在超大 image(数十 TB)上 bitmap 本身变成 MiB 级 RADOS 对象,更新时 RADOS op 延迟是否成为瓶颈,社区有 issue 跟踪但尚无生产级 benchmark 给出明确答案。
参考资料
官方文档(A 级)
- Ceph Squid v19.2.5 — RADOS Block
Device (
docs.ceph.com/en/squid/rbd/) - Ceph Squid — Exclusive Locks — 协作转移、blocklist、与 advisory lock 互斥
- Ceph Squid — Snapshots / Layering — protect/clone/flatten 硬条件
- Ceph Squid — RBD Features (config ref)
- 源码:
src/librbd/、src/osd/PrimaryLogPG.cc(snapset COW)、src/include/rbd/features.h、drivers/block/rbd.c(Linux kernel)@ v19.2.5
论文 / 奠基(A)
- Weil, S. A. (2007). Ceph: Reliable, Scalable, and High-Performance Distributed Storage. Ph.D. Dissertation, UCSC(块设备适配层与 RADOS 对象切分的早期叙述;细节以 Squid RBD 文档为准)。
- Weil, S. A. et al. (2007). RADOS: A Scalable, Reliable Storage Service for Petabyte-scale Storage Clusters. PDSW ’07(对象存储服务语义;RBD 是其上适配层)。
系列内
- 第 5 篇 · PG 与 Peering(COW 触发的 OSD 端 clone 操作由 PrimaryLogPG 处理)
- 第 6 篇 · 客户端写路径(librbd → librados → OSD Op 路径)
- 第 9 篇 · BlueStore 读路径(OSD 端读对象时的 checksum 验证)
→ 上一篇:EC 路径 · 系列目录 · 下一篇:CephFS / MDS
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【Ceph RADOS】RADOS 内核全景:五轴坐标系与 18 篇路线
补齐站内 distributed/38 之上的 RADOS 内核层缺口;定义 Map 传播、通信、一致性、本地存储、接入代价五条坐标系,给出 18 篇阅读路线与系列边界。
【Ceph RADOS】MON/MGR 与 Map:quorum、epoch 传播与客户端可见性
拆解 Ceph MON Paxos quorum、OSDMap/PGMap 结构与 epoch 传播机制;说明 MGR 主备模型与模块框架;分析客户端 epoch 缓存与可见性窗口的边界条件。
【Ceph RADOS】PG 与 Peering:PrimaryLogPG、PG log 与状态机
拆解 PG 命名与生命周期、PrimaryLogPG 的日志式复制设计、pg_log_entry_t 格式、last_epoch_started 与 last_epoch_clean 的语义差异,以及 peering 状态机从 Reset 到 Active 的完整路径。
【Ceph RADOS】客户端写路径:从 librados 到副本应答与 stale read 边界
追踪一次 RADOS 写从 librados IoCtx 构建 MOSDOp、Objecter 路由、Primary 的 do_op() 执行、副本 MOSDRepOp 应答链,到客户端可读性边界与 stale read 触发条件的完整路径。