监控平台同时弹出一堆告警:数据库状态是 SUSPECT,磁盘 S.M.A.R.T. 出现了“当前挂起扇区警告”,虚拟机的电源操作提示“无法挂起”,后台日志里一堆线程池任务阻塞堆积。如果你天天跟这些系统打交道,会发现“挂起”和“阻塞”这两个词几乎无处不在,但它们在不同技术栈里的含义完全不是一回事。
这篇文章不是单纯背操作系统状态机,而是把这些年我在实际环境里遇到的六类高发问题串起来——从 Linux 调度器里进程为什么卡在 D 状态、中断里为什么不能睡眠,到线程池的阻塞队列怎么选、SQL Server 为什么把数据库标记为“挂起”,再到硬盘扇区和 PCIe 直通虚拟机的各种“挂起”异常。每个案例背后都藏着这两个概念的真正边界,看完之后你会对系统为什么卡、能卡多久、能不能恢复有一个更踏实的判断。
1. 从调度器视角重新认识挂起和阻塞的本质区别
1.1 进程状态机:S、D、T 分别意味着什么
很多人混淆“挂起”和“阻塞”,是因为在 Windows 的任务管理器里,挂起(Suspend)一个进程之后,它看起来和阻塞一样都不消耗 CPU。在 Linux 里,你用 ps aux 会看到进程状态列有很多字母,其中最常见的几个是:
S (TASK_INTERRUPTIBLE):可中断睡眠,进程在等某个条件,比如等 IO 完成、等 socket 数据,能被信号唤醒。D (TASK_UNINTERRUPTIBLE):不可中断睡眠,通常是等磁盘 IO,连kill -9都拿它没办法。T (TASK_STOPPED):进程被停止,对应 Ctrl+Z 或SIGSTOP,此时进程被“挂起”在某个执行点。
阻塞的本质,是进程“想运行却暂时不具备运行条件”,于是它主动把自己从 CPU 的运行队列中摘下来,进入等待队列。只要等待条件满足,内核就会把它重新放回运行队列,这是一条可自动恢复的路径。
挂起的本质,是进程从内核调度器的管理范围中完全脱离开来,它不是“等待某个资源”,而是被外部力量(信号、调试器、系统休眠)按下了暂停键,未来能否恢复、何时恢复,不由进程自身决定,而由外部控制方决定。
1.2 判断进程是阻塞还是挂起的实操方法
有一次排查线上服务假死,ps aux 看到 Java 进程 CPU = 0,状态是 Sl。top 里看不出它到底在干什么,这时要更进一步看它阻塞在哪个内核函数上:
bash复制# 查看进程当前状态和等待通道
cat /proc/<pid>/status | grep -E "State|voluntary_ctxt_switches|nonvoluntary_ctxt_switches"
# 查看进程阻塞在内核的哪个函数
cat /proc/<pid>/wchan
wchan 输出的是内核函数名。如果显示 pipe_wait,说明进程阻塞在管道读写;如果显示 do_exit,说明进程处在退出流程;如果显示 0,说明它要么在运行,要么在内核态做不可中断的事。
voluntary_ctxt_switches(主动上下文切换)和 nonvoluntary_ctxt_switches(被动上下文切换)这两个计数器也很有意思:主动切换多,说明进程大量时间在等待(阻塞),被动切换多,说明进程被时间片打断,属于 CPU 密集型。用这个可以快速定位一个“假死”的进程到底是真阻塞,还是只是没抢到 CPU。
1.3 一个容易误判的场景:CPU 占用为 0 不等于挂起
我记得有一次处理一个“进程挂起”工单,查下来其实是进程进入了 D 状态,卡在 NFS 文件系统的不可中断 IO 上。为什么 NFS 会引发 D 状态?因为底层 IO 路径不允许在等待网络文件系统响应时被信号打断,否则会导致文件系统状态不一致。这就导致只要 NFS 服务端不响应,客户端进程可以卡在 D 状态数小时,表面看起来就像“挂起”一样,实际它只是阻塞在一个永远不会返回的 IO 上。
阻塞在等待队列中时,内核知道它在等谁,也给它记了账,调度器完全掌控局面;挂起则更像进程的“快照”被人为冻结了,信号处理、定时器、资源回收都暂停。所以排查时,不要看到 CPU 是 0 就说进程挂了,先 cat /proc/<pid>/status 看进程状态,再查 wchan 和内核栈,能少走很多弯路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 中断为什么不能阻塞:中断上下文的硬约束
2.1 中断处理程序没有“睡眠”的资格
很多人刚接触中断时都有一个疑问:既然中断处理函数也是代码,为什么不能在里面调用 msleep() 或者等待一个信号量?我在代码评审里看到过不少人试图在中断上下文里加锁等待,结果内核直接报 BUG: sleeping function called from invalid context。
原因要从进程和中断的差异说起。一个普通进程能阻塞,是因为内核为它保存了完整的上下文(寄存器、内核栈、task_struct),调度器可以把 CPU 让给别的进程,等条件满足再切换回来。但硬中断处理程序运行在中断上下文里,它没有自己的 task_struct,也没有独立的内核栈上下文切换能力,它是在当前被打断的进程的上下文中临时插入执行的。如果中断处理程序“阻塞”,调度器根本不知道该把谁切出去、把谁切回来——它连被中断的进程的执行现场在中断返回时如何衔接都保证不了。
所以更准确的说法是:中断处理程序不是“不能阻塞”,而是“阻塞”这个机制在中断上下文里压根就不存在。它只能做三件事:快速处理,标记状态,然后尽快返回;或者把耗时操作推迟到下半部处理。
2.2 中断里的延迟工作该放哪:softirq、tasklet、工作队列怎么选
如果硬中断里不能做耗时操作,那网卡收包后的协议栈处理、块设备驱动的 IO 完成处理,这些比较重的活放在哪?
Linux 内核对中断处理做了分层设计:
- 硬中断(hardirq):只能做最少的工作,比如读硬件寄存器、清中断标志、把数据放到 CPU 的 softirq 待处理队列。
- softirq:在硬中断返回后紧接着执行,仍然跑在中断上下文,但可以处理更多工作,比如网络收包协议栈。
- tasklet:基于 softirq 实现,运行在软中断上下文,不能睡眠。
- 工作队列(workqueue):运行在进程上下文,可以睡眠、可以阻塞、可以等锁。
- 线程化中断(threaded IRQ):内核为中断处理函数单独创建内核线程,把整个处理过程放进进程上下文,不阻塞硬中断返回。
选型逻辑其实一句话就能说清楚:能不能睡眠,决定了用哪个层级。需要等锁、等 IO、操作用户态内存的,必须放到工作队列或线程化中断;只是纯 CPU 计算、不需要睡眠的,可以放 softirq/tasklet 上减少调度开销。
2.3 实测网络中断延迟异常的排查链路
之前碰上过一个网络延迟飙升的问题,/proc/interrupts 里看到 eth0 的硬中断集中打在了 CPU0 上,CPU0 的 si(软中断)占用到了 90% 以上。由于硬中断里不能阻塞,但软中断如果处理太重,一样会让 CPU0 陷入“半中断状态”,其他进程在 CPU0 上无法被调度。
排查链路是这样的:
bash复制# 查看中断分布
cat /proc/interrupts | grep eth
# 查看软中断统计
cat /proc/softirqs
# 查看每个 CPU 的负载
mpstat -P ALL 1
确认是单 CPU 软中断热点后,把网卡队列的 IRQ affinity 打散到多个 CPU 上,同时开启 RPS(Receive Packet Steering)让收包软中断负载均摊。改完之后 mpstat 的 si 从单核 90% 降到各核 20% 以下,应用层延迟明显回落。
从这个案例里能直观理解“中断为什么不阻塞”的另一面:中断处理路径上任何可能阻塞的操作都要被拆出去,否则 CPU 会长期停留在中断上下文中,普通进程集体得不到调度,整个系统的表现比单个进程阻塞要可怕得多。
3. 线程池的阻塞队列选择:任务被卡住时的三种现场
3.1 阻塞队列在这里扮演什么角色
转向应用层并发编程,线程池里的阻塞队列是“阻塞”概念用得最密集的地方。Java 的 ThreadPoolExecutor 核心设计里,任务提交给线程池后,不一定立刻执行:
- 如果当前线程数小于核心线程数,创建新线程执行任务。
- 如果核心线程已满且队列未满,任务放入阻塞队列等待。
- 如果队列已满且线程数未达最大线程数,创建临时线程执行任务。
- 如果队列已满且线程数已达最大线程数,触发拒绝策略。
这里的阻塞队列,作用是削峰填谷,让任务的提交速率和处理速率解耦。队列的“阻塞”体现在:线程池里的工作线程在队列为空时执行 take() 会阻塞等待;外部提交任务时如果队列满,执行 put() 会被阻塞或者触发拒绝。
3.2 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 的真实差异
选队列类型时,很多人只背结论,不理解背后的行为差异。我在做过几个线程池压测之后,把常见队列的差异梳理成一张对比表:
| 队列类型 | 是否有界 | 核心行为 | 典型场景 |
|---|---|---|---|
| ArrayBlockingQueue | 有界,需指定容量 | 基于数组,put/take 共用一把锁,容量固定 | 明确系统能承受的最大积压量,防止内存暴涨 |
| LinkedBlockingQueue | 默认无界,也可指定容量 | 基于链表,put/take 两把锁,吞吐通常稍高 | 默认无界有风险,指定容量后与 Array 类似 |
| SynchronousQueue | 无缓冲 | put 必须等待 take,任务不排队直接交付 | 希望任务不积压,立即拒绝或立即执行 |
| DelayQueue | 无界 | 元素延迟到期才能取出 | 定时任务的延迟队列 |
| PriorityBlockingQueue | 无界 | 按优先级出队 | 不是按提交顺序,而是按优先级处理 |
实际生产环境里我最常用的选择是 ArrayBlockingQueue,因为“有界”这两个字太重要了。系统负载永远有上限,无界队列会让任务堆积在内存里,表面上是线程池在阻塞,实际是整个服务的内存被缓慢耗尽。
3.3 一次数据同步任务堆积的事故复盘
有一次做数据同步服务,核心线程 8 个,最大线程 8 个,队列用了默认无界的 LinkedBlockingQueue。某天上游接口突然慢下来,单个同步任务的耗时从 200ms 涨到 30 秒,队列里积压了上百万个任务,内存占用节节攀升,最终导致 OOM。
复盘时看得很清楚:线程池不会拒绝任何任务,因为无界队列永远放得满,最大线程数设置成多少都不会触发拒绝策略。直接把队列改成有界队列,初始容量设置 2000,配合 CallerRunsPolicy 拒绝策略——当队列满了之后,新任务直接用提交线程执行,相当于让生产者反向感受到下游压力,从而触发上游限流。
这个问题本质上是对“阻塞”边界理解不透。线程池的阻塞队列设计初衷是缓冲,但缓冲必须有个度。判断队列容量可以用这个公式做估算:队列容量 = 任务平均处理耗时 × 峰值每秒任务数 × 容忍的额外等待秒数。如果单任务平均处理 100ms,峰值每秒提交 200 个任务,希望积压最多等待 5 秒,那队列容量就是 0.1 × 200 × 5 = 100。
排查线程池阻塞情况时,不要只靠猜,用 JDK 自带的工具能直接看到实时状态:
bash复制# 打印线程栈,查看工作线程阻塞在哪
jstack <pid>
# 代码里周期性打印核心指标
// ThreadPoolExecutor 自带方法
// executor.getActiveCount() 活跃线程数
// executor.getQueue().size() 当前积压任务数
// executor.getCompletedTaskCount() 完成任务数
// executor.getTaskCount() 历史任务总数
jstack 里看到工作线程停在 java.util.concurrent.locks.AbstractQueuedSynchronizer.park 这类调用上,就是标准的阻塞等待行为,说明线程池正在等新任务或等锁释放,是正常的线程池空闲状态,而不是异常挂起。
3.4 拒绝策略不是越激进越好
线程池还有一类常见的坑:AbortPolicy 默认直接抛 RejectedExecutionException,如果调用方没捕获,任务就丢了,而且整个业务请求失败。DiscardPolicy 静默丢弃更危险,数据丢了 log 里什么都看不到。我个人的配置习惯是:核心业务使用 CallerRunsPolicy,让提交线程自己去跑,至少不丢任务;非核心打点、日志上报类任务可以允许丢弃,但要配套打一条告警日志方便统计丢弃量。
一个容易被忽略的细节是线程池的 allowCoreThreadTimeOut(true),设置之后核心线程空闲也会被回收。对某些按小时有明显波峰波谷的系统,这能显著降低空闲时段的资源占用。这个方法的本质是让线程池的“核心线程也允许阻塞超时退出”,和“核心线程永不超时”是两种不同的资源控制策略,按业务特点选就好。
4. 数据库文件被挂起:suspect 模式的成因与恢复顺序
4.1 SQL Server 的 SUSPECT 到底是什么“挂起”
数据库层面的“挂起”,最典型的是 SQL Server 中数据库状态变成 SUSPECT,中文文档直接把这种状态翻译为“挂起”或“可疑”。一旦数据库进入 SUSPECT,用户端会看到数据库无法访问,所有会话都阻塞在访问数据库的请求上。
需要先区分:SQL Server 的 SUSPECT 和操作系统进程的挂起有本质差别。数据库进入 SUSPECT,是 SQL Server 在启动恢复(recovery)过程中发现数据库元数据损坏或日志链断裂,无法安全地完成崩溃恢复,为了保证数据一致性,主动把数据库置于不可用状态。此时任何对数据库的读写请求都会阻塞,但数据库引擎本身并没有停止,其他数据库仍然正常工作。
最常见的触发条件有三个:硬件故障导致数据页写入不完整;覆盖了主数据文件 .mdf 但日志文件 .ldf 已经损坏或缺失;手动删除了日志文件后重启 SQL Server。热搜词里提到的“data 文件下的 .mdf 文件覆盖后被挂起,不认 .ldf”,就是这个典型场景。
4.2 为什么覆盖 .mdf 后“不认 .ldf”
数据库文件不是简单的数据容器。SQL Server 的每个数据库文件头部都记录了该文件的标识符,数据库内部通过 LSN(日志序列号)来衔接数据和日志。正常情况下的崩溃恢复会从 .ldf 文件里读取最后一次检查点后的日志,回放所有未完成事务,保证数据落盘到一致状态。
如果你用另一个数据库的 .mdf 文件覆盖了当前数据库的 .mdf,但 .ldf 还是原来那个,SQL Server 启动时会发现数据文件的 DBID、文件 GUID、LSN 段和日志文件的记录完全对不上。这个完整性检查失败后,recovery 过程无法安全推进,引擎直接把数据库标记为 SUSPECT。
所以“不认 .ldf”不是 SQL Server 在闹脾气,而是它在数据文件和日志文件不一致的情况下拒绝做任何破坏性操作。如果它不做检查强行恢复,轻则事务回滚错乱,重则整个数据库一致性崩溃。
4.3 恢复顺序:从应急模式到完整修复
遇到数据库进入 SUSPECT,DBA 的第一反应别是删库跑路,先按下面的顺序处理,每一步都要做备份或快照。比如磁盘快照可以用 lvcreate 或云平台快照,目的是给当前脏状态留个底,以便失败后还能回到原点。
先查看数据库状态和错误日志:
sql复制-- 查看数据库状态
SELECT name, state_desc FROM sys.databases;
-- 查看当前数据库的恢复模型和日志大小
DBCC SQLPERF(LOGSPACE);
-- 查看错误日志最后部分
EXEC sp_readerrorlog 0, 1, 'SUSPECT';
确认损坏范围后,把数据库切换到紧急模式,让它允许只读访问,方便评估数据是否仍然可读:
sql复制ALTER DATABASE [YourDB] SET EMERGENCY;
ALTER DATABASE [YourDB] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
-- 检查数据库物理和逻辑完整性
DBCC CHECKDB ([YourDB]) WITH NO_INFOMSGS, ALL_ERRORMSGS;
DBCC CHECKDB 会输出大量错误信息,需要重点看有没有 Could not repair 这类字样,同时观察错误码是页面级损坏还是元数据损坏。如果只是页面级损坏切日志可用,可以尝试修复:
sql复制-- 使用修复级别(最后手段,会有数据丢失)
ALTER DATABASE [YourDB] SET ONLINE;
DBCC CHECKDB ([YourDB], REPAIR_ALLOW_DATA_LOSS) WITH NO_INFOMSGS;
REPAIR_ALLOW_DATA_LOSS 这个名字起得很直白,它不是无损失修复,会删除损坏页对应的数据行。所以执行前必须告知业务方可能丢数据,执行后立即做一次完整备份。
4.4 .ldf 文件确实没了,怎么重建日志
如果日志文件本身丢了或者完全损坏,修复思路要换一下。先把数据库设置为紧急模式,然后删除损坏日志文件的逻辑引用,让 SQL Server 重建日志:
sql复制ALTER DATABASE [YourDB] SET EMERGENCY;
ALTER DATABASE [YourDB] SET SINGLE_USER;
-- 删除崩溃的日志文件逻辑
DBCC REBUILD_LOG('YourDB', 'D:\Data\YourDB_log.ldf');
-- 将数据库恢复为多用户模式
ALTER DATABASE [YourDB] SET MULTI_USER;
重建日志之后,数据库完整性检查一定要做一遍。没有原日志的情况下,最后提交的事务无法完整恢复,数据停留在离最后检查点最近的一致状态。丢失多少数据,取决于日志被删时还有多少未写入数据文件的事务,这点在向业务方汇报时要坦诚说明。
数据库的“挂起”状态本质上是数据库引擎自我保护性的强制阻塞,它宁可拒绝访问也不让你读到损坏数据。恢复工作真正要理解的是 SQL Server 把文件对不上的深层原因,而不是为了恢复而把数据库硬掰回 ONLINE。
5. 硬盘的“当前挂起扇区警告”:C5 和 05 到底谁更致命
5.1 S.M.A.R.T. 属性里的“挂起”指的是什么
磁盘告警里的“当前挂起扇区”其实是 S.M.A.R.T. 属性 C5 Current_Pending_Sector 的中文翻译。这是个让人困惑的命名——它既不是进程挂起,也不是存储库挂起,而是磁盘固件内部的一种扇区管理状态。
机械硬盘在读写过程中如果发现某个扇区的 ECC 校验失败,不会立刻判定这个扇区损坏,而是把它标记为“待处理”(pending)。下次如果对这个扇区有写入操作,固件会尝试写一次,如果写入成功,C5 就减一,这个扇区重新可用;如果写入继续失败,这个扇区就会被重映射到备用扇区,C5 减一、05 Reallocated_Sector_Ct 加一。
所以“挂起”在这里是一个临时状态:扇区当前读不稳定,固件在等待下一次写入来测试它是否还能用。如果 C5 持续不为零,说明有扇区处在读不出来的边缘,这是磁盘物理退化的早期信号。
5.2 用 smartctl 主动验证而不是等告警
很多服务器自带的 RAID 卡会屏蔽透传 S.M.A.R.T. 信息,系统层的工具可能看不到原始属性。对直通盘(JBOD 模式或单盘),可以直接查:
bash复制# 查看磁盘 S.M.A.R.T. 信息
smartctl -a /dev/sda
# 只看关键属性行
smartctl -A /dev/sda
输出里重点看几个属性:
| ID | 属性名 | 临界值 | 含义 |
|---|---|---|---|
| 05 | Reallocated_Sector_Ct | 36(视厂商而定) | 已重映射的坏扇区数,增长代表物理坏道在扩散 |
| C5 | Current_Pending_Sector | 0 | 当前待重映射扇区数,“挂起”的待确认扇区 |
| C6 | Offline_Uncorrectable | 0 | 离线扫描发现不可纠正的错误扇区数 |
| 10 | Spin_Retry_Count | 0 | 电机起转重试次数,增长说明机械部分可能出问题 |
| 197 | Current_Pending_ECC | 0 | 某些厂商另用此编号,语义类似 C5 |
Raw_Value 是原始计数值,Value 是归一化后的健康分(100 满分,低于 Threshold 判定 FAIL)。很多只看 Value 的人会被误导,因为一个本来 100 分的盘坏几个扇区后 Value 可能还有 92,看起来很正常,但 Raw_Value 已经从 0 涨到了几十。
5.3 C5 和 05 同时出现时的应急处理顺序
处理时区分场景很重要。如果只是 C5 有值、05 还是 0,说明还没形成永久坏道,可以尝试主动触发一次全盘写入,让固件对每个 pending 扇区做重写测试。比如用 dd 把文件系统空余区域写一遍,或者在磁盘无业务时做一次全盘写零,往往能清掉一部分假性 C5。
如果 05 已经在增长,且 C5 也持续存在,这说明磁盘的备用扇区池正在被消耗,坏道区域在扩散,这已经不是软件能修复的问题。处理顺序建议是:
- 立即确认数据有没有在 RAID 阵列之外做过备份。
- 有热备盘就直接替换,让 RAID 卡做重建。
- 单盘环境先用
ddrescue将全盘镜像到一块好盘,再对镜像盘做数据提取。 - 不要反复用坏盘做长时间读写测试,每一次大量读写都是在加速坏道扩散。
bash复制# 用 ddrescue 对整盘做镜像,日志文件记录错误位置
ddrescue -d -r 3 /dev/sdb /dev/sdc rescue.log
-d 是直接 IO 读取,跳过系统缓存;-r 3 是对失败扇区重试三次。跑完之后看 rescue.log 里的错误大小,能判断这次损坏的严重程度。我见过很多人在 C5 只亮黄灯时心存侥幸继续跑业务,最后真到 05 涨到阈值、系统 IO 卡死再想抢救,盘已经连读都读不出来了。
硬盘的这个“挂起”状态给了你一个时间窗口,但窗口的长度完全不确定。它可能撑几个月,也可能几个小时后就直接重映射失败。看到 C5 告警唯一正确的动作就是备份、备份、再备份,然后尽快换盘。
6. PCIe 直通下虚拟机无法挂起与 vMotion:硬件绑定的反向约束
6.1 为什么虚拟机“挂起”操作会被禁用
虚拟化平台里的“挂起”和操作系统进程挂起是另一个维度的事。虚拟机挂起(Suspend)是 hypervisor 把虚拟机的完整运行状态保存到磁盘,包括 vCPU 寄存器、内存内容、虚拟设备状态,下次通过恢复(Resume)操作从快照点继续运行。
但如果虚拟机直通了 PCIe 设备——比如 NVMe 硬盘、GPU、专用网卡——情况就复杂了。直通(Passthrough)意味着物理设备被直接分配给单个虚拟机,hypervisor 不再虚拟化这台设备的中断、DMA 和寄存器行为。此时虚拟机挂起需要把 PCIe 设备的内部状态也一并保存,但这台设备是真实硬件,不是软件模拟出来的,它的内部状态(固件上下文、DMA 队列、门铃寄存器内容)不是 hypervisor 都能读取的。
所以我遇到过 vSphere 平台上给虚拟机直通了一款 NVMe 控制器后,电源操作里的“挂起”按钮直接置灰,操作提示就是热搜词里那句:“存在 PCI/PCIe 直通设备时,部分虚拟机操作将不可用。您无法挂起、通过 vMotion 迁移该虚拟机。”这其实不是一个 bug,而是 hypervisor 做不到安全地保存和恢复物理设备状态,干脆在功能层面禁用了相关操作。
6.2 设备状态保存为什么这么难:以 NVMe 为例
以 NVMe 为例,一块直通给虚拟机的 NVMe 盘在运行中,它的控制器可能已经在处理队列里积压了大量命令,缓存里有未落盘的数据,固件可能有正在进行的内部垃圾回收或磨损均衡任务。hypervisor 想插进来“冻结”这块盘,需要能拿到控制器的内部状态,包括:
- 提交队列和完成队列的头尾指针位置;
- 门铃寄存器里尚未被控制器消费的命令;
- 可能仍在 DRAM 缓存里没写到 NAND 的数据(如果没启用 FUA);
- 控制器正在执行的内部后台任务。
这些状态对 hypervisor 来说是一个黑盒。就算强行触发 PCIe 的 PM(Power Management)状态切换,也保不齐设备固件是否完整实现了状态保存。所以绝大多数 hypervisor 采取的策略就是:检测到有 PCIe 直通设备,直接禁止挂起,避免后续恢复时出现设备状态不一致。
6.3 vMotion 迁移的限制与规避手段
vMotion 能迁移虚拟机,依赖的是源宿主和目的宿主上虚拟机的执行状态同步,包括内存页面的迭代拷贝和最终切换时设备状态的移交。对普通虚拟设备来说,hypervisor 能完整模拟设备状态,但对直通设备,源宿主机无法把它内部状态完整抽取出来再注入到目的宿主的新设备上。
即使是同一型号的设备,不同物理实例的状态也不能简单转换。比如直通 GPU 的显存内容、DMA 页表映射,都是绑定物理设备的。所以在 vSphere 上,直通了 PCIe 设备的虚拟机会被自动排除在 vMotion 之外。
规避手段需要分场景取舍:
第一种是换用 SR-IOV。SR-IOV 让物理设备暴露多个虚拟功能(VF),每个 VF 可以分配给虚拟机,但配置和管理仍由物理功能(PF)统一控制。SR-IOV 设备的状态保存由硬件参与,所以部分平台支持开启 SR-IOV 的虚拟机做迁移,前提是源目的宿主机都有同一型号的物理设备。
第二种是先卸载直通设备再做维护操作。例如在 vSphere 中先把 PCIe 设备从虚拟机上移除,然后就能正常挂起或 vMotion,迁到目的宿主机后再重新添加设备。这个操作的代价是虚拟机需要关机或设备驱动需要重新初始化,对业务连续性有影响,但总比无法迁移强。
第三种是改用软件定义的 IO 虚拟化。有些现代设备提供可迁移的 vDPA(virtio data path acceleration)模式,让数据面走硬件加速,控制面保持 virtio 标准接口,这样设备状态可以由软件动态保存。不过 vDPA 的支持还依赖设备固件和平台版本,不是所有设备都具备。
6.4 KVM 环境下怎么确认设备可迁移性
KVM 环境下用 VFIO 做 PCIe 直通已经很成熟,但同样面临挂起和迁移的限制。想确认当前设备到底能不能随虚拟机迁移,要先看设备是不是在 VFIO 管理下:
bash复制# 查看 VFIO 相关内核模块
lsmod | grep vfio
# 查看直通设备所在 IOMMU group
lspci -nnk | grep -i nvme
ls /sys/kernel/iommu_groups/
ls /sys/kernel/iommu_groups/ 输出里,如果一个 IOMMU group 里除了你的设备还有其他无关设备,那说明无法安全隔离,VFIO 直通可能失败。这个 group 是 IOMMU 做 DMA 重映射的最小单位,kernel 必须保证 group 内所有设备都被隔离或全部直通给同一虚拟机,才能保证安全。
KVM 对虚拟机做 suspend(比如 virsh suspend)时,如果虚拟机里有 VFIO 直通设备,默认行为同样不可靠。管理工具可能会拒绝执行,或者执行后恢复时设备状态异常。所以在 KVM 环境里维护直通虚拟机,通常做法是先 virsh detach-device 卸下设备,再做挂起或迁移操作。
从这些限制里可以提炼出一个共性认识:挂起操作要求系统能完整保存并恢复所有资源状态。PCIe 直通设备恰好是状态无法被外部程序完整保存的一类资源,所以挂起功能在它面前就会失效。理解了这个约束,后续做虚拟化架构设计时就会主动评估业务是不是非要物理直通,能不能用 SR-IOV、virtio-blk 或软件模拟来换取运维灵活性。
7. 几个能救命的小经验
讲了这么多层的挂起与阻塞,最后沉淀几条我常用的小经验。
排查任何系统异常前,先分清它到底是阻塞还是挂起。阻塞通常有明确的等待条件,理论上条件满足可以恢复;挂起则需要外部动作来恢复。这个区分决定了你是该继续等、该重启、还是该介入恢复。看到 CPU 占用为 0 别急着下结论,先看进程状态和内核栈。
线程池、数据库、硬盘、虚拟机这四类告警背后,都有一层系统自我保护逻辑。它们宁可暂停服务也不让状态继续恶化。所以处理时第一优先级往往是保存现场和备份,而不是强行恢复。数据库的 SUSPECT 要留出紧急模式检查的余地,硬盘的 C5 要第一时间做镜像,虚拟机的挂起限制要在设计架构时就考虑进去。
做技术方案时,把“会不会长期阻塞”“阻塞了谁能唤醒”“挂起后怎么恢复”这三个问题想清楚,很多服务不可用的坑都能在设计阶段避开。这些概念说起来是教科书里的两个词,真正落地时却散布在代码、存储、数据库、虚拟化各个层面,理解了它们在不同场景下的真实表现,排查问题时心里会更有底。
