
先认真说个事。去年我帮一家客户排查虚拟化平台故障两台KVM宿主机共享同一个RBD卷做的是标准的主备切换。备机在接管前就提前rbd map了同一个镜像监控层面一切正常Ceph集群也一直HEALTH_OK。结果切换那一瞬间备机挂载文件系统直接报EXT4-fs error最后目录项错乱不得不拿备份往回补数据。那次事故让我每次听到“Ceph RBD能不能多客户端同时映射”这个问题反应都是能但你要先回答自己写数据的一端到底有几个。今天这篇就围绕这个事展开先说 RBD 多客户端映射到底支持到什么程度再说并发写风险是怎么发生的然后给出可落地的排他锁、只读映射、锁监控方案最后聊一聊哪些场景压根就不该用 RBD 裸设备来扛。1. 先搞清楚“能映射”和“能安全共享”之间的本质区别1.1 为什么会有人想让多个客户端映射同一个 RBD 镜像这个需求不是凭空来的我接触过的场景大致有这几类高可用主备架构。主备节点共用同一份数据盘为了让切换速度更快有些运维会把 RBD 卷提前map到所有节点上以为这样“切换时就省去了一次 map”。容器或虚拟机热迁移。迁移过程中源端和目标端会有一小段时间“同时看到”同一个块设备很多平台为了保证迁移顺利会在两端都打开设备。备机要预检数据。比如备节点需要起一个进程去检查块设备是否可读、大小是否正确于是顺手就把镜像映射过来看一眼。多台机器同时从同一个“模板镜像”启动系统。这里往往用的是镜像克隆或快照但很多人一开始会直接 map 同一个原始镜像。这些需求背后真正想实现的其实是“多个节点都能访问这份数据”但“访问”和“并发写入”是完全不同级别的诉求。多数事故都发生在没有区分清楚这两者的前提下。1.2 RBD 的“共享”本质上共享了什么RBD 镜像在 Ceph 里其实就是存储在 RADOS 对象之上的一组有序对象组合成的虚拟块设备。客户端通过内核模块krbd或 librbd 把对象拼成一个类似/dev/rbdX的块设备。你让两个节点都rbd map同一个镜像本质上是让两台机器的块设备层指向同一个 pool 下的同一批对象。这里的关键点在于客户端之间没有任何直接通信Ceph 的 OSD 也不会帮你做“这台机器能不能写”的仲裁。每个客户端只知道自己发起了写请求不知道还有另一个客户端也在写同一偏移。打个不太严谨但很好懂的比方两个人拿同一串钥匙开同一扇门不等于两个人可以同时坐在同一把椅子上办公。你能看到同一个设备能发读写命令但设备背后没有“排队”“互斥”这类机制替你兜底。1.3 大多数踩坑的人其实是把“块设备共享”当成了“文件系统共享”块设备之上跑的是文件系统比如 ext4、xfs、btrfs。文件系统负责管理 inode、目录、位图、日志这些数据结构之间有一堆一致性要求。ext4 这类普通文件系统在设计时默认“这块盘只有一个系统在读写”所以它的日志和元数据更新不需要考虑第二台机器同时也在改。两个节点各自以 ext4 挂载同一个 RBD 裸设备就等于两个系统同时在修改同一组元数据且它们互相不知道对方的存在。这跟 RBD 本身是不是稳定、Ceph 集群是不是健康没有关系纯粹是上层使用方式超出了 ext4 的能力边界。这也是为什么后来出现了 GFS2、OCFS2、CephFS 这类分布式文件系统它们生来就要处理“多个节点同时读写同一份数据”的一致性。问题从来不是“RBD 不能共享”而是“你想让它以什么协议、什么语义去共享”。2. 不设防时内核到底发生了什么为什么数据损坏总在“静默”中发生2.1 默认状态下多个客户端可以同时写且 OSD 毫不知情当你没有任何锁机制时rbd map之后的块设备写请求会直接变成 RADOS 对象写请求发往 OSD。两个客户端完全可以对同一个对象、同一个偏移发出写请求OSD 层面看到的只是“来了一个写”它不会去追查这个写请求是不是和另一个客户端冲突。所以经常出现一个诡异的现象明明两个节点同时在往一个卷里写ceph -s却是HEALTH_OK没有慢请求、没有错误告警。问题根本不会在 Ceph 侧暴露它发生在文件系统层和应用层。2.2 静默损坏的几种典型表现第一类最容易暴露ext4/xfs 挂载时报错或者挂载成功但dmesg里出现EXT4-fs error (device rbd0): ext4_find_entry。这是因为两边对目录项的更新互相覆盖目录结构已经坏了。第二类发生在数据库场景。两个 MySQL 实例同时写同一个数据文件或 redo log页缓存不一致最终落盘的数据可能是 A 写的、也可能是 B 写的完全不可预期。数据库日志里会出现“page corruption”“lost write”这类问题。第三类最隐蔽两边看似都正常只有某一份文件被对方的写操作覆盖了没有任何系统报错。你只能通过对比数据版本才能发现“上一次写入的数据没了”。2.3 为什么“我只是短暂挂载一下”也可能翻车有人觉得“我这边只是 mount 检查一下很快 umount不写数据应该没事”。问题是“不写”很难做到。普通文件系统挂载时可能因为日志恢复机制写 superblock可能更新挂载次数可能因为目录项缓存写回产生元数据变更。哪怕只是瞬间的写也足以和另一端的写操作撞车。我见过一个测试环境事故A 节点在格式化一个共享 RBD 卷B 节点同时把它挂载成了旧文件系统。格式化操作更新了超级块和块组描述符B 节点的日志和缓存又把这些旧数据回写覆盖掉最后两边看到的都是残缺状态。所以不要拿“我只读一小会儿”当挡箭牌在共享块设备上任何写都要当作并发写来对待。3. 真正管用的防并发写机制exclusive-lock 排他锁应该怎么用3.1 exclusive-lock 到底锁的是什么RBD 镜像上有一个 feature 叫exclusive-lock。开启后客户端想要对镜像执行写操作必须先获取一把排他锁。这把锁在 librados 的 watch/notify 机制之上实现锁的持有者会保持在镜像锁对象上的 watch。关键点锁不是锁“整个文件系统”而是锁“谁能写这个块设备”。拿到锁的客户端可以正常写没拿到锁的客户端在尝试写时会被拒绝。读操作一般不受影响所以只读客户端可以在写者存在时正常读。我实测下来rbd-nbd 和较新内核的 krbd 都会遵守这把锁。老内核的 krbd 行为差异较大有的版本在 map 阶段不去抢锁也不检查锁结果就是你开了 feature 但实际没生效。所以我会建议生产环境尽量用 rbd-nbd或者确认内核版本对 RBD exclusive-lock 支持完整。3.2 开启和关闭 exclusive-lock 的操作先看当前镜像的 feature 列表rbd info pool-name/image-nameFeature 一栏会列出layering, exclusive-lock, object-map, fast-diff, deep-flatten之类的内容。开启排他锁rbd feature enable pool-name/image-name exclusive-lock关闭rbd feature disable pool-name/image-name exclusive-lock这里有两个实战提醒。一是开启或关闭 feature 会影响在线客户端。比如你给一个已经被两个节点 map 的镜像开启排除锁已连接的 krbd 客户端不一定会立刻重新抢锁行为可能不一致。稳妥做法是先确认没有客户端在写入再操作 feature再重新 map。二是feature 之间有依赖关系。object-map、fast-diff依赖exclusive-lock如果你同时开着这些 feature不要单独去 disableexclusive-lock否则可能导致 object-map 无法正常工作。先查文档理清依赖再做变更。3.3 手动加锁的真实用途别和 exclusive-lock 混淆rbd lock add这类命令经常被人当“排他锁开关”来用实际上它更常用于运维级保护比如防止镜像在某些操作过程中被误写或误删rbd lock add pool-name/image-name my-maintenance-lock --mode exclusive rbd lock list pool-name/image-name rbd lock remove pool-name/image-name my-maintenance-lock client.1234你手动加的锁和 feature 机制的锁处于同一套锁空间librbd 在写入时一样会被挡住所以它确实能起到“临时禁止写”的效果。但它不等于常规并发控制方案通常只在迁移前、删除前、备份前临时加一把“维护锁”。真正用来日常防双写的是exclusive-lockfeature加完之后不建议再去手动操作这把锁交给客户端自己抢就行。3.4 锁冲突后客户端的表现当客户端 A 持有排他锁时客户端 B 尝试打开镜像写数据常见行为是直接报错比如Device or resource busy或者 librbd 返回-EBUSY。有的实现会等待一段时间重试但不会出现“两边同时写成功”的情况。这里也提醒一句锁只在客户端配合时才有意义。如果某个客户端代码里强制绕过锁、直接掉过 librbd 的检测去写对象那谁也拦不住。所以部署层面还要配合 cephx 权限把能写该镜像的客户端限制到最小集合。3.5 客户端崩溃后锁怎么释放这是运维人员最容易卡壳的地方。如果持有锁的客户端突然宕机锁不会立刻消失。Ceph 靠 watch 超时来感知客户端失联超时后 OSD 一般会将该客户端加入 blocklist锁的持有者被判定掉线其他客户端才有机会重新获取锁。默认超时时间通常在 30 秒左右具体看集群配置。如果这个镜像确实没有客户端在写而锁一直没释放等不及的话可以直接强制摘锁rbd lock list pool-name/image-name rbd lock remove pool-name/image-name lock-id locker-id但摘锁前一定要确认没有进程在写入否则等于人为制造双写风险。4. 一场真实“双写事故”的完整复盘从现象到根因再到止血和恢复4.1 事故背景HA 脚本“提前 map”埋下的雷客户的虚拟化平台里有一套 MySQL 多实例架构底层用的是共享 RBD 卷。HA 脚本的逻辑是所有能访问存储的节点在启动后都会提前rbd map这个共享卷然后通过文件锁决定“谁接管”。这个设计的问题在于HA 脚本保护的是应用层的“谁接管”却没有保护块设备层的“谁能写”。正常情况下只有一个节点写数据但一旦发生切换、网络抖动、脚本竞态两个节点就可能同时进入接管逻辑同时往同一个 RBD 卷写数据。4.2 排查链路故障不在 Ceph而在文件系统切换事故发生后备机挂载时报EXT4-fs error。我接手后的排查顺序是这样的先看ceph -s确认集群健康状态。结果是HEALTH_OK说明 RADOS 层没有对象丢失、没有慢请求。再看两个节点的内核日志。备机上能看到文件系统层的大量错误但没有“另一个客户端持锁”这类提示——因为根本没开锁。然后跑rbd info看 feature 列表果然没有exclusive-lock。再跑rbd status pool-name/image-name和rbd lock list能看到两个客户端都是 watcher但没有任何锁在被持有。到了这一步我才敢下结论这不是 Ceph 坏了是文件系统被两个客户端并发写踩坏了。ext4 的目录项和块位图处于不一致状态后面的挂载无法继续。4.3 止血与修复不只是 fsck还要改脚本逻辑先对硬件链路做隔离确保只有一个节点能写断开备节点的 RBD map或者直接把备节点的存储链路断开。在主节点上执行文件系统检查和修复。ext4 的修复通常需要先 umount再跑e2fsck -f。修复过程有一条重要经验fsck 之前先做一次块级备份。当时我用rbd export-diff或直接对镜像做快照保留了一个可回滚的现场避免修到一半发现修复方案不对连退回的机会都没有。修复完成后我和客户一起改了 HA 脚本把“所有节点提前 map”改成“只有主节点 map备节点不 map”。切换时旧主节点必须完成 umount/unmap 后再允许新主节点 map。map 之前先检查rbd status确认没有排他锁被其他客户端持有。同时在镜像上开启了exclusive-lock。后续又加了一层监控任何节点在 map 共享卷后都要输出 lock 状态防止再出现“静默双写”。4.4 这次事故给我的三条运维原则第一默认把所有 RBD 镜像的 feature 带上exclusive-lock。即使当前只有一个客户端在写这把锁也能在未来某天给你兜底。第二共享卷的写者必须“唯一化”不能只依赖应用层判断。锁应该在块设备层就完成仲裁而不是靠脚本里的 if 分支。第三所有共享卷操作留痕。map、unmap、lock 获取、锁冲突都要有日志否则事后只能靠猜。5. 运维工具箱锁监控、锁清理与日常巡检5.1 快速判断当前谁在写、谁持有锁rbd status pool-name/image-name # 输出大致是 # Watchers: # watcherclient.123 hostnode-a # ...这个命令能告诉你当前有哪些客户端连接着镜像但注意“watcher 存在”不等于“客户端持有排他锁”。想看锁的更多细节rbd lock list pool-name/image-name输出里会列出锁 ID、锁模式exclusive/shared、锁持有者。有经验后你会习惯把这两个命令一起跑。5.2 客户端崩溃后锁清理的规范动作遇到“锁还在、客户端已死”的情况正确顺序是确认没有进程在读写该镜像检查lsblk、mount、fuser -v /dev/rbdX。等待 watch 超时让 Ceph 自动 blocklist 失联客户端通常 30 秒到几分钟。等不及再手动摘锁命令见上一节。我见过有人嫌超时太慢直接rbd lock remove摘锁结果那个“已死”的客户端其实还在半死状态重新恢复后继续写数据造成真正意义上的双写。所以摘锁前宁可多等一个超时周期也不要赌客户端已经死透。5.3 巡检脚本把锁状态纳入日常监控共享卷的巡检不复杂但一定要有。下面这个脚本思路可以抄走#!/bin/bash POOLsharedpool for img in $(rbd ls $POOL); do echo $img rbd status $POOL/$img rbd lock list $POOL/$img done还可以加一个“双写检测”遍历所有节点检查同一个 RBD 设备是否同时存在多个 map 记录。用lsblk配合节点间比对或者让每个节点把自己的 map 记录集中上报交给监控平台判断。另外一个非常实用的告警项监听rbd lock冲突日志。librbd 在写失败、锁冲突时会在客户端日志里留下记录。把这些日志接进告警平台比事后看数据损坏要快得多。5.4 cephx 权限减少人为误操作的另一种手段cephx 认证可以限制某个 key 只能访问某些 pool甚至只读某些镜像。比如给只读节点创建一个只能读的 cephx keyceph auth get-or-create client.ro mon allow r osd allow rw poolsharedpool \ -o /etc/ceph/ceph.client.ro.keyring注意osd allow rw里的rw在 RBD 语境下还是会允许读写的。如果真想限制成只读需要配合 RBD 层的只读映射或者用快照只读访问。cephx 是减少人为误操作的手段不是并发控制的替代品。6. 如果业务真的需要多端写重新选择存储协议而不是硬扛6.1 单写多读场景只读映射和快照映射是正解绝大多数“共享卷”需求实际都是单写多读。主节点正常读写备节点只需要在切换时接管平时不需要写。此时最干净的做法是备节点不 map只在需要时只读 map。rbd device map pool-name/image-name -o readonly或者基于快照只读访问效果更佳rbd snap create pool-name/image-namereplica rbd device map pool-name/image-namereplica -o readonly快照只读的好处是即便主节点在写备节点读到的也是某个时间点的稳定视图不会读到写了一半的中间状态。多节点同时只读同一个快照是安全的因为没人会写它。6.2 多写多读场景别拿 RBD 裸设备硬扛如果业务真的需要多个节点同时读、同时写同一份文件系统RBD 裸设备加 ext4 这条路基本走不通。你有两个更合理的升级方向方向一是直接用 CephFS。CephFS 本身是分布式文件系统元数据服务、分布式锁、客户端缓存一致性都是现成的。多个客户端并发读写同一目录、同一文件是它设计内的事很多 RBD 方案里的“脑裂”“双写”问题在 CephFS 里会被 MDS 锁机制处理掉。方向二是在 RBD 之上跑集群文件系统比如 GFS2 或 OCFS2。这条路能做但复杂度不小需要自己维护集群心跳、锁管理、fencing。我一般不推荐普通团队为了省事而选择这个方案除非你对文件系统底层机制很熟且有足够人力维护。数据库场景要单独说一句Oracle RAC 这类多实例共享存储架构通常依赖 SCSI-3 Persistent Reservation 这类底层锁语义。RBD 并不能直接等价提供这些语义千万不要把“裸盘 RAC”直接架在 RBD 上否则遇到锁冲突、节点驱逐时你会非常被动。6.3 虚拟化和容器场景尤其要遵守“单一活跃写者”原则KVM 在线迁移时源端和目标端可能同时在某个时间段访问同一个块设备但业务设计上仍然要求同一时刻只有一端写入。QEMU 的存储迁移机制会在块设备层做好 mirror 和切换不能简单理解成“两边都在写”。容器平台的持久卷访问模式更直白RBD 卷通常只适合做 RWO也就是单节点读写。如果你需要一个跨节点的共享读写卷不要试图把 RBD 硬塞给多个 Pod 同时挂载直接选 CephFS 这类共享文件系统语义更稳。6.4 实在无法避免多客户端映射时的兜底清单有些历史架构短期内改不动只能先把风险控制住。我会按这个顺序检查确保镜像开启exclusive-lock并且所有客户端使用的内核/librbd 版本支持锁检查应用层明确“唯一写者”并把写者身份写进锁的 cookie 或客户端标识里只读客户端统一走只读映射或快照只读映射监控rbd status、rbd lock list对异常 watcher 和锁滞留立即告警切换脚本增加锁检查和 fencing 逻辑宁可让流程卡住也不要让两个写者同时出现。最后再分享一点实际操作中的体会我现在接手任何一套“共享卷”架构第一件事就是先把所有涉及多客户端映射的镜像 feature 列表拉出来逐个确认有没有exclusive-lock。这比看 Ceph 集群的容量和性能指标都要优先因为大量事故压根不是存储故障而是使用方式错了。有一个小技巧我一直在用也推荐给你如果现场实在没有办法开启exclusive-lock比如内核版本太老、客户端太杂那就让备节点多等 30 秒再接管也不要在切换前提前去rbd map。多等 30 秒不会死人两个节点同时写同一个 ext4会。Ceph RBD 提供的是一个稳定可靠的块存储底座但它不会替你决定“谁有资格写入”。锁是保护机制而真正安全的前提永远是架构上只有一个明确写者。能想清楚这一点多客户端映射这个话题基本就不会再让你踩坑。