☰
Ceph RBD镜像锁机制详解:排他锁原理与运维排查实战
2026/10/3 3:06:36 网站建设 项目流程

1. 先弄明白rbd镜像的锁到底是什么

1.1 一个被多挂盘“教做人”的运维现场

先说个真实场景。前几年我维护一套Ceph集群,某天业务方反馈说数据库虚机突然IO卡死,rbd status看了半天,镜像没有异常,OSD也全部active+clean,但那一块块rbd盘就是写不进去。排查到最后,发现是另一个测试环境的主机把这块镜像也挂上了,两边同时有IO进来,librbd客户端和OSD之间因为锁冲突直接僵在那了。

这就是rbd镜像锁在干活——它不让两个客户端同时写同一块块设备。Ceph的rbd镜像在默认开启exclusive-lock特性时,任何客户端想要对镜像执行写操作,必须先拿到这把锁。拿不到锁,客户端连写IO都不会发出去。很多人用Ceph做云平台后端,OpenStack Nova或Cinder在挂载块设备时,底层实际都在跟这把锁打交道。说白了,它就像卫生间门口那个指示灯:灯亮着,你只能在门外等,强行推门进去,里面的人和你都得遭殃。

1.2 排他锁:分布式存储的“单写者”协议

rbd镜像的锁,准确叫法是exclusive lock,排他锁。它的职责是在分布式环境下保证“同一时刻只有一个客户端能对镜像发起写操作”。为什么需要它?因为底层rbd块设备对应的是分布在不同OSD上的多个对象,如果两个客户端同时往同一块地址写,对象内的数据版本会互相覆盖,而OSD层做不到像本地文件系统那样的细粒度锁。

排他锁的粒度是整个镜像,不是某个对象或某个偏移范围。也就是说,谁拿到锁,谁就拥有对整个镜像的写权限。读操作不需要锁,任何客户端都可以直接读,这也符合块设备最常见的“单写多读”使用模型。

锁还有一个关键性质:它不是永久持有。客户端持有锁后,会通过watch机制持续向镜像头对象续约,一旦客户端异常退出或网络隔离,锁会在一段时间后自动释放或被其他客户端抢占。这个“能抢锁”的设计,是高可用集群切换时必须依赖的,否则一台宿主机宕机,虚拟机的盘永远没人能接管了。

1.3 锁是镜像特性,不是OSD层自动默认

新手最容易误解的一点是:以为Ceph的OSD天然就有锁。实际上rbd镜像的锁是rbd镜像层面的特性,在镜像创建时通过features控制的。你可以用rbd info查看一个镜像的features,如果列表里有exclusive-lock,那说明这块镜像启用了排他锁机制。

镜像特性可以在创建时指定,也可以后期动态修改。比如早期创建的镜像可能只有layering特性,后来业务需要迁移场景,才把exclusive-lock加上。需要注意,关闭这个特性和开启这个特性都是有代价的,后面我会专门展开。

2. 排他锁的实现机制:头对象、watch与锁记录

2.1 锁信息存哪儿:镜像头对象

rbd镜像在Ceph里不是单个对象,而是一个对象集合,其中最特殊的是rbd_header头对象。它在镜像创建时生成,像一块“门牌”区域,专门记录元数据:镜像名称、大小、特性、快照列表,以及锁记录。

old format和v2 format在头对象命名上不一样,但锁相关的逻辑是基本共通的。锁记录写在头对象的locker字段里,内容包括:

  • 持锁客户端的唯一标识(通常是client.进程号或主机名、Pod名字)
  • 锁的cookie,随机生成的一串标识符
  • 锁类型(exclusive就是排他锁)
  • 持锁时间戳等相关信息

用rados -p rbd get rbd_header.xxx /tmp/header --snapshot这类命令能dump出头对象的原始内容,锁就明明白白躺在里面。平时我们不用这么底层,但理解这一点,后续排障时会很清楚自己改的是什么。

2.2 watch/notify:把锁的状态变化广播出去

锁的管理依赖Ceph底层的watch/notify机制。简单说,获取锁成功的客户端,会在头对象上注册一个watch;其他客户端也可以注册watch,用于感知锁的变化。

场景是这样的:客户端A拿到锁,客户端B也想写,于是B尝试获取同一把锁,但锁已被A持有。此时B有两个选择,一个是立刻失败,另一个是“等待锁释放”。实际实现中,B可以设置一个锁超时时间,也就是说B会阻塞在那里等,直到A释放锁,或者watch收到通知说A已经失去锁,B再去抢。

watch/notify还承担了“心跳”的功能。A持有锁期间,会定期向头对象发出通知,如果A宕机,watch连接就会断开。OSD管辖头对象时,会判定A的watch超时,随后将锁标记为可抢占。这个机制的响应速度,直接决定了故障切换的快慢。

2.3 一次典型加锁的完整动作

我把一次加锁的完整动作拆开讲,你感受下这个协议的精妙之处。

第一步,客户端Open镜像。librbd读取头对象,发现镜像启用了exclusive-lock特性,于是进入“需要获取锁”的状态。

第二步,客户端在头对象上注册watch,然后调用锁申请接口。OSD收到请求,检查头对象里的locker记录当前是否有人持有锁。没人持有,就写入客户端的锁记录,申请成功。

第三步,客户端继续执行后续IO。此后的每次写操作,它都会直接向对象发送写请求,对象在写入前还会再检查一次该客户端确实持有锁。这就是为什么锁还能防止“客户端自己忘了拿锁就写”的野路子行为。

第四步,客户端正常关闭镜像时,会主动释放锁,清掉头对象上的locker记录。如果异常退出,watch超时后锁被标记为过期,等待其他客户端抢占。

第五步,其他客户端如果配置了锁等待,会在watch上收到锁被释放或抢占的通知,随即尝试重新获取锁。

这套机制说起来简单,但它解决的是分布式系统里最头疼的“谁有权写”的问题。rbd没有引入额外的锁服务,而是完全复用Ceph底层的对象操作和watch/notify能力,这也是它能在大规模集群里稳定跑的原因之一。

3. 锁的运维实操:查看、释放、监控一条龙

3.1 加锁、查锁和解锁的常用命令

平时运维中,锁相关操作主要靠rbd命令完成。我直接把常用的列出来,附带解释。

# 查看某块镜像当前是否有锁,以及锁被谁持有 rbd lock ls --pool rbd --image disk-001 # 手动获取一把独占锁(不常用,但测试时有用) rbd lock add --pool rbd --image disk-001 testlock # 手动释放指定锁(id参数填锁名,cookie参数可以省略或填具体值) rbd lock remove --pool rbd --image disk-001 testlock # 查看镜像的watcher信息,可以看到谁在“看着”这个镜像 rbd status --pool rbd --image disk-001

rbd lock ls的输出一般是这样的:

Client Address Cookie client.82 10.0.0.11:0/123 auto

这里client.82是librbd客户端的名字,Address是客户端所在主机和tcp地址,Cookie是锁的唯一标识。有了这些信息,你就能判断是谁占着锁。

rbd status会输出类似“Watcher: client.91=10.0.0.22:0/456;”的信息,它告诉你当前有哪些客户端watch了这个镜像。注意,watch和lock不是一回事:看的人可以有很多,但拿锁的人只能有一个。排查时两者要互相印证。

3.2 锁残留的常见场景与强制解锁

锁本身不是bug,真正让人头疼的是“锁残留”。最常见的一个残留场景:某个客户端所在的宿主机宕机或网络隔离后,Ceph侧没有及时把锁清掉,导致新的客户端长时间拿不到锁,镜像打不开,业务起不来。

出现这种情况,官方本来就给你提供了强制解锁手段:

# 强制清掉某个锁,cookie写锁名即可 rbd lock remove --pool rbd --image disk-001 client.82 auto

执行后,头对象里的locker记录会被清掉。但这只是头对象层面的清理。那个还活着的客户端,其实并不知道自己的锁已经被强制移除了,它依然会继续发写IO。OSD在收到它的写请求时,会检查locker记录,发现它已经没有锁了,于是直接拒绝写入,并把这个客户端加入blacklist。

所以强制解锁是一把双刃剑:它让新的客户端有机会获取锁,但也会让旧的客户端陷入“IO失败”状态。如果是宿主机确实已经宕机,这个操作是合理的;但如果你搞错了,被清的锁其实是一个健康客户端正在使用的,那业务IO会立刻异常,虚机里会出现文件系统只读、数据库报错等连锁反应。

3.3 三个必须记住的参数

实际集群里,锁相关的行为是可以通过配置调优的。我重点提三个参数,大家根据自己的业务场景调整。

第一个是rbd_blacklist_on_break_lock,默认值是true。它控制的是:当有人通过rbd lock remove强制破锁时,是否把原先的持锁客户端加入OSD黑名单。如果设为false,则破锁后原客户端还有可能继续IO,但数据一致性风险极高。我的建议是保持默认true,安全大于一切。

第二个是rbd_lock_timeout,默认0,表示客户端获取锁失败后立即报错。设成大于0的秒数,比如10,客户端就会在拿不到锁时等待这么久,期间不断重试。对经常做故障切换的集群,设个合理值能避免虚机在切换期间直接报IO错误,而是等锁释放后自行恢复。

第三个是rbd_request_timed_out_seconds,它决定客户端等待一个请求的全局超时时间。实际排查中,我发现很多“锁等待卡死”都跟它有关——耐心等锁的客户端,如果这个值设得太小,反而会提前放弃。

这三个参数,集群重启时可以通过ceph daemon osd.0 config set在线调整,但最好是统一写进ceph.conf里,防止重启后丢失。

4. 常见问题与排查技巧实录

4.1 镜像卡在opening/等待锁超时

有段时间我频繁遇到云平台创建虚机时,镜像一直处于opening状态,最后超时。用rbd status一看,镜像上挂着一个来自“疑似已宕机”主机的watch。这台机器能在rbd status里看到,但ping不通,ens级问题。

排查思路很清晰:先确认原主机确实不可用,然后直接清理watch和锁。清理watch需要用到一些底层的rados操作,但多数情况下,直接执行rbd lock remove就够了,因为锁清掉后,watch也会随之失效。

值得提醒的是,如果你发现镜像卡在打开阶段,但镜像本身并没有锁记录,那问题可能不是锁,而是客户端和OSD之间的网络连通性。别一看到卡住就往锁上猜,先分清是“拿不到锁”还是“根本联系不上OSD”。

4.2 强制破锁后客户端IO直接挂掉

这是个经典的“我本将心向明月”案例。我以为某块镜像是一个废弃虚拟机在占用,直接rbd lock remove把锁清了。结果那台“废弃”虚机其实还在跑,业务侧立刻报IO错误,虚机内出现EXT4-fs error、数据库事务写不进去之类的问题。

原因是rbd_blacklist_on_break_lock默认开启,破锁后原客户端被加入了黑名单,它的原watch连接也失效了。客户端收到-EBLACKLISTED错误,所有IO都被打回,意识不到竞争关系明确以前,任何强制操作都存在风险。

从那次以后,我给自己定了一条规矩:执行强制破锁前,必须能看到原客户端所在宿主机已经关机或网络隔离的明确证据。拿不准就先联系对应业务的负责人确认,哪怕多花十分钟,也比误伤业务强。

4.3 性能优化与锁相关设计的副作用

有些团队为了追求性能,会关闭exclusive-lock特性,理由是锁竞争会给每次写IO带来额外开销。确实,每次写操作OSD都要检查锁,会让写入路径多一次对象属性的读取。在低配机械盘集群上,这个开销可能很明显。

但关闭排他锁的后果是你无法再保证“单写者”协议。块设备一旦被多个客户端同时挂载写,那就是自找麻烦——轻则数据错乱,重则整个镜像损坏,连rbd restore都救不回来。

我的建议是:不要把排他锁和性能对立起来。性能瓶颈通常出在磁盘IOPS、网络带宽或EC校验上,锁检查的消耗占比微乎其微。真正想优化,应该从缓存策略、副本池和EC池的选型入手。排他锁是保命的机制,不值得用数据安全去换那一点点性能。

4.4 关闭exclusive-lock的后果与替代方案

如果某个场景真的需要多客户端同时挂载写同一块rbd镜像,比如Oracle RAC这类共享存储集群,标准的做法不是粗暴关闭exclusive-lock,而是需要考虑更合适的产品形态。Ceph官方对RBD多写场景的支持力度一直有限,即使有journaling特性加rbd mirror,也不是为了多写设计的。

正确姿势是:如果真的要多写,建议把数据库分成多个rbd镜像,每个实例挂自己的盘;或者改用CephFS共享文件系统,走文件锁机制;再或者用支持多写的存储方案,比如Lustre或GFS等分布式文件系统。

有一种临时做法是在镜像上关闭exclusive-lock,让多个客户端同时“裸写”,但这属于饮鸩止渴,数据安全性完全没有保障,只适合测试环境临时验证,生产环境千万别碰。

5. 我踩过的坑和经验总结

5.1 别在业务高峰随手破锁

我吃过一次很大的亏。某个上午业务高峰,一个rbd镜像被某台主机异常占用,新主机无法获取锁。图省事,我直接在业务侧没有确认的情况下执行了rbd lock remove,结果新主机拿到锁后,旧主机还在尝试IO,两边在同一块盘上交叉写,虚机文件系统直接出现大量错误。

事后复盘,正确做法应该分三步:先确认旧主机状态,再决定是否要把旧主机联系从平台上摘除;然后在业务低峰执行破锁;最后等新主机拿到锁,做一次文件系统检查,确认数据没有异常再恢复对外服务。很多问题的根源不在锁本身,而是操作者太着急。

5.2 先看watchers再看锁

锁和watch是两回事,但排障时两者必须一起看。rbd status能告诉你谁在watch镜像,rbd lock ls能告诉你谁在持有锁。如果watch里有多个客户端,但只有一个持有锁,那是正常状态;如果lock ls里显示的客户端,在status里根本不在watcher列表里,这更像是客户端异常退出后锁没清干净的残留。

反过来,如果lock ls显示没有锁,但镜像仍然打不开,那问题基本不在锁,而在别处,比如网络策略、osd状态或者客户端配置。把思维从“锁”上抽离出来,顺着IO路径去查,往往更容易找到真凶。

5.3 让“锁”成为自动化运维的常规巡检项

最后提一个建议:把这套锁机制做成巡检脚本,而不是只等出故障再手工查。

#!/bin/bash # 巡检所有rbd镜像,找出持有锁但超过N小时未变化的记录 rbd ls --pool rbd | while read img; do echo "== $img ==" rbd lock ls --pool rbd --image "$img" 2>/dev/null done

锁记录长时间不变化,一般意味着要么客户端正常持有但低频写,要么就是残留锁。加一个时间维度去判断,比我前面说的“看是不是宕机”更实用。单靠人工一个个看,集群规模大了根本忙不过来。

rbd镜像的锁就像分布式存储世界的红绿灯。平时它安安静静工作,你不觉得它有多重要;一旦它出问题,整个业务都会卡在原地。我这些年做集群运维,凡是数据一致性问题,最后追到根因,十有八九都能跟“锁”扯上关系。理解它、尊重它、监控它,这是每个和Ceph打交道的工程师都值得花功夫的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询