device is busy 与 dmsetup remove_all 风险 简介围绕 Linux 存储管理中的多服务器挂载冲突这份文档聚焦同一存储设备挂载到两台服务器后一台已挂载而另一台无法挂载或提示“存储正忙”的典型故障面向 Linux 运维工程师、系统管理员及正在学习 Device-Mapper 机制的读者可用于日常排障与命令查阅。包内共 1 个 docx 文件压缩包大小约 11KB内容按问题现象、成因分析和处理方案组织重点讲解设备文件、映射关系以及 dmsetup 的使用思路并提到 dmsetup remove_all 等命令在清理映射、重新挂载场景中的作用。读者可借此理解多服务器访问同一存储时的冲突来源掌握从设备映射层排查、清理并恢复挂载的基本路径也能延伸了解 dmsetup 在快照、克隆和镜像管理中的用途亦可作为执行相关命令前的思路核对与速查参考。已有 1650 人学习适合作为存储故障排查的轻量资料。1. 存储正忙远端宕机只是触发条件真正攥着设备的是本地内核一台服务器掉电另一台却挂不上存储提示 device is busy——直觉会认为故障在扩散实际上这是 Linux 块设备栈在表达它自己的状态。同一块 LUN 被两条路径认领之后内核里留下的是一串block_device引用、一套 device-mapper 映射表、若干holders目录项并不是一条可断开的“连接”。远端有没有活着本地内核并不知道也不关心。搞懂这条引用链才能明白为什么一句dmsetup remove_all能让存储重新挂上也才能明白它为什么危险它删掉的是全机所有 device-mapper 映射包括你根分区底下那一层。这类故障不会撒谎但会误导方向。2. 从设备文件到 dm 表一次 I/O 到底被谁接住了2.1 /dev/sdb 只是门牌号gendisk 与 block_device 才是实体ls -l /dev/sdb输出里的8, 16不是装饰是内核索引这块设备的唯一坐标主设备号 8 归 SCSI 块设备次设备号 16 指向某块具体的盘。VFS 打开这个字符意义上的“文件”时走的是def_blk_fops把控制权交给块层块层再用major:minor在 bdev 的 inode 哈希里找到对应的block_device结构最终调到gendisk-fops-open。硬盘本身、HBA、多路径聚合出来的虚拟盘在这一层全都长一个样。真正决定“能不能打开”的是block_device上的两个字段bd_holder和bd_holders。前者记录当前持有者是谁一个指针文件系统类型、dm 目标或者别的内核子系统后者是持有计数。文件系统挂载时走mount_bdev最终以FMODE_EXCL方式打开块设备并把sb作为 holder 传进去。带排他标志的打开请求如果发现bd_holder已经被别人占了且不是自己直接返回-EBUSY。这就是“设备正忙”最原始的那一声。# 一块物理盘被谁“持有”了目录非空就代表有上层占用 ls -l /sys/block/sdb/holders/ # 空目录 → 没人持有可以直接 mkfs 或 mount # dm-2 → 已经被 device-mapper 拿走物理路径不再对外暴露 # 反向查某个 dm 设备的下游是谁、上游又是谁 ls -l /sys/block/dm-2/slaves/ /sys/block/dm-2/holders/s slaves往下看这块 dm 由哪些底层设备拼出来holders往上看谁又压在这块 dm 上面。这两个目录构成了一棵依赖树后面所有排查都围绕它展开。多路径场景里典型的样子是sdb、sdc两条物理路径的holders里都是dm-0而dm-0的holders里是dm-1LVM 的 PVdm-1的holders里是dm-2LV最终dm-2上挂着文件系统。任何一层没解开最上面那层就挂不上。提示holders是判断“谁占着”最快的入口比lsof和fuser更接近真相。用户态进程打开的是挂载点里的文件内核态持有者根本不会出现在lsof里。2.2 Device-Mapper 在块设备之上再套一层块设备Device-Mapper 的定位很容易被误解成“一个管理工具”实际它是块层里的一个框架允许你在已有的块设备之上用一张表描述出一块新的块设备。这张表由若干 target 行组成每行的格式是固定的四段式起始扇区、长度扇区数、target 类型、target 私有参数。内核拿到表后把新设备的每个 bio 按扇区区间路由到对应 target 的map回调上。# 看当前所有 dm 设备的映射表 dmsetup table # 线性映射把 8:16 从 0 扇区开始的 2097152 个扇区原样映射成 dm-0 # 0 2097152 linear 8:16 0 # 多路径同一个区间挂了两个底层设备轮询选路 # 0 2097152 multipath 0 0 1 1 round-robin 0 2 1 8:16 1000 8:32 1000 # 只看某一类的映射 dmsetup ls --target multipath dmsetup ls --tree # 用树形展示 dm 之间的父子关系multipath那行的私有参数看起来像天书拆开其实有规律第一个 0 是 feature 个数第二个 0 是硬件 handler 的参数个数接着的 1 是路径组数量再往后的round-robin 0 2 1 8:16 1000 8:32 1000描述了一个轮询路径组组内两条路径每条路径带优先级。这些参数不影响本文主线但要知道一件事表中每个底层设备的major:minor都是硬编码的。一旦底层设备号变化重启、换 HBA、驱动重新枚举老表就指向了错误的对象映射会以各种奇怪的方式失效。映射表不是静态的。dmsetup suspend可以把设备挂起、换表、再resume这是 LVM 扩容、快照、加密卷改密钥的通用套路。也正因为表可以随时换dm 设备在生命周期里会经历 create、load、suspend、resume、remove 五个阶段每个阶段都是一次 ioctl。2.3 “正忙”是本地引用计数不是远端握手这里是最反直觉的一层。FMODE_EXCL的排他判断完全发生在本地内核的block_device上它没有任何跨主机协商能力。也就是说远端服务器把存储写坏了、掉电了、内核 panic 了本地内核不会收到任何通知本地的bd_holders计数一个都不会减。反过来本地报device is busy也不代表远端正在访问这块盘。它只说明本地有东西没释放。那为什么“一台挂了之后另一台才挂不上”常见的因果其实是反的两台机器上原本都存在残留的半开状态只是其中一台一直撑着没暴露问题那台一崩业务切过来另一台需要重新mount此时才撞上自己本地早就存在的残留引用。所谓“挂了之后才出问题”往往是被崩掉的那台机器把问题从“潜伏”变成“显性”。本地残留的来源很集中按出现频率排上一次umount没成功但没人管multipathd 或 LVM 的pvscan周期性打开设备blkid、udev在热插拔时短暂持有设备读超级块旧的 dm 映射没有随着存储下线而消失仍然占着底层设备的 holder。最后这一条正是dmsetup remove_all想解决的场景也是它最容易误伤的地方。# 本地内核视角的引用计数Open 列大于 0 就说明有人在用 dmsetup info -c -o name,major,minor,open,segments,events # 输出示例 # mpatha 253 0 2 3 41 # vg0-lv0 253 1 1 1 12 # 第二行的 Open1 很可能是挂载点第一行的 Open2 则是 multipathd 挂载各占一个events列也值得留意它是设备的 event 序号每次 resume 或表变更都会增长用来和 udev 的 cookie 对齐。dmsetup命令卡住不动的情况九成是卡在等 udev 回这个序号。3. 双挂为什么会坏块层不拦你文件系统和 SCSI 预留才拦3.1 两个内核各自以为自己是唯一写者page cache 与 journal块层对“同一块盘被两台机器打开”这件事没有任何意见。它只负责把 bio 发下去不负责判断这块盘上是不是已经有一个文件系统被别人接管。这是分层设计的结果块层不该懂文件系统语义文件系统也不该管网络拓扑。于是同一块盘被两台机器各自挂载时两个内核会各自建立一整套私有状态页缓存、buffer_head、inode 哈希、extent 状态树以及最重要的日志journal。两边都认为自己拥有这块盘的元数据写权限都按自己的节奏把脏页刷下去。写 journal 的顺序在单机上是保证崩溃一致性的核心机制到这里变成了两个写者互相覆盖日志区。后果通常分三档轻度是目录项错乱、文件大小与实际块数不符中度是 journal 回放时把对方的已提交事务覆盖掉回滚出不存在的 inode重度是 extent 树指向已释放的块fsck跑完之后出现大量文件被塞进lostfound。这三档的共同特征是冲突当场不报错报错要等到下一次 mount 或fsck。所以“能挂上”从来不等于“没问题”。还有一层更隐蔽的顺序问题机器 A 崩掉时它缓存在内存里的脏页可能只写下去一半。机器 B 之后去 mount看到的是一个逻辑上从未存在过的中间状态。这种情况下mount甚至可能成功因为超级块的校验位恰好是对的。3.2 SCSI 持久预留真正意义上的“谁有权写”如果是块层之上没有文件系统的场景裸设备给数据库用冲突就不会以fsck报错的形式出现而是以 I/O error 的形式出现。这时候起作用的是 SCSI 协议里的持久预留Persistent Reservation。机制很直接某个 initiator 可以对自己能访问的 LUN 注册一个预留键并声明预留类型独占写、独占访问、全通道独占等。其他 initiator 发来的写命令会被目标端直接拒绝返回 sense key0x0BABORTED COMMAND或0x05加0x2ARESERVATION CONFLICT。这个拒绝发生在存储阵列的控制器里是真正的跨主机互斥不依赖任何客户端协作。但 POSIX 的mount命令不会自动发起预留也很少有部署默认开启。要用得靠sg_persist这类工具显式注册或者由集群件fence 机制在节点切换时下发。于是现实中的绝大多数双挂场景两边都处于“没有预留、没有锁、谁都能写”的状态。3.3 三种“忙”的报错要对号入座同一句“存储正忙”背后至少三条完全不同的路径处置方式也完全不同。把三者混为一谈是这类故障里最常见的翻车起点。报错形态产生层实际含义处置方向mount: /mnt: device is busy或EBUSY本地block_device的 holder 判定本机已有携带排他标志的持有者查holders、dmsetup info的 Open 列清理本地残留dmsetup remove返回Device or resource busydm 层的open_count/ holder 计数这个 dm 设备自身还被引用先umount停 multipathd再--deferredCannot open block device/ I/O errordmesg 里有Reservation conflict存储控制器另一台主机持有 SCSI 预留查预留键走 fence 流程释放wrong fs type, bad option, bad superblock文件系统层看到的是被污染的超级块或压根不是这个文件系统只读fsck -n先看不要急着-y表格里第二行和第一行经常同时出现因为 dm 设备的open_count里就包含了它作为块设备被上层文件系统以排他方式打开的那一次。清理顺序必须自外向内先解挂载点再停会主动打开设备的守护进程最后才动 dm 映射。4. dmsetup remove_all 的内核路径一次 ioctl 删掉了什么4.1 控制节点与 ioctl 命令集dmsetup是个薄客户端它自己不碰任何设备所有操作都通过一个特殊字符设备完成/dev/mapper/control。这个节点在早期由 udev 静态创建现在是 misc 设备主设备号 10。打开它下发ioctl内核里的 dm 核心执行动作回填结果结构体。命令集是一组固定编号的请求常用的几个# 打开调试输出看 dmsetup 每一步实际下发了哪个 ioctl、返回什么 dmsetup --debug remove_all 21 | head -40 # 典型输出会包含形如 # dm_task_run: ioctl DM_VERSION ... # dm_task_run: ioctl DM_LIST_DEVICES ... # dm_task_run: ioctl DM_DEV_REMOVE ... (mpatha)DM_LIST_DEVICES负责把内核里所有 dm 设备的名称、uuid、设备号捞出来DM_DEV_REMOVE负责删一个DM_TABLE_LOAD装载表DM_DEV_SUSPEND承担挂起与恢复两个语义传 flags 区分DM_DEV_CREATE建空设备。remove_all的实现就是先DM_LIST_DEVICES再对列表里每个名字循环下发DM_DEV_REMOVE。它没有任何智能筛选不看 target 类型不看是否被挂载。4.2 DM_DEV_REMOVE 的 EBUSY 分支与 deferred remove内核收到删除请求后先找到对应的mapped_device然后做一次忙碌判定。判定不通过就返回EBUSY客户端把 errno 翻译成那句熟悉的Device or resource busy。/* 简化示意删除路径上的忙碌判定非逐字源码 */ static int __dm_destroy(struct mapped_device *md) { /* open_count 累计的是本地内核对这块 dm 设备的引用 mount 时的排他打开、LVM 的 pvscan、blkid、multipathd 的探测都会 1 */ if (dm_open_count(md) || dm_holder_count(md)) return -EBUSY; if (dm_deferred(md)) { /* 已带 DEFERRED 标志不立即销毁挂到 workqueue 等最后一个引用 put 掉之后由回调完成销毁 */ dm_defer_remove(md); return 0; } /* 真正销毁摘表 → 释放各 target → 释放 gendisk 与动态 minor */ }这段逻辑解释了两种行为差异。不带--deferred的删除是“现在就要”忙碌就直接失败带--deferred的删除是“标记一下等你空了再删”它把设备从可见列表里摘掉但内核对象还留着直到最后一个引用释放。后者在清理残留时几乎是必选项因为总有些探测进程会在你删除的瞬间刚好打开了设备。# 先同步删失败就退化成延迟删除避免和探测进程抢时间窗 dmsetup remove --noudevsync mpatha 2/dev/null \ || dmsetup remove --deferred --noudevsync mpatha # --noudevsync 跳过等待 udev 回 cookie避免命令长时间挂住 # 代价是 /dev/mapper 下的符号链接可能残留需要 dmsetup udevcomplete_all 收尾--noudevsync和udevcomplete_all这一对参数值得记住。dm 设备删除后内核通过 netlink 通知 udev 删节点dmsetup默认会等 udev 回一个 cookie 确认。如果 udev 自己卡住比如规则里调用了会阻塞的外部脚本dmsetup就会毫无输出地僵在那里看起来像内核挂了其实只是在等用户态。4.3 remove_all 的真实执行顺序与一个更安全的替代脚本把上面的机制串起来remove_all的实际效果可以精确描述遍历所有 dm 设备对每一个尝试销毁销毁前检查本地引用计数计数为零的当场消失计数非零的报EBUSY并继续处理下一个。它不会帮你umount不会停掉 multipathd不会区分这块 dm 是残留的多路径映射还是正在服役的 LVM 逻辑卷。这就是它危险的地方。LVM 的 PV、LV、快照、thin pool加密卷的 crypt target容器的 overlay 底层全都是 dm 设备。一条remove_all下去只要某个 LV 恰好没被挂载比如备份卷、未激活的 LV它的映射就会被无声删掉/dev/mapper/vg0-lv0直接消失。数据还在盘上但设备节点没了业务侧看到的是“卷不见了”。更稳妥的做法是按 target 类型和依赖关系筛选只动真正要动的那一层#!/usr/bin/env bash # 只清理 multipath 层的残留映射保留 LVM / crypt / 根分区所在的 dm set -euo pipefail dmsetup ls --target multipath 2/dev/null | awk {print $1} | while read -r name; do # 该映射上层是否还压着别的 dmLVM 常常建在 multipath 之上 dev/dev/mapper/$name sysfs/sys/block/$(basename $(readlink -f $dev)) holders$(ls -A $sysfs/holders 2/dev/null || true) if [ -n $holders ]; then echo skip $name: 上层仍有 [$holders] continue fi # 先同步删失败退化为延迟删除全程不等 udev dmsetup remove --noudevsync $name 2/dev/null \ || dmsetup remove --deferred --noudevsync $name echo removed $name done脚本里三个判断各有分工。dmsetup ls --target multipath把范围收敛到多路径映射避免误伤 LVM 与加密卷holders检查保证不会把上层还依赖着的底层映射抽走抽走之后上层会变成一堆 I/O error--noudevsync避免命令挂死。如果环境里 multipathd 是开着的还要在动映射之前先停止它或执行multipath -F否则删完几秒内它就会重新扫出路径、把映射建回来让人误以为删除没生效。5. 排查与避坑从 lsblk 到 holders 的固定顺序5.1 六条命令定位持有人顺序不能颠倒现场排查最怕的是上来就删。固定顺序的价值在于每一步都能排除一类可能并且不会破坏现场证据。# 1) 全局拓扑谁挂在谁上面文件系统在哪一层 lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,HCTL # 2) dm 层的依赖树一眼看出多路径、LVM、加密的嵌套关系 dmsetup ls --tree # 3) dm 设备的引用计数Open 列非零的先记下来这是嫌疑人名单 dmsetup info -c -o name,open,segments,events # 4) 反查 holders某个底层设备到底被几层压着 for d in /sys/block/sd*/holders /sys/block/dm-*/holders; do [ -n $(ls -A $d 2/dev/null) ] echo $d - $(ls -A $d) done # 5) 挂载表里有没有指向已消失设备的僵尸条目 findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS | grep -i mapper\|deleted # 6) 最后才查进程确认没有进程把挂载点当 cwd 或持有打开的文件 lsof D /mnt/data 2/dev/null | head fuser -vm /mnt/data第 4 步和第 6 步的分工要清楚holders查的是内核态持有者lsof查的是用户态持有者。少了任何一个都可能得出“没人用它”的错误结论然后删除时继续EBUSY。5.2 五条踩坑记录现象、原因与处置第一条remove_all把 LVM 逻辑卷一起删了。现象执行完之后存储确实能挂了但另一套业务报“逻辑卷设备不存在”/dev/mapper/下少了几个节点vgs里卷组还在。 原因LV 本质就是 dm 设备remove_all不做任何类型区分未挂载的 LV 一旦open_count为零就会被直接销毁。 处置立即用vgchange -ay vg重新激活卷组映射会按元数据重建数据本身不受影响之后改用第 4 章那种按 target 筛选的脚本永不使用remove_all。第二条删完几秒映射又自己长回来了。现象dmsetup remove mpatha返回成功dmsetup ls里确实没了但过十几秒mpatha又出现EBUSY复发。 原因multipathd 在后台周期性扫描 SCSI 设备发现有可用路径就重建映射和手工删除形成拉锯。 处置先systemctl stop multipathd或multipath -F清空所有映射确认进程真的停了再动 dm清理完根据业务需要决定是否恢复服务。第三条dmsetup命令挂住没有任何输出。现象敲下去之后光标一直闪十几分钟不返回Ctrl-C也退不干净。 原因默认要等 udev 回 netlink cookie而 udev 规则链里有阻塞操作常见于规则脚本里调用了会等待网络或锁的命令。 处置换终端执行dmsetup udevcomplete_all把积压的 cookie 一次性清空或者后续操作统一加--noudevsync事后再用udevadm trigger补节点。第四条重启之后故障自动复发。现象手工清理干净、挂载正常一重启又变成两台抢一块盘。 原因/etc/fstab里还留着基于设备路径的条目systemd 生成了对应的.mount单元开机会无条件挂载甚至因为设备名漂移挂到了错误的盘上。 处置把 fstab 改成noauto或直接注释掉用systemctl list-unit-files *.mount确认没有残留单元设备标识统一换成UUID或LABEL避免/dev/sdb这种会漂移的写法。第五条以为删掉映射就安全了结果文件系统早就坏了。现象清理之后能正常mount业务跑了几小时开始报“Structure needs cleaning”日志里出现EXT4-fs error或者XFS (dm-0): Corruption detected。 原因双挂期间两边都在写journal 和 extent 树已经被覆盖过mount成功只代表超级块还能读不代表元数据自洽。之前的清理只是解开了引用没有修复数据。 处置立刻只读挂载或umount用fsck -n不做任何修改先评估损坏范围再决定修复还是从备份恢复-y自动修复在元数据大面积损坏时会把情况弄得更糟。真正的前置动作应该是双挂发生时就阻断写入而不是事后清理。6. 设计取舍remove_all是后悔药根治要靠“谁有权写”的约定把整条链路摊开之后一个结论就很难回避Linux 在块层不做跨主机互斥这不是疏忽而是刻意的分层取舍。块层如果要去判断“这块盘是不是被别的机器挂了文件系统”它就必须懂文件系统语义、懂网络拓扑、懂集群成员关系——这些东西不该出现在submit_bio的路径上。代价就是把互斥的责任推给了上层而上层普通本地文件系统默认假设自己是唯一写者。这个假设在单机时代永远成立在共享存储时代需要由部署者补上。补的方式有三条路各有代价。第一条是 SCSI 持久预留加 fence由存储控制器做硬互斥节点故障时由集群件下发预留键抢占语义最干净但需要阵列支持和一套可靠的 fence 流程误 fence 会直接把好节点打死。第二条是换成集群型文件系统引入分布式锁管理器每个 inode 的元数据变更都要走锁仲裁正确性有保障代价是锁开销和部署复杂度小文件密集场景的性能下降很明显。第三条是只读挂载或单写多读把第二台机器的挂载改成ro或者在切换流程里强制先umount再交接实现成本最低但对流程纪律要求最高——它依赖人不出错。方案互斥层正确性性能开销适用场景SCSI 持久预留 fence存储控制器强极低裸设备数据库、双机热备集群文件系统分布式锁管理器强中等偏高多节点同时读写同一目录只读挂载 / 严格交接流程运维流程依赖执行无主备切换、备份节点dmsetup remove_all无不解决正确性无仅清理本地残留映射表格最后一行不是方案是止血。它能解决的是“本地引用没释放导致挂不上”解决不了“两个内核同时写坏了元数据”。把这两件事分开看很多现场决策就清楚了先止血让业务起来再回头做数据校验最后补上真正的互斥机制。顺序反了就是用可用性换一个迟早要还的债。有个容易被忽略的细节dm 表里硬编码的major:minor让映射本身很脆弱。底层设备重新枚举、HBA 换槽位、驱动加载顺序变化都会让老表指向错误的设备。这也是为什么很多“重启后就好了”或“重启后更糟了”的玄学现场最后都追溯到一张没被清理的旧表。判断方法很简单dmsetup table里的设备号和lsblk当前的实际设备号对不上那张表就该作废了。我自己的习惯是任何涉及共享 LUN 的变更先跑一遍dmsetup ls --tree和 holders 反查把依赖树截图留档再动手删除操作一律按 target 过滤并加--noudevsync绝不打remove_all这四个字。这个习惯是被一次误删未激活 LV 的事故逼出来的——当时业务没受影响但备份卷消失了两天才被发现。从那以后我每次碰共享存储都强制走一遍“拓扑留档、按类型筛选、删后校验”这三步。希望帮到你。本文还有配套的精品资源点击获取