ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

挂起和阻塞到底有什么区别?一文讲透原理与排查方法

2026/9/7 16:49:33 拓冰建站 浏览量
挂起和阻塞到底有什么区别?一文讲透原理与排查方法 做了这么多年后端每隔一段时间就会被问一次“挂起和阻塞到底有什么区别”。说实话上学时我也背过标准答案什么“挂起是主动的、阻塞是被动的”之类的但真正让我彻底搞明白这两个词的是工作后踩过的那些坑虚拟机挂起失败、磁盘挂起扇区报警、数据库文件莫名挂起、线程池任务卡死……每一个场景里“挂起”和“阻塞”都有着各自的含义共享同一个词根却在操作系统、硬件、应用层完全不同的语境下扮演着不同角色。这篇文章想写很久了。它不是一个纯理论科普而是把我这些年遇到过的、和“挂起/阻塞”相关的真实场景串起来从操作系统线程状态到线程池阻塞队列再到数据库死锁给出一套能落到实处的理解框架和排查方法。适合做后端开发、运维、虚拟化平台管理的朋友看也适合准备面试、想把这俩概念彻底理清楚的同学。在往下走之前先把核心结论放在前面挂起和阻塞最本质的区别不在“卡住”这个表象上而在“谁发起的”“是否占用CPU”“资源是否可用”这三个维度。后面所有场景的讨论都会绕回这三点。1. 先搞清楚挂起和阻塞到底是什么1.1 操作系统的视角线程状态机里的两个“卡住”任何一个操作系统课程都会讲进程/线程的状态迁移。教科书上的经典五态模型是新建、就绪、运行、阻塞、终止。稍微现代一点的实现里还会多一个“挂起”态或者把挂起从阻塞里单独拆出来。先看阻塞Blocked。当一个线程执行到某个需要等待外部条件的操作时比如读取磁盘数据、等待网络报文、获取一把锁它就不再占用CPU了。操作系统会把它的上下文保存下来放进这个等待对象对应的等待队列里等条件满足后再被唤醒重新进入就绪队列。这个过程就叫阻塞。注意一个关键点阻塞中的线程CPU确实没有在跑它但它“随时可以被叫醒”条件一满足就恢复执行重新参与调度。再看挂起Suspended。挂起的核心是“暂停执行且不自动恢复”。它可能是操作系统把整个进程组换出内存放到交换分区里也可能是虚拟机管理程序把客户机的执行状态整体冻结保存还可能是调试器把某个线程暂时停住。挂起和阻塞最大的不同在于阻塞是“我会被唤醒的只是时间不确定”挂起是“我不打算自己醒来除非有外部显式干预”。这里有个容易混淆的地方Linux内核里其实没有单独的“挂起态”线程状态只有TASK_RUNNING、TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE这些。但“挂起”这个词在工程语境里用得极其广泛虚拟机的Suspend、进程的SIGSTOP、线程池里的Park操作全都被叫做“挂起”。所以理解这个概念时不能只背状态机得结合具体场景看它到底指什么。1.2 挂起与阻塞的三点核心区别如果把各种场景抽象一下挂起和阻塞的区别可以收敛成三句话谁发起的阻塞通常是“被迫等待”线程自己也不知道要等多久挂起则更多是“主动暂停”要么是外部管理工具发起的要么是代码主动调用了暂停接口。是否占用CPU两者都不占用CPU这是它们共同的特征。但阻塞中的线程一旦条件满足随时能跑挂起中的线程状态被完整保存恢复时要重新经历调度甚至重新加载。资源的可用性阻塞期间线程持有的资源一般不会释放比如锁所以阻塞可能引发连锁效应挂起期间整个执行上下文都被冻结资源处于“定格”状态外部看到的就是这个任务消失了。用一个生活化的类比阻塞像你在餐厅排队等位人站在那里不动不干活但一旦叫到号就能立刻进去用餐挂起像你出去旅游前把家里电闸拉掉、水管关掉、门锁上房子状态被完整保存但你不知道什么时候回来也没人能替你决定什么时候开门。前者是“等”后者是“停”。1.3 一个类比在脑子里钉死这两个概念我常用另一个类比来给新同事讲这块可以把线程想象成一条流水线工位。阻塞是这个工位在等物料。机械臂伸出去等着零件送过来电机停转不耗CPU但人还在工位上控制系统还盯着物料一到立刻开始装配。而挂起是整个流水线按下了急停开关所有机械臂停在半空中现场的每一个位置信息都被记录到日志里之后想恢复得先手动确认状态、重新启动甚至可以恢复成完全一样的位置继续干也可以直接放弃这条工位的任务。这个类比能帮你快速判断一个场景到底是阻塞还是挂起如果它只是因为一个条件没满足而在原地等待大概率是阻塞如果它整个上下文被冻结、不再参与调度并且需要外部显式恢复那就是挂起。后面我在分析虚拟化、磁盘、数据库的具体案例时你会看到这个判断框架非常管用。2. 挂起的百变面孔从虚拟机到磁盘再到数据库2.1 虚拟机挂起PCI/PCIe直通设备为什么是拦路虎先说一个我在虚拟化平台踩过的坑。很多人应该都见过这样的提示“存在 PCI/PCIe 直通设备时部分虚拟机操作将不可用。您无法挂起、通过 vMotion 迁移。”我第一次看到这行字的时候专门去查了文档才明白为什么硬件直通的虚拟机不能随便挂起。原理其实不难理解。虚拟机挂起的本质是把客户机的完整运行状态CPU寄存器、内存内容、设备状态保存到宿主机磁盘上之后可以恢复。但如果你给虚拟机直通了一块物理网卡或者GPU那就意味着这块物理设备被这个虚拟机独占了它的内部状态可能包含大量无法被“序列化”的内容比如网卡内部的DMA描述符、固件状态、中断状态等。虚拟机管理程序可以保存CPU和内存但没法把一个真实物理设备的内部状态完整地冻结保存。所以直通设备的虚拟机会被限制挂起和迁移因为操作之后设备状态会不一致恢复时很可能直接蓝屏或者丢包。这个问题在执行层面没有太好的绕行方案只能做取舍要么放弃直通改用SR-IOV这类半虚拟化方案要么在需要挂起维护时先把直通设备从虚拟机里热卸载如果支持的话挂起完成后再热添加回来。但热卸载本身又是一次硬件状态重建未必总是成功。所以在规划虚拟化架构时如果业务本身需要维护窗口和迁移能力就要尽量避免给关键虚拟机配置PCI直通。换一个角度想这也是“挂起”和“阻塞”在架构层面的一次碰撞挂起一个虚拟机本质上要求所有设备状态都能被完整保存而直通设备的内部状态无法冻结挂起这个操作就被“阻塞”了。2.2 硬盘的“当前挂起扇区”警告别只看健康度再往下看一层。硬盘SMART信息里有一条叫“Current Pending Sector”当前挂起扇区中文直译就是“当前挂起扇区”我最早看到这个词也很困惑扇区怎么会“挂起”实际上它描述的是硬盘在读写过程中发现了一个扇区的数据无法确认是否完好于是把这个扇区标记为“待重映射”——相当于给这个扇区判了个缓刑。后续对这个扇区的读写硬盘会尝试重新读取或者重写通常是重写触发重映射如果成功这个挂起扇区会被移动到备用区计数归零如果失败它会升级为“坏扇区”计数。这个“挂起”状态就是硬盘固件层面的“阻塞等待确认”既不确认它是坏的也不当它完全好的。我见过很多同事只看SMART Overall Health整体健康状态是绿灯就忽略这些细节。实际经验是如果Current Pending Sector这个值从0变成个位数且没有继续增长可以再做一次全盘读写测试观察但如果这个值持续增长基本可以断定盘体开始出现物理退化属于“准备备份、安排更换”的信号。这里的关键是不要只盯着“健康状态”字段而是要把SMART的各个计数项拉出来做趋势观察尤其是Pending Sector和Reallocated Sector重映射扇区两个指标联合判断。从“挂起”的角度去理解这个扇区正处于“被OS和其他程序挂起”的中间态这个软件不能正常读硬件又不确认坏于是所有对它的读写请求都会卡住这就是扇区层面的“阻塞”。2.3 数据库文件被“挂起”一次MDF/LDF故障复盘“挂起”这个词在Windows SQL Server生态里也有一个专有含义数据库状态变成“Suspect”置疑/挂起。热搜里那条“数据库更换server2019后data文件下的.mdf文件覆盖后被挂起不认.ldf”说的就是这个场景。这类问题我帮人排查过几次过程很有代表性。大概是这样的旧服务器上把数据库文件复制出来新服务器装了SQL Server 2019直接把.mdf覆盖过去附加数据库时发现状态挂起日志文件不被识别。这里的核心矛盾在于SQL Server的数据库是数据和日志配套的LSN序列必须连贯。如果你只覆盖了.mdf而.ldf是旧的、不匹配的或者.mdf文件的页面上记录的日志序列号和.ldf里记录的对不上数据库就会拒绝进入Online状态停在挂起模式。遇到这种情况第一反应永远是先备份原文件然后才谈修复。常见做法有两种如果你确实不关心最近的事务一致性可以把数据库改为紧急模式执行DBCC CHECKDB做一致性检查修复再重置日志如果你需要保住最近的数据则需要找备份做日志恢复或者把日志文件删除后以“仅恢复”模式附加数据库SQL Server会重新生成日志。实际过程中容易翻车的是很多人直接执行了“ALTER DATABASE xxx SET EMERGENCY”后没有先做完整备份导致修复失败后连原始现场都丢了。记住任何数据库修复动作之前先做文件级备份。从概念上看这里的“挂起”其实是数据库引擎对数据一致性的一种保护机制既然日志对不上我就把你挂起来拒绝提供读写服务防止数据进一步损坏。这个“挂起”本质上是主动的、保护性的和操作系统里那个“暂停执行”的挂起含义一脉相承。3. 阻塞的主战场队列、线程池与中断3.1 线程池的阻塞队列怎么选线程池和阻塞队列是并发编程里最经典的一对搭档也是“阻塞”这个词出现频率最高的地方。线程池的工作机制可以简化成任务到来时如果线程池没满直接开线程执行线程满了任务进队列等待队列也满了触发拒绝策略。这里的“队列”几乎都是阻塞队列区别在底层实现。我整理过一个对比表帮助自己选型队列底层结构锁模型核心行为适用场景ArrayBlockingQueue数组循环队列单锁生产和消费共用一把锁有界公平/非公平可配需要严格控制队列长度的场景LinkedBlockingQueue单向链表两把锁take锁和put锁分离有界/无界可配默认吞吐较好适合大多数线程池SynchronousQueue无容量无存储直接交付不存任务生产消费直接交接需要立即执行、不做缓冲的场景LinkedTransferQueue链表 Transferer无锁CAS支持Transfer语义高并发且需要等待消费确认的场景选型时我个人的准则是如果任务是CPU密集型、执行时间短且量不大LinkedBlockingQueue默认实现就行如果任务是IO密集型的突发流量为了防止任务堆积导致内存膨胀一定要用有界队列也就是ArrayBlockingQueue或者给LinkedBlockingQueue设置容量上限如果业务希望任务必须被立即处理、不接受排队那就用SynchronousQueue配合CallerRunsPolicy这种拒绝策略反而能形成一种天然的背压机制。有个细节容易忽略newFixedThreadPool默认用的是无界LinkedBlockingQueue如果任务长期提交且没有执行完队列会无限增长最终把内存撑爆。搜索引擎一搜一大把的OOM案例很多都出在这里。所以生产环境我从来不用裸的newFixedThreadPool一定会通过ThreadPoolExecutor显式指定有界队列、线程工厂和拒绝策略。队列的“阻塞”机制本身没问题问题在于无界队列把阻塞的边界无限延后了一旦任务堆积系统在OOM之前根本不会触发任何保护。有界队列加合理的拒绝策略才是生产级的正确姿势。3.2 中断为什么不能阻塞“中断为什么不能阻塞”这个问题是嵌入式、内核和驱动开发里经常被问到的一个点也是很多人对“阻塞”理解最模糊的地方。中断处理程序的运行环境极其特殊。它是硬件事件触发的异步回调运行在中断上下文里不是普通进程上下文。中断上下文里没有进程的概念不能睡眠、不能调度、不能调用任何可能阻塞的函数比如获取信号量、等待I/O、调用kmallocGFP_KERNEL标志会休眠等等。原因很简单中断处理器本身不属于任何进程一旦阻塞内核无法找“另一个进程”来切换执行整个CPU就可能卡死在这个中断里而且中断优先级很高其他中断排不上系统直接挂掉。我在做驱动开发时对这点感触特别深。一个标准的做法是中断处理程序里只做“记录事件、标记标志位、唤醒内核线程”把所有耗时和可能阻塞的工作丢给底半部Bottom Half比如软中断、tasklet、工作队列。如果是真正需要等待I/O的操作就通过workqueue把一个内核线程调度起来执行中断函数本身必须保证“快速返回”。这个设计原则放在用户态编程里也有对应物事件循环里不能放同步阻塞调用否则整个事件循环卡住其他连接全部遭殃。高频交易系统的低延迟里一个业务逻辑不小心调了一次阻塞IO延迟就从微秒级跳到毫秒级一个量级的差距就此拉开。从“挂起/阻塞”的本质角度看中断不能阻塞的根本原因就是你一旦阻塞就丢失了“可被调度”的前提而中断上下文恰恰是那部分不可调度的代码。理解这个约束才算真正理解阻塞这个机制存在的边界——阻塞是有前提的它依赖于操作系统提供的“可被唤醒、可被调度”的进程骨架脱离这个骨架谈阻塞系统就没办法收场了。3.3 任务阻塞的排查思路线程池或者业务线程一旦出现“卡住不执行”线上怎么排查我自己的固定套路分四步。第一步先把所有线程的堆栈导出来。Java环境就是jstackLinux原生线程就是gdb attach或者看/proc/PID/task/*/stack。看堆栈里线程阻塞在哪个方法上是等待锁、等待IO、还是跑在不可中断的系统调用里。第二步看线程状态和CPU占用。Java里BLOCKED状态说明在等锁WAITING状态说明在调用wait/join/parkTIMED_WAITING可能是在sleep或者带超时的等待。如果再配一下CPU占用哪些线程在空转、哪些线程在干等基本心里有数。这里有个容易忽略的点WAITING状态的线程CPU占用是0但如果你看到某个线程CPU占用很高而状态是RUNNABLE那它不是在等待而是在忙循环或者GC。第三步重点排查“连锁阻塞”。一个线程阻塞不可怕可怕的是它持有锁导致后面所有等这把锁的线程全部阻塞。有一次线上故障最开始只有一个线程在等待一个外部接口响应因为那次上游服务挂了等到超时之前它手里的数据库连接池连接一直没释放于是连接池被打满所有请求全部阻塞在获取连接上。这种问题的根因是超时和连接释放机制设计不合理加再多的线程池容量都没用。第四步查阻塞等待的那个外部资源是不是本身有问题。数据库连接满了、下游HTTP接口没有设置超时、本地磁盘IO写不进去这些都是常见的“源头阻塞”。先把源头处理掉线程堆栈上的阻塞自然就解开了。有时候你盯着堆栈看半天线程全都卡在同一个地方其实不一定是这个线程的问题而是某个大家都依赖的外部资源卡住了。4. 阻塞的代价与释放从异步非阻塞到MySQL故障4.1 异步非阻塞为什么大家都在往这个方向走提到阻塞就绕不开对立面的“异步非阻塞”。最近这些年从Node.js到Netty从Redis的事件循环到Kafka的异步发送几乎所有的中间件和高性能框架都在强调非阻塞。背后的逻辑其实很简单阻塞的本质是线程被占用但没干活是一种资源浪费。一个典型的对比传统的同步阻塞IO模型下一个线程处理一个连接线程在等待网络数据时是阻塞的10000个连接就需要10000个线程而线程切换本身就是巨大的开销。非阻塞IO模型则是通过事件循环用一个或者少数几个线程处理成千上万的连接真正干活的时候才占用CPU等待期间线程可以继续处理别的事件。这个思路的极致就是Netty这样的框架把IO事件和业务逻辑彻底解耦用少量线程支撑海量连接。但我想说一个容易走偏的地方不要为了“非阻塞”而“非阻塞”。异步非阻塞在提升吞吐的同时带来了调试困难、代码复杂度增加、局部状态管理变难等问题。如果业务本身并发量不高或者IO等待时间很短同步阻塞模型反而更清晰、更可靠。我在实际项目里见过太多把代码改成异步回调、结果回调地狱层层嵌套出bug时堆栈根本无法定位的反面案例。架构选型要结合业务不能盲目追新。理解“阻塞”的本质之后你会发现阻塞本身不是问题阻塞导致的资源浪费才是问题。如果并发量不高线程资源本身就是富余的阻塞等待反而是一种最简单的编程模型。4.2 阻塞释放如何让卡住的任务恢复阻塞的释放是分布式系统里最能体现工程能力的一个环节。线程在阻塞等待一个条件时怎么把它“放出来”通常手段有几类超时机制所有阻塞等待都尽量设置超时时间而不是无限期等待。这是一个最简单、最保底的释放手段。网络请求设置connectTimeout和readTimeout分布式锁加过期时间数据库操作设置statement timeout都是这个思路。中断机制Java的Thread.interrupt()可以唤醒正在等待的线程使其抛出InterruptedException。配合cancel()/shutdownNow()可以主动终止任务。哨兵对象/信号量释放显式调用Condition.signal()、Semaphore.release()来触发条件满足让等待线程醒过来。线上排查时我有个习惯凡是用到阻塞等待的地方必须确认“最坏情况下这个线程能被谁唤醒”。如果答案是“没有只能等”那这里就是一个隐患。加一个超时是最低成本的保险哪怕超时后只是记录一条WARN日志也比无限阻塞强。有一次排查一个生产问题发现有一条消息消费线程卡了两天就是因为消费逻辑里调了一个没有设置超时的RPC接口上游服务挂掉之后这个线程一直睡在那里。加了一个3秒超时之后虽然偶尔会有失败重试但至少系统不会因为一个卡死的线程把整个消费链路拖垮。4.3 MySQL慢查询、死锁与语句阻塞的排查数据库是“阻塞”这个词冲撞最猛烈的一个场景。慢查询、死锁、语句阻塞三个问题常常纠缠在一起热搜里专门提到了“以MySQL为例”我展开说下自己的排查路径。先说慢查询。慢查询是“语句执行得慢”它不一定造成阻塞但会占着线程和资源。标准做法是打开慢查询日志set global slow_query_logONlong_query_time设成1秒甚至更短跑一段时间后看慢日志里出现频率高的SQL用EXPLAIN分析执行计划。常见毛病就那几类没走索引、排序/临时表过大、索引失效函数作用在索引列上、隐式类型转换、单表数据量过大且没有分页优化。优化方向一般是加联合索引、改写SQL、拆大查询。慢查询是“病根”它往往就是阻塞和死锁的诱因。再说语句阻塞。语句阻塞的本质是锁等待。在MySQL里InnoDB行锁、表锁、元数据锁都可能造成阻塞。排查利器是information_schema的三张表INNODB_TRX当前事务、INNODB_LOCKS当前锁、INNODB_LOCK_WAITS锁等待关系。通过它们可以拼出等待链路找到谁在持有锁。实际场景里最常见的阻塞源是一个长事务一直不提交它持有的行锁把其他事务全部挡在外面。这时候不是去KILL那个被阻塞的语句而是去定位那个长事务要么等它提交要么KILL掉它。判断依据就是INNODB_TRX.trx_started事务开始时到现在的时长就是它“占坑”的时间。最后说死锁。死锁是两个事务互相持有对方需要的资源形成环形等待。InnoDB会自动检测并回滚其中一个事务所以死锁一般不会无限阻塞但频繁死锁会拖垮业务。排查死锁用SHOW ENGINE INNODB STATUS里面会打印最近一次死锁的详细信息和涉及的事务SQL。处理死锁的核心不是“避免死锁”这种正确但没用的废话而是两个具体动作一是让事务尽量短减少持锁时间二是让多个事务访问多张表的顺序一致避免交叉。如果是并发更新同一行导致的死锁可以考虑用原子UPDATE先锁定目标行再插入关联数据。MySQL这三个问题的排查顺序我的经验是先看死锁和阻塞因为它们会导致业务大面积卡死慢查询反而可以事后通过日志和监控慢慢优化。先把数据库从“卡死”状态救回来再谈性能提升。有一次我们线上一个核心表出现了严重的行锁阻塞业务侧所有更新都堵住了我第一时间通过INNODB_TRX找到那个长时间未提交的事务KILL掉之后系统马上恢复。事后分析才发现那个事务是一个定时任务里没加commit导致的一次简单的代码问题却因为阻塞的连锁效应放大了整个故障面。5. 最后说点实操心得写了这么长全是这几年实际踩坑换来的体会。最后分享几个我一直在用的习惯。第一任何涉及阻塞等待的代码都要回答一个问题这个线程最坏情况下会被卡多久如果答案没有上限就一定要加超时。这是最低成本的保护也是我在代码审查里一定会问的问题。第二挂起和阻塞这两个概念在不同领域语境不同别拿操作系统的定义硬套所有场景。虚拟机挂起、磁盘挂起扇区、数据库挂起状态虽然都叫“挂起”背后的机制完全不同。理解了各自的上下文遇到报错才能快速定位。第三排查线上问题先看线程堆栈再看资源使用最后才动代码。堆栈会直接告诉你线程卡在哪里资源使用会告诉你它为什么会卡。这两个信息齐了90%的阻塞问题都能定位。第四遇到紧急情况先保住现场再动手。无论是数据库文件修复还是磁盘扇区处理备份永远是最重要的一步。宁可多花5分钟备份也不要在没备份的情况下赌一次修复。最后再分享一个小技巧排查挂起/阻塞类问题我习惯先判断它属于哪一类——是等待某个条件阻塞还是整个状态被冻结挂起——然后直接跳到这一类对应的排查套路上。这个判断框架帮我省了很多绕弯子的时间也推荐给你试试。