CPU不高服务却慢?用vmstat和pidstat -wt定位上下文切换瓶颈
发布时间:2026/10/21 15:35:37 锦皓数字建站

CPU分析做了很多轮之后我逐渐发现一个容易被忽略的盲区大家都在盯 CPU 利用率那条曲线却很少关心系统的上下文切换。很多服务看起来“CPU 不高、IO 不高、空闲一大把”可接口就是慢得像蜗牛。经验多了以后我才习惯性地先把 vmstat 的 cs 列和 pidstat -wt 的输出拉出来对上号之后进程/线程上下文切换造成的性能瓶颈基本就能现出原形。这篇文章就立足在一个典型排查场景上聊清楚 vmstat 和 pidstat -wt 怎么配合使用以及怎么把一个“隐形”的切换瓶颈完整定位出来。1. 上下文切换为什么能独立成为性能瓶颈1.1 切换成本寄存器、缓存和流水线的“三角债”先补一个基础概念。操作系统在 CPU 上调度的是线程不是“代码”这堆抽象的东西。当一个线程从运行态变成就绪或阻塞态另一个线程被调度上 CPU 时内核需要把前一个线程的寄存器、程序计数器、栈指针、内存映射状态保存下来再加载后一个线程的上下文。这个过程就叫上下文切换。单次切换的代价表面上看只是几十到几百纳秒的内核态处理但真正贵的不是这部分。线程换出后CPU 里的 L1/L2 缓存、TLB、分支预测器状态都很可能被新的线程“冲掉”。下次这个线程再被调度回来又要重新冷启动一套缓存。也就是说切换的成本大致可以拆成“内核调度开销 缓存失效成本 流水线重启成本”三块在业务是热数据、高频率轮转的场景里后面两块往往比第一块还要命。做个粗估假设环境里每秒发生 20 万次切换每次按 5~10 微秒的综合开销算光切换就会消耗掉 1~2 秒的 CPU 时间。如果你的机器一共 8 个核那意味着大约 12%~25% 的算力被调度器本身吃掉了真正给业务用的资源被明显压缩。这还没有统计因为频繁抢锁、缓存局部性被破坏带来的附加延迟。1.2 CPU看起来不高而服务却很慢的诡异组合纯 CPU 密集型的瓶颈判断起来很容易us 冲到很高直接把 CPU 打满top 一眼看到。但上下文切换造成的瓶颈是反直觉的因为它的特征是“CPU 利用率不高服务却响应很慢”。原因在于大量线程并不在真正跑业务而是在不停地排队、等待被调度、被换出、被唤醒、再被换出。CPU 看似有空闲但那只是相对“没有业务代码在消耗时间片”而言。线程切来切去的时间只会体现在 sy 或者干脆被调度器吞掉不会变成 us如果业务线程的总量又没到打满 run queue 的程度top 的 id 甚至还有剩余。这类问题如果只盯着 CPU 百分比看很容易误判成“系统资源充足”然后去调别的模块越调越偏。正确做法是先把视角拉高用 vmstat 观察系统整体的调度状态和切换频率再下沉到进程和线程级别去定位具体对象。2. 先用vmstat框定系统整体状态2.1 vmstat的采样姿势与关键字段vmstat 是最容易上手的系统级性能工具。排查切换类问题我习惯用这样的命令采样vmstat 1 10意思是每 1 秒采样一次连续采 10 次。建议不要只看单次输出因为单次快照反映不了频率和趋势必须看一段连续时间内的波动。如果是压测场景就把采样时间拉长到 20~30 秒至少要覆盖一个完整的压力周期。输出里和切换瓶颈强相关的主要是下面几个字段字段含义在看切换瓶颈时的作用r运行队列中的线程数包含正在运行和等待 CPU 的如果长期大于 CPU 核数说明调度已经在排队b不可中断睡眠的进程数看是否存在 IO 等其他阻塞因素干扰判断cs每秒上下文切换次数核心指标直接反映调度压力us用户态 CPU 占用比例如果很低说明业务代码实际没在跑sy系统态 CPU 占用比例切换、系统调用、中断都会拉高它id空闲比例不可只凭空闲判断“没问题”其中最有欺骗性的就是 id。很多新手觉得“id 还有 40%系统很闲”其实可能是大量线程在抢锁和唤醒CPU 都在空转和切换只是我们没有用一个合适的指标把它度量出来。2.2 典型切换瓶颈在vmstat里的画像看一组模拟数据假设备机有 16 核压测平稳阶段采到的输出大致如下procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 2 0 0 3841000 1120 3020000 0 0 1 8 12000 220000 15 21 62 2 0 3 0 0 3841000 1120 3020000 0 0 1 9 12550 238000 16 24 58 2 0 2 0 0 3841120 1120 3020000 0 0 1 8 12800 215000 14 22 61 2 0这组数据有几个判别点r只有 2~3说明运行队列并不长线程没有大面积排队等 CPUb是 0说明不是磁盘或 IO 阻塞cs达到 20 万以上已经明显偏高us只有 15% 左右sy却有 20% 以上id还有不少。如果us和sy都很低、id很高但cs超高那是更加典型的“调度空转”状态线程在频繁切换但没干什么实事。如果sy偏高、us却上不去则要怀疑切换里掺入了大量系统调用或锁竞争。这里给一个经验基线供参考单 CPU 每秒几千次切换属于正常四核环境每秒四五万次就需要高度警觉十六核以上环境每秒十几万次往往已经严重影响业务。但这都不是绝对标准最稳妥的办法是提前给自己的业务建立压测基线基线之内的cs不影响延迟基线之外的cs增长要格外留心。3. 用pidstat -wt把切换落到进程/线程3.1 参数与输出列怎么读vmstat 只能告诉我们“系统级的调度压力很高”但它回答不了“是谁在制造这些切换”。这一步就要用 pidstat命令是pidstat -wt 1 5-w表示输出上下文切换统计-t表示把当前进程的线程也展开默认会输出系统中所有进程及其线程的切换情况。如果想缩小范围可以配合-p指定进程号pidstat -wt -p 2100 1 5输出大概长这样关键列做了注释Linux 6.1.0 (lab-server) 04/02/2025 _x86_64_ (16 CPU) Average: UID PID TID cswch/s nvcswch/s Command Average: 1000 2100 - 1562.33 17.20 mockgateway Average: 1000 - 2100 126.54 1.44 |__mockgateway Average: 1000 - 2101 4365.10 24.99 |__mockgateway Average: 1000 - 2102 311.02 6.85 |__mockgateway Average: 1000 - 2103 390.71 8.32 |__mockgateway有几个点要解释清楚cswch/s是自愿上下文切换次数nvcswch/s是非自愿上下文切换次数。自愿切换是指线程主动让出 CPU典型原因包括等待锁、等待 IO、sleep、阻塞在某个系统调用上。非自愿切换是指线程被调度器强制换出常见原因是时间片耗尽、有更高优先级线程抢占、可运行线程数超过 CPU 核数、负载均衡时的跨核迁移。当看到某个 TID 的cswch/s明显高于同进程其他线程时基本可以说它在频繁进入“等待-唤醒”循环。这个现象多数情况下不是线程本身写得有问题而是它背后依赖的锁、队列或 IO 资源出现了争用。至于nvcswch/s高则要往 CPU 资源不足或运行队列过长的方向去查。3.2 从局部数字回到全局验证pidstat 输出的只是它能观测到的进程/线程视角的切换数。拿到单线程数据之后还需要做一步“对账”把 pidstat 里所有进程的 cswch/s 和 nvcswch/s 汇总再和 vmstat 的 cs 总量比一比。如果两者接近说明切换基本都发生在用户态进程线程之间如果 pidstat 汇总远低于 vmstat 的 cs 总量那说明还有一部分切换来自内核线程或处理器中断上下文需要进一步用pidstat -wt配合 root 权限去检查内核线程的切换情况或者用cat /proc/loadavg和cat /proc/stat辅助判断。这一步很关键。因为有些场景下业务侧线程很干净真正疯狂切换的是某个内核线程或驱动线程。如果不做对账容易把“罪名”错扣在业务进程头上优化半天没效果。4. 一次由线程切换拖垮网关的排查复盘4.1 复现场景以为该加并发结果加了灾难下面用我做过的一个压测案例来说明完整链路为表述方便把程序称为“模拟项目X网关”。它本身逻辑很简单收到请求后放入一个共享队列再由一个线程组取出处理涉及一小段加锁计算。第一轮压测发现吞吐上不去我的第一反应也是“并发不够”于是把线程组从 32 调到 128试图让服务吃满 CPU。结果非常反直觉并发越高吞吐反而越低平均响应时间从 20ms 拉到 80ms。还原代码后发现共享队列是所有线程同时操作的并且锁的临界区里还带了一些不必要的日志和统计操作。线程数一多大家全挤在锁上抢来抢去。这种配置在压测里特别容易复现把并发线程数调高然后把每个请求处理时间压到很短锁竞争的效应就会被无限放大形成“高切换、低吞吐”的经典现场。4.2 vmstat现场与CPU百分比之间的矛盾压测过程中我抓的 vmstat 输出如下16 核机器procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 4 0 0 3204500 150 4200000 0 0 2 16 28213 238000 13 36 48 2 0 5 0 0 3204100 150 4200000 0 0 2 15 28960 251000 14 38 45 2 0 3 0 0 3203800 150 4200000 0 0 1 14 27750 241000 13 36 48 2 0r只有 3~5甚至比 32 线程时还低CPU 利用率看起来也不高us才 13%~14%id有 45% 以上。但cs稳定在每秒 24 万次左右sy异常高。如果按照“CPU 不高就是没事”的习惯这一步就会漏掉问题。但把 cs 和 sy 放一起看结论已经很清晰CPU 的大量时间被系统态消耗掉而这些系统态消耗主要来自上下文切换和锁相关的调用。业务代码几乎没有机会跑起来所以 us 才那么低。4.3 pidstat -wt定位到“频繁让出CPU”的线程接下来用 pidstat 定位具体对象pidstat -wt -p 2100 1 5采样结果的一个片段Average: UID PID TID cswch/s nvcswch/s Command Average: 1000 2100 - 4590.12 30.11 mockgateway Average: 1000 - 2101 201.25 3.02 |__mockgateway Average: 1000 - 2102 4632.44 32.00 |__mockgateway Average: 1000 - 2103 198.40 4.12 |__mockgateway Average: 1000 - 2104 10.32 1.20 |__mockgatewayTID 2101 和 2102 的cswch/s都超过 4500而其他线程只有一二百或更低。这说明这两个线程基本一直在“让出 CPU - 被唤醒 - 再让出”的循环里。结合代码里共享队列的锁设计我当时的判断就是锁竞争导致的等待唤醒风暴一个线程释放锁后会有大量等待线程被唤醒但其中大多数线程醒来后发现锁又被其他人抢走了只能再次睡眠。一次释放能引发几十次无效唤醒和切换。为了进一步确认我抓了这两个线程的内核栈和等待点看到它们都阻塞在同一个互斥锁的等待队列上和 vmstat 的 sy 高位完全对得上。锁等待基本可以确认。4.4 调整线程模型后的数字变化定位到锁之后优化不是无脑扩并发而是反向收敛。我把线程组调回接近核数的水平同时把锁临界区里的日志和统计操作挪到锁外再把共享队列拆成多队列、减少全局锁竞争。优化后同样压测条件下vmstat 的 cs 从每秒 24 万降到 4 万以内sy 从 36% 降到 12%us 反而从 13% 升到了 55% 以上。这才是真正的“把 CPU 还给了业务”。接口 P95 延迟从 85ms 降到 14ms吞吐量也回到正常水位。最直接的体感是CPU 利用率变“高”了但服务反而变快了。这个案例给我留下很深的一个结论提升性能的手段未必是增加并发而是减少无意义的上下文切换。判断手段也很直接压测前后各跑一遍 vmstat 和 pidstat -wt对比阈值、延迟、吞吐调整方向对不对数字自己会说话。5. 从“切换高”到“瓶颈在哪”的因果排查5.1 先从自愿/非自愿切换比例猜瓶颈类型拿到 pidstat 的数据后我一般先按“自愿切换”和“非自愿切换”的比例做一个粗分类把目标范围缩小。现象可能根因下一步检查方向cswch/s 明显高nvcswch/s 不高锁竞争、IO 阻塞、sleep、频繁系统调用查线程等待点、锁监控、日志输出频率nvcswch/s 明显高cswch/s 不高CPU 资源紧张、时间片抢占、运行队列过长查 run queue、线程总数、优先级设置两者都高整体压力过大线程模型和资源同时出了问题做压测降载验证再分层定位单线程异常高其他线程正常热点锁或热点逻辑在某个线程上对该 TID 抓栈、看 wchan、查调用链这个分类不是绝对严谨因为真实场景常常混合出现但它能帮我们快速找到起手方向。手动排查问题时确立优先方向比立刻深入代码更重要。5.2 线程数量与锁竞争是两个高频根因我见过的大多数“上下文切换打垮性能”的案例根因最终都落在两类问题上。一类是线程模型不合理。比如每来一个请求就创建一个线程、线程池开得过大、或者线程池里塞了大量只 sleep 不干活的线程。线程一旦多到远超 CPU 核数调度器必须不停地在它们之间切换哪怕业务负载不高空转也能切换出几万次的 cs。看一个简单命令可以快速确认线程总规模ps -eLf | wc -l cat /proc/PID/status | grep Threads线程总数接近或超过核数几十倍时切换开销基本不可能低。这时候合理的方向是收敛线程池让活跃线程数和核数保持在合适的倍数范围内而不是继续堆线程来模拟“并发”。另一类是锁竞争。锁竞争不一定会表现为死锁它最常见的形态就是“唤醒风暴”持锁线程释放锁时内核把等待队列里的一堆线程全部唤醒但锁只有一个。最终线程们发现抢不到锁又一个个睡回去。这个过程每发生一次就会贡献多次上下文切换。锁临界区越大、等待线程越多这个效应越明显。处理这类问题通用思路是减小临界区、分离读写锁、用无锁队列或分片队列替代单一大锁、把耗时的 IO 操作移出锁外。具体选型要结合业务数据结构和访问频率没有银弹但“减少无效唤醒”是所有方案共同的优化目标。5.3 内核线程和中断在切换中的占比业务进程排查完后如果 vmstat 的 cs 总量和 pidstat 的汇总对不上就要考虑内核线程和中断处理的贡献。这类切换的特点是不属于某个用户进程但大量占用系统 CPU。常见的来源包括网络收包软中断、驱动队列处理线程、内存回收和后台刷盘线程、以及某些内核事件机制的高频唤醒。如果是网络收包导致软中断频繁抢占进程通常会看到 CPU 的si软中断列跟着上涨如果只是某内核线程在空转式唤醒则 sys 会高而 si 不高需要再用 pidstat 以 root 权限展开内核线程看。对内核线程的调优通常比较敏感不建议为了压低 cs 而随意调整内核参数。先确认是哪个线程在切换再判断它的行为是否合理。多数情况下我们能做的是在应用层规避减少高频定时器、合并小任务、防止业务进程和内核线程互相频繁唤醒。5.4 缓解手段与验证节奏缓解手段按侵入性从低到高排可以分成几档应用层优化收敛线程池、减少锁竞争、合并小粒度任务、批量处理、避免没必要的 sleep 和轮询。调度亲和相关用taskset或numactl把进程绑定到固定 CPU 或 NUMA 节点减少任务在核间迁移带来的额外切换。系统配置调整根据服务特点调整中断亲和性、进程线程上限等。这类调整需要先在测试环境验证不能直接在核心业务上拍脑袋改。这里特别想提醒一个心态降低 cs 不是目的降低延迟、提高吞吐才是。如果某个cs数值高但不影响业务没必要为了数字好看而去做激进调整。每一次改完都要用 vmstat 和 pidstat 同时验证“切换降了”和“业务指标变好”两者必须同步成立才算优化有效。6. 这套分析方法的使用体会6.1 我固定的检查顺序时间久了我遇到疑似上下文切换瓶颈的问题都会按固定顺序来过一遍vmstat 1 5先看全局确认 r、b、cs、us、sy、id 的关系确定是否还需要继续往下查。如果 cs 异常立刻记录当前压测流量、延迟、吞吐作为后续对比基线。pidstat -wt 1 5观察所有进程线程的 cswch/s 和 nvcswch/s把异常切换对象锁定到具体 TID。按自愿/非自愿切换的比例判断方向自愿高查等待和锁非自愿高查 CPU 和调度。结合代码和等待点确认根因应用层修复优先再考虑系统层调整。改完之后重复步骤 1~3把切换数据、延迟、吞吐和基线放在一起看确认收益。这套流程的好处是每一步都有数字支撑不会出现“感觉是锁的问题”这种结论失准的情况。压测数据、vmstat 输出、pidstat 输出、优化后的对比结果就是最可信的排查证据链。6.2 别被平均值和绝对数值误导最后讲两个容易踩的认知坑。第一个坑是只看平均值。vmstat 和 pidstat 输出里的 Average 只是平均状态尖峰流量下可能每秒切换 40 万次平均下来只有 8 万。如果只看平均值很容易觉得“好像还行”从而漏掉真正的瓶颈窗口。我的习惯是同时看原始样本把最大值的出现时间和压测曲线对齐。地址对齐之后就能看出高切换到底发生在流量最高点还是锁持有异常时段。第二个坑是拿绝对数值当唯一标准。“cs 超过多少就是有问题”的说法本身就不靠谱。CPU 核数、业务形态、线程规模都会影响合理值。同样是每秒 10 万切换对 32 核的短连接网关可能完全没影响对 4 核的延迟敏感服务可能就是灾难。正确的方式是和自己业务的压测基线比较基线内 cs 是多少异常时涨到多少延迟变化趋势是否跟 cs 一致。只要这条相关性是成立的即便绝对数字不高也值得挖下去。上下文切换是一个藏在 CPU 百分比之下的影子指标单看 CPU 很难抓到它。熟练使用 vmstat 做全局判断、用 pidstat -wt 做局部定位之后大部分“CPU 不高但服务很慢”的怪问题都能变成一条有据可查的排查链路。下回再遇到类似现象不妨先敲一条 vmstat 1 5看看 cs 列是不是在悄悄替瓶颈“说话”。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。