土法炼钢 · 系统与基础设施

【Ceph RADOS】RBD:image 到对象的映射、krbd 与 librbd、快照与克隆边界

文章导航

分类入口
storagedistributed
标签入口
#ceph#rbd#rados#krbd#librbd#snapshot#clone#squid#v19.2.5

目录

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.5docs.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_prefixorder,再用偏移公式算序号。命名一旦写错(手工拼对象名、漏补零),会落在错误对象标识上,表现为「静默空洞」而非明确报错。运维核对应以 rbd infoblock_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.hsrc/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 引入,持续维护至今。它依赖 libcephnet/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 Lockexclusive-lock feature)是 RBD 层的客户端互斥,不是 OSD 层的 advisory lock。Squid 文档(docs.ceph.com/en/squid/rbd/rbd-exclusive-locks/)钉死三点:

  1. 保护对象:保证同一时刻只有一个客户端在写 image,从而保护 object-map、journal、PWL cache 等内部结构不被并发改坏。新建 image 默认开启;可用 rbd_default_features--image-shared 覆盖。
  2. 协作转移(默认):写方先抢锁;若锁在别的客户端手上,请求对方停写、flush 缓存后释放。默认模式不会禁止多客户端「轮流写」——只是把写线性化。要禁止自动转移,需 rbd device map --exclusive(对应 RBD_LOCK_MODE_EXCLUSIVE)。
  3. 非优雅退出 → blocklist:持锁客户端硬杀/掉电时,新写方不能直接抢锁;它与 Monitor 协商,先把旧客户端 blocklist(fencing),等 OSDMap 生效后再破锁。缺少 osd blocklist 能力(profile rbd 含此 cap)时,破锁路径会失败。

Exclusive Lock 不兼容 rbd lock add / rbd lock rm 那套 advisory lock。object-mapfast-diffjournaling 都依赖它;关掉 exclusive-lock 会连带拖垮这些特性的正确性与性能假设。

krbd map 另有 features 闸门:内核若未实现 journaling 等,rbd device map 直接失败。核对:

rbd info {pool}/{image}

krbd 的适用场景

局限:与 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 内部用 ImageCtxsrc/librbd/ImageCtx.h)持有 image 的打开状态,包括:

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 列表)。真正的复制发生在之后第一次覆盖写该对象时,由 PrimaryLogPGsrc/osd/PrimaryLogPG.cc @ v19.2.5)在 OSD 侧完成。

机制链:

  1. 客户端每次写带上当前 SnapContext
  2. Primary 查该 OID 的 snapset:若 head 仍被某一 snap 引用、且本次写会破坏「snap 可读旧内容」不变量,则先 clone / 拆分 snapset(OSD op 语义上的 copy-on-write),再写 head。
  3. 同一 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。

快照的失败场景


五、克隆:父子链与 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 中的一个对象时:

  1. librbd/krbd 先检查子 image 对象池是否有该对象。
  2. 若无,检查父 image 对应偏移是否在 overlap 范围内。
  3. 若在,从父 image(按快照 epoch)读取对应对象。
  4. 若不在(子 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 依赖 layeringobject-mapfast-diff 依赖 exclusive-lockjournaling 依赖 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 级)

论文 / 奠基(A)

系列内

上一篇:EC 路径 · 系列目录 · 下一篇:CephFS / MDS

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .