我在生产环境里跟这玩意儿死磕过不少次。rbd镜像的锁说白了就是Ceph给块设备加的一道“写入互斥闸门”防止两个客户端同一时刻对同一块镜像下手写坏。锁一旦出问题表现往往很典型虚拟机起不来、迁移卡死、rbd map报busy更恶心的是明明没有马在车上锁却赖着不走。这篇文章适合谁看只要你的Ceph集群在用RBD提供存储——不管背后是OpenStack、KVM/QEMU、Proxmox还是普通物理机直接mount——我都建议把全文读完再上岗。镜像锁的很多细节不在常规文档里而在真实故障现场尤其在你经历过高可用切换、集群重启、宿主机宕机之后你一定会回来翻这篇文章。我会顺着“锁是什么 → 锁怎么工作 → 命令行怎么操作 → 故障怎么排查 → 参数怎么调优”这条线索把整个锁机制从头到尾过一遍。1. rbd镜像锁到底是什么1.1 一句话定位RBDRADOS Block Device镜像锁是librbd和krbd在镜像header对象上实现的一种分布式协作锁。你可以把它理解成文件锁只不过锁的粒度不是某个文件而是一整块虚拟磁盘。客户端打开镜像以后如果接下来要写数据就必须先拿到锁拿到锁才能写其他人要么只读等待要么直接报Device or resource busy。锁主要服务于两类场景。第一类是同一块镜像同时被多个客户端打开必须串行化写操作不然磁盘文件系统会在几秒钟之内被写成一锅粥。第二类是快照、克隆、在线扩容这类元数据操作它们要重新读写镜像的元数据如果这个时候有人在乱写数据同样会出大问题所以这些操作同样要走锁流程。默认配置下新建RBD镜像会开启exclusive-lock特性标准客户端只要挂载就会自动加锁。这也解释了为什么很多事故本质上是同一个原因挂载进程异常退出锁没有立刻释放下一个客户端在傻等。1.2 没有锁会发生什么以前有个测试环境为了图省事把两个计算节点的虚拟机放在同一块裸盘上做共享存储结果两块文件系统同时挂载什么都没干几分钟后磁盘出现了大量目录项错乱。这种事故不是Ceph的锅是典型的没有锁的锅。RBD本质是一块带网络访问的磁盘如果你把裸设备同时交给两台物理机去mount后果和一块SCSI盘同时挂到两台机器上没有任何区别。除非底层是集群文件系统否则就是在赌命。有锁也不代表万能。锁实现不当客户端A持锁写入客户端B不遵守锁协议强行写入一样损坏数据。所以RBD锁准确说是“协作锁”它希望所有客户端都讲武德。值得庆幸的是标准客户端内核krbd模块、librbd库确实都遵循这套协议问题通常出在人为配置或异常进程上。1.3 两种锁形态独占锁和共享锁RBD锁分两种类型。独占锁就是互斥锁同一时刻只能有一个持锁者符号是X锁用途是写镜像。共享锁叫做S锁允许多个客户端同时持锁主要用于只读场景比如多节点同时读同一个镜像做备份。共享锁带一个tag同tag的S锁互相兼容不同tag的S锁互相冲突。这个设计看起来多余实际非常实用。比如两套备份系统如果它们用同一个tag的锁说明约定在做同一种读操作可以并行如果tag不同说明彼此业务逻辑可能对镜像做不同事情就不能并发。这和分布式锁的经典套路对比tag机制相当于给不同业务方划了一条看不见的道。1.4 与文件锁、数据库锁的类比很多人第一次接触RBD锁会问这和Linux的flock、mysql的表锁有什么区别本质都是“在一个公共位置记录谁占用了什么资源”。flock在内存里、mysql在数据字典里、RBD在RADOS对象的omap里。区别在于RBD锁要面对网络故障、节点宕机、时钟漂移所以它的核心设计是锁记录本身要持久化同时要有超时机制不能因为一个客户端死掉就永久锁死资源。理解了这一点再看后面的watch机制就不会觉得奇怪了。2. 锁是怎么落地的从加锁到解锁2.1 锁存在哪里锁不是存在某个独立锁服务里而是存在镜像的header对象上。每个RBD镜像都有一个元数据对象锁信息写在对象的omap中记录了锁的ID、cookie、客户端ID形如client.1234、网络地址这些字段。也就是说锁可以持久化也能被其他节点看见关键是它必须依赖一个“活”的持锁客户端做心跳。客户端对header对象注册一个watch。OSD根据watch判断客户端是否还活着如果客户端一直不续约超过watch超时时间OSD就判断它死了锁就变成陈旧锁。默认watch超时是30秒所以客户端崩溃后锁不会马上消失最坏情况要等30秒甚至更久才能被下一个客户端从逻辑上抢占。2.2 加锁到解锁的完整链路我习惯把整个流程拆成五步来记打开镜像读取header元数据确认这条镜像开了哪些features。如果有写意图尝试获取镜像锁本质是往header对象的omap写一条锁记录。如果锁已经被别人持有且冲突获取锁失败。librbd会按配置重试内核krbd模块通常直接报busy。持锁期间客户端要周期性续约watch证明自己还活着。正常退出时主动删除锁记录并撤销watch异常退出只能等watch超时过期。这个流程和分布式锁没有本质不同一个共享存储点、一个持锁标记、一个心跳机制。只不过RBD的存储点是RADOS对象心跳走的是watch/notify协议。2.3 陈旧锁是怎么产生的明白锁的机制后再解释陈旧锁持锁客户端崩溃、网络中断、被强杀watch没有正常撤销OSD过了watch超时后判定客户端失联锁就在逻辑上“悬空”了。此时rbd lock ls看到的锁依然存在但属于过期锁。在实现上陈旧锁不会被动消失而是等下一个客户端尝试获取锁时发现锁已过期并主动抢占。所以你会发现有时候故障发生后什么都不做过一两分钟业务自己又恢复了就是其他客户端在后台把陈旧锁抢走了。反过来如果没有任何客户端尝试抢锁这把锁记录会一直躺在header对象上直到你手动清或进行重启。所以定期检查锁列表是件很划算的事。2.4 锁与blocklist的关系这里要提醒一句watch过期和OSD对客户端生死状态的判断是两套时间线。OSD通过心跳机制判断客户端节点是否整体不可达如果它判定节点失联可能触发blocklist操作把节点踢出黑名单。节点一旦被blocklist它持有的所有镜像锁都失去意义。看锁问题的时候不能只盯锁本身还要看一眼mon日志里有没有blocklist事件。很多场景里blocklist和陈旧锁是同时出现的把被blocklist的节点清理掉之后锁自然就好处理了。不要以为锁问题只是锁的问题它常常是节点健康问题的投影。3. 实际操作用rbd命令管理镜像锁3.1 查看锁状态遇到锁相关的报错第一步永远是看锁列表rbd lock ls pool-name/image-name老版本写 rbd lock list 也行两者等价。输出示例Pool Image ID Locker Address pool img auto client.1234 172.16.1.2:0/123456如果锁已经变成陈旧锁较新版本会在状态里标出stale。看到stale基本可以放心清理。这里有个经验用rbd lock ls之前先确认命令背后连接的是正确的pool和namespace曾经有人写错pool查了半天查不到锁实际上锁在另一个pool里。自动化脚本里我还习惯加 --formatjson方便后续解析和判断。3.2 手动加锁与解锁除了客户端自动锁RBD也支持手动锁常用在外部备份软件、容灾切换工具做业务协调。加锁rbd lock add pool-name/image-name backup-lock --shared backuptag释放rbd lock remove pool-name/image-name backup-lock client.1234手动锁和自动锁的区别在于手动锁不会随进程崩溃自动过期它完全由发锁方自己管理。写脚本时一定要在异常分支里清锁否则下次备份会被自己当年留下的锁卡住。这是我在自动化脚本里踩过的最蠢的坑没有之一。光备份脚本本身测通了不算数测试故障注入路径时手动锁残留问题一定会暴露。3.3 强制清理残留锁确认持锁客户端已经不存在后清理标准动作rbd lock remove pool-name/image-name auto client.1234.xxxauto是锁IDrbd lock ls输出里ID列就是。必须强调强制清锁前要确认原持锁者真的没有在写镜像。如果对方还在写你删锁后它后续写入会失败更麻烦的是别的客户端抢到锁开始写两边同时写同一块数据。生产环境我宁可多等一下watch超时也不轻易强制清锁除非业务侧已经明确这台虚拟机被强杀了。3.4 锁和镜像特性的组合使用RBD锁不是孤立存在的它和一组镜像特性绑在一起。exclusive-lock是最核心的特性object-map、fast-diff、journaling这些特性都依赖它。比如要开journaling做灾备必须先开exclusive-lock。所以你在创建镜像时如果手动指定了features一定要留意是否把exclusive-lock关了否则后面要么不能开journaling要么多写者场景锁保护失效。我见过有人为了省性能把exclusive-lock关掉结果做在线迁移时数据写乱。这种为了性能牺牲一致性的操作在存储领域从来都不划算。真觉得锁开销大可以先排查业务是否真的需要多写者而不是一关了之。镜像创建时留一条规范化的features模板比每次手工敲参数要可靠得多。4. 排查实录镜像锁引发的经典故障4.1 内核挂载失败Device or resource busy这个报错出现频率最高。rbd map时报rbd: map failed: write to /sys/bus/rbd/devices: Device or resource busy意思是锁被占。常见场景虚拟机被强杀或宿主机崩溃旧的krbd锁还在。处理顺序先rbd lock ls确认锁再判断持锁客户端是否真的不在了确认残留后执行rbd lock remove清理完重新map就恢复。如果不想每次强杀后都手动清就把watch超时调小同时把挂载流程做成有规范启停脚本的让锁走正常释放路径。4.2 在线迁移卡住锁易主OpenStack或Proxmox的虚拟机在线迁移时源和目标两端qemu进程同时存在都通过librbd访问同一块镜像。迁移后半段目标qemu要抢exclusive lock源qemu要交回锁。这个锁易主瞬间往往伴随短暂IO阻塞如果磁盘负载高迁移会卡几十秒甚至超时。我在实际环境踩过一个坑调整了迁移并发数后大量迁移同时抢占锁锁冲突频繁整个集群IO延迟一度飙到上百毫秒。后来把迁移并发控制住又把librbd侧锁超时参数调短让抢锁失败尽早报错而不是无限等迁移才稳定。对live migration场景建议把librbd侧锁等待时间设为20到30秒不要用默认的60秒。4.3 客户端崩溃后的锁残留与自动恢复qemu进程被kill -9或宿主机断电锁不会走正常释放路径于是残留下来。经过watch超时后在rbd lock ls里变成stale。此时新客户端尝试拿锁时部分librbd会自动抢占陈旧锁不需要人工干预但有的版本会等待一个锁重试周期表现就是虚拟机启动特别慢。我的习惯是运维平台的故障转移脚本里先扫一遍rbd lock ls发现stale锁直接remove能显著缩短故障转移时间。前提是锁确实stale否则不要动手。操作前可以用时间戳对比一下锁记录时间是几分钟前的而进程列表里根本没有对应客户端那才符合清锁条件。4.4 快照、克隆、扩容操作被锁卡住锁不只影响挂载和写入有些镜像元数据操作被锁卡住时表现是任务pending前台IO越来越慢。比如正在运行的虚拟机做快照时ceph底层会短暂冻结IO去处理元数据保护如果锁状态异常快照就卡住。遇到这类问题不要第一时间怀疑存储底层硬件先查锁。有一次告警说克隆任务一直pending查了半天结果是一个备份脚本的手动锁没释放把整个克隆流程压住了。手动锁是隐形问题不在自动锁路径里但拦起路来一视同仁。排查时注意rbd lock ls里ID列不是auto的锁基本都是手动锁优先级要更高。4.5 容器和rbd-nbd场景的锁边缘情况用rbd-nbd而不是内核krbd挂载RBD时锁行为也值得注意。rbd-nbd走的是用户态librbd进程异常退出时同样会产生陈旧锁。但更隐蔽的问题是如果在容器里映射RBD容器重建后旧的rbd-nbd进程还可能留在宿主机上锁一直有效新的容器实例永远拿不到锁。这种场景下容器编排脚本里除了清理容器还要kill对应rbd-nbd进程并清锁。我把这条写进过checklist因为只要漏掉一次故障转移就会卡在启动阶段。排查现场如果看到rbd lock ls的持锁地址指向某个已经不存在的容器IP别怀疑就是残留。5. 避坑总结与参数调优5.1 关键参数速查表生产环境真正影响锁行为的参数整理成表位置参数默认值影响建议OSDosd watch timeout30秒控制watch过期、陈旧锁自动抢占速度故障转移频繁可调10~15秒注意心跳流量librbdrbd_lock_timeout60秒librbd抢锁重试上限迁移/故障转移建议20~30秒镜像特性exclusive-lock开启决定是否启用自动锁多写者场景必须开启镜像特性journaling关闭依赖exclusive-lock增加日志开销按灾备需求单独开调参不是越激进越好。osd watch timeout调太短客户端续约watch的频率要提高OSD的watch流量明显增加高负载集群可能带来额外CPU和网络压力。我的思路是先明确故障转移的最坏可接受时延再倒推参数不要为了“快”而调。配套监控里加上锁等待延时的指标比盲目调参会更有依据。5.2 krbd和librbd对锁的不同态度对同一把锁内核krbd和用户态librbd表现很不同。krbd在rbd map时尝试拿锁拿不到立刻报错不会无限等待这是内核模块干净的脾气。大量使用内核挂载的环境要特别重视故障转移时的锁清理。librbd则更有耐心支持配置锁超时并重试逻辑更灵活代价是有时候你会觉得“它卡住了”其实人家在等锁。两者看的是同一把锁因为锁数据都在header对象的omap上。所以不用担心krbd和librbd各搞一套它们遵守的是同一套协议。真正需要区分的是业务场景内核挂载多见于物理机或容器宿主机直连librbd多见于qemu虚拟机两者的故障处理节奏完全不同。5.3 我压箱底的经验最后分享几条反复验证过的判断规则。第一看到busy报错别急着清锁先判断持锁客户端是否活着能ping通、能看进程、rbd status还有输出那锁大概率是健康的别动。第二异常退出后的锁watch超时后基本可以安全抢占但强制清理前看一眼mon日志里有没有blocklist事件免得清完锁节点又被踢。第三部署层面把挂载、卸载都做成明确启停脚本让锁走正常释放路径减少非正常残留。这篇文章写到这里刚好把我这些年和rbd镜像锁搏斗的体会讲完了。最后再分享一个小技巧容灾切换脚本里把rbd lock ls输出解析成机器可读格式加一步自动判断stale并清理的逻辑能省掉不少半夜被叫起来清锁的麻烦。锁这东西平时看着不起眼真出了事它就是那根压垮业务的最后一根稻草。
阅读完成 · 觉得有帮助?