监控大盘上 CPU 使用率停在 31%,业务方已经在群里连着问了三天为什么接口慢。运维第一反应是"机器闲着呢,不是我们的问题",第二反应是申请加配,把 8 核加到 16 核。结果几乎是原样:曲线还是三成上下,耗时柱状图一动没动。这个场景在服务器上演了不知道多少遍,它的迷惑之处在于——那 31% 从来就不是一个物理量,它是好几种性质完全不同的东西被加在一起、再被平均若干次之后剩下的一个残影。
先说它经历了哪两次平均。第一次是时间性质的平均:操作系统把一颗核上的时间切成像 usr、sys、iowait、irq、softirq、steal、idle 这样的几段来记账,而大盘上的"使用率"通常只呈现它们加总(或按某种口径扣除部分等待)之后的结果,"这一个指标"由此诞生。第二次是核数维度的平均:16 颗核各自的使用率被算术平均成一个数字,任何单颗核的打满都会被另外 15 颗稀释。第三次往往还有采样窗口的平均:秒级的一次冲高,摊进一分钟的窗口里就只剩下几个百分点。
所以本文要说的核心就一句话:CPU 不是一个数字,而是几段性质完全不同的时间。真正能把"使用率不高但业务慢"解释清楚的,恰恰是最容易被这个汇总值抹平的三段——sys、iowait 和 steal。它们分别代表"时间花在内核里了""时间其实没在干活只是在等盘""时间被别人拿走了",这三种情况在"使用率"这个数字上可能长得一模一样,甚至都很低,但对应的处置方向南辕北辙。
更麻烦的是还有一种开销压根不会被任何一段统计到:切换带来的缓存与 TLB 代价、以及请求在队列里排着却不消耗任何 CPU 时间的那部分等待。它们是"看着不忙,单次请求却明显变长"的直接来源,而你在百分比里看不到它们的名字。这就是为什么"使用率只有三成"完全不能作为"CPU 不是瓶颈"的证据,甚至连弱证据都算不上。
把一秒的时间想象成一把尺子,操作系统按用途把它切成几段分别计数。不同系统的命名略有出入,但主流口径基本是下面这几段,先把每一段到底在说什么讲清楚,后面的所有判断才立得住。
user,用户态时间。CPU 在执行应用程序自己的代码:业务逻辑、加解密、压缩、序列化、正则回溯、图像处理、各种循环。这一段通常被认为是"有效工作",因为它直接对应你在代码里写的东西。需要说明的是,用户态时间高并不一定代表计算量大,也可能是算法写得糟糕,比如一段本可以提前退出的遍历,或者本该缓存的结果被反复重算。
sys,系统态时间。CPU 在执行内核代码。系统调用进来之后干的活都算在这里:读写文件的路径处理、权限校验、页表与内存管理、进程线程的创建销毁与调度、网络协议栈的收发处理、文件系统的元数据与日志提交。它同样是在干活、CPU 同样是满的,但它服务的是"把这事儿办了"这部分的开销,而不是在完成你的业务逻辑本身。sys 和 user 的分野非常重要:两者都让 CPU 变忙,但优化手段几乎不重叠。
iowait,等待块设备 IO 的时间。这一段的定义要咬住:CPU 此刻是空闲的,但系统里存在尚未完成的块设备 IO 请求。也就是说,它记录的是一个"闲着但闲得不踏实"的状态——核是空的,可上面挂着活没收尾。这一段最容易被当成 CPU 负载,而它的瓶颈一次都不在 CPU 上,后面单独用一个小节讲。
irq,硬中断时间。设备敲 CPU 的门:网卡收到了帧、磁盘控制器完成了传输、定时器到点了。CPU 停下手里的活去响应,这段响应时间被记到 irq。它的特点是短促、优先级高、不可被普通任务抢占,来得密集时会把正常的执行流切得很碎。
softirq,软中断时间。中断上半部要快,就把耗时的部分推到下半部去做,这部分延后执行的处理就是 softirq。收包之后的协议栈处理、定时器回调等都落在这里。本文只把它当成"CPU 时间的一段"来说明,不展开任何中断层面的均衡与绑定话题,那属于另一套主题。
steal,被偷走的时间。这是虚拟化环境独有的计数:本该分配给这台虚拟机的 CPU 时间,被宿主机调度去给了别的负载。注意它和 iowait 的差别极大——iowait 期间这颗物理核真的是空的,而 steal 期间这颗物理核是实打实在干活的,只不过做的事跟你没关系。这一段的存在感通常很低,可一旦抬头,它是解释"毫无理由地变慢"最有力的证据。
nice 与 idle。前者是调整过优先级的用户态时间,性质上仍属于应用代码的执行;后者是真正的空闲,这颗核上没有任何任务想跑。idle 和 iowait 的区别只有一句话:idle 是"没人排队",iowait 是"有人排队,但排的不是 CPU 而是存储"。
把这七段摆在一起看,会发现一个很实在的结论:同样是"CPU 在忙",user 忙、sys 忙、irq/softirq 忙、以及别人的活让你的核忙(steal 感知),其成因和处置完全不是一条路。总使用率把这几段的差异全抹平了,而最常解释"不高却慢"的三段——sys、iowait、steal,恰好都藏在这层抹平底下。
| 时间类型 | 说明 | 偏高时的常见成因 | 最容易被误判成什么 | 该往哪个方向查 |
|---|---|---|---|---|
| user(用户态) | CPU 在执行应用自身的代码,不含内核部分 | 加解密、压缩解压缩、序列化、正则回溯、图像处理、批量遍历等计算动作 | 被直接理解为"机器核数不够",于是先加核再说 | 落到进程与线程看 CPU 归属,对热点函数做采样剖析,先确认工作量本身能不能减少、能不能并行 |
| sys(系统态) | CPU 在执行内核代码:系统调用、内存管理、进程调度、文件系统与网络协议栈 | 海量小文件读写、逐行刷日志不缓冲、频繁收发小网络包、频繁创建销毁进程或线程、反复内存映射与缺页、元信息查询过多 | 被当成 user 侧的业务性能问题,或者被当成采样误差忽略掉 | 统计系统调用的频次与分布,按线程看内核态占比;方向是减少调用次数与串行环节,而不是加核 |
| iowait(等待 IO) | 这颗核处于空闲状态,但系统里存在未完成的块设备 IO 请求 | 存储路径慢:盘本身、控制器、设备队列、文件系统自身的提交与元数据操作 | 被当成 CPU 不够用,于是扩容 CPU 或加大进程数,反而把队列压得更长 | 转向存储一侧:队列深度、请求等待时间、文件系统与刷盘行为;这条路加 CPU 没有任何收益 |
| irq/softirq(中断与下半部) | 硬中断处理与其延后执行部分所占的时间,本文仅作为 CPU 时间的一段作概述 | 设备中断到达的量变大且集中 | 被当成普通业务运算消耗,归到"应用太重"头上 | 结合中断速率与每核分布一起看,确认主要落在哪些核上;中断层面的调整不在本文讨论范围 |
| steal(被宿主机占用) | 虚拟化环境中本该给本机的 CPU 时间,被宿主机调度给了别的负载 | 宿主机侧超售、邻居负载抢占物理核 | 被当成自己程序的性能问题,反复做无效的代码层优化 | 记录发生时段、持续时长与核分布,向服务商核对宿主机状态;租户侧优化无法消除这一段 |
| idle(空闲) | 真正的空闲,没有任何任务需要在这颗核上运行 | 本身不是问题;持续偏高只说明 CPU 有余量 | 被反过来当成"CPU 一定没问题"的证据,从而放弃继续排查 | 结合运行队列、上下文切换与延迟分位数判断:核是空的但请求仍慢,问题多半不在 CPU 时间本身 |
iowait 是这几个数里面被误解最深的一个。回到它的定义:CPU 空闲,且存在未完成的块设备 IO 请求。这里的关键字是"CPU 空闲"——从物理上看,这一刻这颗核什么活都没干,它跟 idle 在"核是否空转"这件事上没有区别,唯一的区别是记账时给它贴了张"有人在等盘"的标签。既然核本身是空的,你给它更多的核,指望谁来干活?
这就是为什么"看到 wa 高就去扩容 CPU"是最典型的资源错配。真实的瓶颈在存储路径上:可能是盘本身处理不过来,可能是控制器或设备队列已经排满,也可能是文件系统在做自己的事情——日志提交、元数据更新、回收与同步操作。这些环节里没有任何一个会因为你的 CPU 核数从 8 变成 16 而加速,反而加核之后能并发发起 IO 的线程变多,队列挤得更厉害,等待时间继续拉长。见过不少情况是加完核之后整体耗时反而涨了一截,就是因为堵点被喂了更多请求。
iowait 还有两个容易踩的反直觉点。第一个是它也会被平均稀释:一颗核上有线程卡在不可中断的等待里,在 32 核机器上按全部核摊下来可能只有两三个百分点,看着风平浪静,但那个被卡住的请求链已经彻底停摆了。第二个是iowait 反映的是"有任务在等",不等于"盘有问题":一次同步刷盘、一次文件系统日志提交、一次元数据遍历,都可能让一批线程同时挂住,而这些其实是正常行为,只是恰好撞在关键路径上。所以看见 wa 抬头,正确的动作不是问"要不要加 CPU",而是问"等待发生在哪个设备上、队列有多深、单次请求的服务时间是多少、是谁在发这些 IO"。
至于"wa 高的时候降低业务并发能不能缓解",答案是通常能,但这属于把请求的入口收窄来减少对存储的压力,跟 CPU 依然没关系。判据很简单:只要是 IO 等待占主导的场景,你去动 CPU 相关的任何配置都是在绕远路。
sys 段抬头的信息量比很多人以为的大得多。它说的是:这段时间 CPU 是实打实在跑的,但跑的是内核的代码,不是你的业务逻辑。用户态和内核态的分界线很清楚——凡是应用自己的指令都算 usr,凡是陷入内核之后干的活都算 system。所以 sys 高的第一层结论永远是"这批时间的性质是大内核开销,不是计算量太大"。
什么东西会把时间成批地推进内核态?最常见的几类行为很具体:一是大量小文件的读写,尤其是那种逐个打开文件、读几十字节、再关闭的循环,路径解析、权限校验、页缓存查找的开销被放大了几十倍;二是不缓冲的日志写入,每打一行就发起一次写系统调用,日志越密内核态越满;三是频繁收发很小的网络包,每次都完整过一遍协议栈与系统调用边界,包小而量大时开销几乎全落在 sys 上;四是频繁创建销毁进程或线程,比如在请求处理路径上 fork 外部命令、或者每次任务都新建线程而不复用;五是反复的内存映射与缺页,新申请的内存第一次触碰时都会产生缺页处理,加上临时的大块分配与释放交替,会把页表与回收环节的重量直接反映到 sys 上。
这类场景的共同点是:瓶颈是调用的次数,而不是每次调用的强度。所以加 CPU 核数未必有效。核数只在两件事成立时才有意义——这些内核态工作本身可以并行,而且里面没有必须串行的环节。可惜现实里系统调用密集的负载往往同时踩着两样东西:要么是同一把锁,要么是同一个文件系统的一致性提交点。这时候加核等于在更宽的马路上排同一条队,队列长度没变,单次等待时间也没变,只是同时排队的人变多了。真正有效的是把调用次数降下来:批量读写替代逐次读写,缓冲写替代逐行写,连接与线程复用替代反复建立,合并小的网络读写,减少请求路径上的元信息查询,把可以推迟的初始化挪出关键路径。
我个人的习惯是不看 sys 的绝对值,而看它在"已用时间"里占的比例。如果一段观察窗里 system 长期占到已用时间的三分之一往上,基本可以断定这次的性能问题不在业务算法,而在"应用和内核之间的交互方式"。这时候再去翻业务代码的热点排序往往是白费力气——热点函数排得再靠前,它调用的那些系统调用才是真正吃掉时间的地方。要注意这是个经验性的观察比例,不是普适阈值,不同负载形态差别很大,用之前先在自己环境里跑一段基线。
steal 是本文唯一一个"在物理机上恒为零"的计数。它只在虚拟化客户机里出现,含义非常直白:这台虚拟机认为自己有活要跑、也已经排到队列里了,但宿主机把 CPU 资源调给了别的负载,本机拿不到时间。换句话说,你的程序被迫停在那里,而停摆的原因不在你的代码、不在你的配置、也不在你的资源规划能力之内。
把它和另外两段并列看会更清楚。iowait 时物理核是空的;idle 时物理核也是空的;而 steal 时物理核是满的,正在被别人使用。这个差别决定了处置方式完全不同:前两者你可以从自己的机器里找原因,steal 不行。你把自己的代码优化到极致、把进程数调到最合理、把中断亲和折腾一遍(更别提那是另一个话题),都不会让这段数值下降哪怕一个点,因为决定权在宿主机的调度器手里。
所以持续偏高的 steal 说明的事情只有两个可能性:宿主机本身的资源规划偏紧,或者这台物理机上某个邻居负载太吵。这两种情况的共同点是——都属于需要向提供机器的一方去确认的信号,不是自身调优能解决的。这里要说得再直白一点:steal 长期不为零并且和你的延迟劣化在时间上重合时,你在自己这台机器里做什么都改变不了结果,正确动作是留证据、提问题、要求核对。
怎么区分"真问题"和"正常抖动"?看形状而不是看单点。瞬时抖一个值不用紧张,虚拟化环境下调度延迟的毛刺是常态;但如果它在某个时间段里连续存在、跨越多颗核呈现一致性、并且这段时间恰好对应着业务延迟的抬升,那就不该再自我怀疑了。需要攒下来的数据包括:发生在什么时段、持续多久、分布在哪些核上、同期延迟分位数变化了多少。把这几样对齐了再提,沟通效率完全不一样。
碰到 steal 这种租户侧改不了的信号,我一般会先把数据攒齐再找深耕 IDC 19 年(成立于 2007 年)的一万网络这类服务商核对——是宿主机负载还是邻居噪声,得让他们查;裸金属没有这层调度,也就不存在这个数;硬件故障 10 分钟自动迁移这条对硬件层问题还算实在;具体机型与是否独享以官网实时信息为准。
就算你把所有分项都看全了,还有一个陷阱等着:这些分项绝大多数也是被平均过的。常见的采集间隔在秒级到分钟级,而在 60 秒的窗口里,一颗核被吃满 6 秒,反映到这分钟的平均值上只有 10%。对批处理类业务这可能是真实体感,对一次请求要在几十毫秒内返回的在线业务来说,那 6 秒里发生的事情就是全部——打满期间进来的请求全部排队,它们的耗时被永久记录,但 CPU 曲线上几乎看不出波澜。
这就是平均值最大的问题:它回答的是"这段时间平均而言忙不忙",而你真正要问的是"这段时间里有没有片刻被打满"。两个问题完全不在一个尺度上。延迟敏感的业务尤其吃亏,因为用户感知的是单次请求的耗时,不良的表现会集中在长尾里:一千次请求里最慢的那十次决定了投诉量,可这十次在平均值上连涟漪都算不上。
所以结论是明确的一刀:别用平均值判断在线业务的 CPU 健康度,用分位数。延迟要看 P95、P99,甚至更高的分位点;CPU 侧则要往下钻采样粒度,用秒级乃至更细的窗口去看分项构成,并且把这个细粒度的图线和延迟分位数放在同一条时间轴上。两条线是否同步起伏,比任何单点数值都有说服力。日均 CPU 三成这种说法,对判断一个接口为什么在某两分钟里慢了两倍,几乎没有任何参考价值。
还有一层容易被忽略:平均不仅发生在时间维度,也发生在对象维度。整体平均值掩盖了具体进程、具体线程、具体核的差异,就像把城市平均气温当成某个房间里的体感。后面两节分别讲的就是这两个维度的失真。
这是本文最关键的一节,也是最难从常规监控里看出的一层。先把一个式子摆出来:一次请求的端到端耗时,大致等于在 CPU 上真正执行的时间,加上排队等待 CPU 的时间,加上等待锁的时间,加上等待块设备的时间,再加上等待网络与下游依赖的时间。"CPU 使用率"这个指标,严格来说只能覆盖第一项,剩下的四项它一概看不见,而恰恰是这四项决定了绝大多数在线系统的长尾。
再说上下文切换。什么情况下会切:时间片用完被抢占、主动放弃 CPU 去等锁或等 IO、优先级更高的任务打断了当前任务、等待缺页或等待某个事件发生。系统通常会区分自愿与非自愿两类计数——自愿切换是任务主动让出,多半意味着它在等某个东西;非自愿切换是被强行剥夺,多半意味着运行队列里排着别人,CPU 不够分。这两个计数的绝对值本身没多大意义,有意义的是它们的变化率以及二者的对比关系:非自愿切换的速率突然抬升,通常就是"请求开始被抢占、开始排队"的直接证据,而这时候使用率可能还稳在很低的位置。
切换本身的成本构成要拆开看。一部分是明面上的:保存和恢复寄存器、走一遍调度器的选择逻辑、更新统计。这部分会计入内核态时间,你在 sys 里能看到,但没法分辨其中多少是"调度开销"、多少是"业务确实需要的内核工作"。另一部分是暗处的,也是真正贵的:切换之后 cache 的局部性被破坏,数据要重新加载;TLB 里的地址映射失效或不命中,地址翻译要重新走一遍页表;流水线与分支预测器的历史作废,重新积累需要时间。这些周期 CPU 是实实在在消耗掉的,它们会被算作"忙",却不产出任何有效工作量,也不会在任何一层里被单独列出来。
于是那句"CPU 看着不忙,单次请求却明显变长"就说得通了:使用率告诉你的是"有多少比例的时钟被占用",它不告诉你这些时钟里有多少是浪费掉的,更不告诉你在没占用时钟的那些时刻,请求是不是正卡在某条队列里等着。一次包含几百次网络往返、几十次锁竞争、若干次跨核迁移的请求,CPU 占用可能只有几十毫秒,端到端时间却可能到几百毫秒——差额全在它看不见的地方。这也是我从来不用"使用率不高"来排除 CPU 问题的原因。
顺带说一句,请求在系统里被搬到不同核上继续执行,也会付出同样的缓存与地址翻译代价。就算总核数很多、整体使用率很低,只要任务在核之间来回跑得不巧,单次耗时照样会被抬起来。这种损耗同样不会以独立的字段出现在任何统计里。
平均负载和使用率经常被当成同一个东西的两个说法,这是个误会,而且是个代价很大的误会。它们统计的根本不是一类对象:使用率统计的是时间占比——一段时间内有多少比例的时钟被非空闲任务占用;平均负载统计的是任务数量——处于可运行状态的、加上处于不可中断状态的任务,共同构成这个数字(并且它是随时间指数衰减加权的一、五、十五分钟平均)。一个的单位是百分比,另一个的单位是"个数",两者之间不存在换算关系。顺便提醒一句,负载这个数一定要和核数一起读,同一个数值在 8 核和 64 核机器上代表的是完全不同的拥堵程度。
正因为统计对象不同,两者的背离本身就是极强的线索,四种组合各自指向不同的地方。
第一种,负载高而使用率低。CPU 不忙但排队很长,说明排的不是 CPU——大多数情况是一批任务正卡在不可中断的等待里,典型来源是块设备 IO,也可能是大量线程被同一个外部条件挂住。这种情况盯着 CPU 看永远找不到答案。
第二种,负载低而使用率高。CPU 很忙但排队的任务不多,常见于少量线程在满负荷烧 CPU 的情形:单核被打满、或者并行度天生受限的环节。加核往往没用,因为你加了它也上不去。
第三种,两者都高。这才是 CPU 真不够用的样子,或者 sys 段虚高把核占满了。这时候扩容或减并发都是有效动作,方向是对的。
第四种,两者都低但请求仍然慢。这一条最容易被忽略,因为它会让排查者陷入自我怀疑。可它的含义其实最清晰:CPU 既没有被占满也没有排队,那么时间花在别处了——锁竞争、下游依赖、网络往返、以及前述那些不计入任何百分数的等待。到这一步就该果断离开 CPU 这条线,再在 top 里翻来覆去是在浪费时间。
把这四个格子记住,比背下任何单个指标的阈值都实用。
前面提到过第二次平均:所有核的使用率被算术平均成一个数字。举个最极端的例子,一台 8 核机器里有 1 颗核吃满到 100%,其余 7 颗闲着,整体使用率是 12.5%。这个数字放在任何大盘上都是"很健康",但那颗被打满的核上排着的请求,正在以最慢的速度被处理。整体平均把热点核完全藏起来了。
哪些东西容易形成单核热点?一切在一个线程或一个连接里串行处理的环节:单条连接上的协议解析、单次握手的非对称运算、某个不肯多线程化的重任务、事件循环里那段被阻塞的处理、单线程的网关或代理。这类环节的共性是——它的并行度上限是 1,你给机器加到 64 核,这条路径上的吞吐也不会变。
所以看 CPU 一定要看逐核视图,而且要看逐核的分项,而不只是逐核的总量。有几类失衡特别值得标记:所有流量压在一两颗核上、某几颗核的 softirq 或中断比例异常突出、以及随时间推移热点在不同核之间漂移。第三种尤其隐蔽,因为它在任何单点采样里都显得"分布还行",只有连续观察才能发现任务在核之间搬来搬去。
还有一种"伪均衡"要警惕:调度器本身会做负载均衡,把任务往空闲核上挪,于是你会看到热点被摊开、每颗核的百分比都变得不难看。但任务本身依然是串行的,排队没有消失,只是分散到了更长的链条上,端到端延迟可能原样没动,甚至因为跨核迁移付出了额外的缓存代价而变得更差。分布变平不等于问题变好,唯一可信的判据还是延迟分位数有没有跟着下来。
知道了每一段在说什么,还需要一个能落地的顺序。我自己的做法是下面这条链路,每一步的目的都是把下一步的范围收窄,而不是急着改配置。
第一步,先把"慢"钉死。是哪个接口、哪个调用方、看哪个分位数、发生在哪个时间窗。没有这一步,后面所有的指标都找不到对齐的锚点。"有点慢"不是一个可排查的问题,"下单接口 P99 从某个时间点起翻了一倍,持续了七分钟"才是。
第二步,拉同一时间轴上的全部指标。把那个时间窗内的 CPU 分项构成、逐核分布、上下文切换速率、中断速率、运行队列长度、平均负载、以及业务延迟分位数,全部摆在一起。单看任何一个指标都会误判,它们的价值来自于彼此印证。这一步同时也是在检查采集粒度够不够——如果你的最低分辨率就是五分钟平均,那这一次多半查不出东西,先把监控粒度调细再说。
第三步,看分项构成走岔路。只有 user 高,就顺着进程与线程往下找热点函数;sys 高,转向系统调用频次与它们的调用点;iowait 高,转向设备队列与等待时间;steal 高,转向宿主侧确认;irq 或 softirq 高,结合中断速率与落在哪些核上一起看(具体的中断层调整不在本文范围)。这一步的分岔决定了剩下的全部工作量,不要在没分项之前就凭直觉挑方向。
第四步,看逐核分布。确认是热点核还是全局性的忙。热点核意味着并行度问题,需要改变的是任务的分派方式;全局性的忙才是总量不足的信号。
第五步,看运行队列与切换速率。队列有没有持续积压,非自愿切换的速率有没有抬升。这两项直接回答"是不是有人在排队等 CPU"。它们经常在使用率还很温和的时候就先动起来,是最灵敏的前兆。
第六步,把 CPU 指标与延迟分位数对齐。这一步是验证,不是铺垫:只有两者在时间上同步起伏,才能说这次的延迟问题和 CPU 相关。如果 CPU 侧一片平静而 P99 已经冲上去了,那就别再盯着 CPU 了。
第七步,落到具体的进程与线程,确认是哪几个执行体贡献了这段时间,把结论收口到具体的代码位置或具体的一次系统调用上。最后回到第二节那个式子:这个请求的耗时,到底是被"跑"吃掉的多,还是被"等"吃掉的多。这个判断一旦做出来,处置方向通常也就只剩一条路了。
前面讲了一堆"使用率不高不代表 CPU 没问题",但如果不给出反向条件,这套说法就会变成永远无法证伪的挡箭牌——任何问题都能说"说不定是某段没看到的开销",那就没有讨论价值了。所以这里把话说死:同时满足下面几条,就可以判定 CPU 不是本次问题的瓶颈,请立刻转去查锁、依赖服务与网络。
条件一,各核分布均匀。没有单颗核或少数几颗核被打满的情形,热点不存在于任何一条串行路径上。判据要看逐核使用率,而不是整体均值。
条件二,sys 与 steal 都低。内核态没有被系统调用或调度开销填满,也没有被宿主机拿走时间。这两项只要有一项长期抬头,CPU 这条线就不能放下。
条件三,运行队列没有持续积压。等待被调度的任务数量长期保持在低位,非自愿切换速率没有异常抬升。队列是空的,说明没有人排 CPU。
条件四,延迟长尾与 CPU 指标不相关。把业务的 P95/P99 曲线和 CPU 侧的所有曲线叠在同一时间轴上,看它们是否同步起伏。不同步就是不相关,哪怕使用率曲线本身再难看,也不是这次的成因。
条件五(辅助),吞吐往上走时,延迟劣化了但 CPU 侧没有同步变化。把请求量抬一截做对比观察:如果逐核使用率、队列长度、切换速率都没有明显抬升,而 P99 先崩了,说明增量被消耗在了别的地方。这条要在业务的低谷时段做,别挑已经满载的时段,否则分不清是过载还是本来就慢。
条件六(前提),观察窗口要覆盖一个完整的业务周期。只看十分钟就宣布"排除",很容易被偶然性骗过去。业务有自己的波峰波谷,某些排队现象只在特定的负载形态下出现。把整段窗口里的最坏情况单独拎出来跟常态对比,才有资格说"排除"这两个字。
这几条是同时成立才有意义的。只用其中一条就急着下结论,跟只看使用率就下结论是同一种懒。把上面的清单逐条划掉,四条全都过了一遍,CPU 这条线就可以盖章收工。我见过不少人在这种情况下还围着核数和调度参数纠缠好几天,最后发现问题出在一个下游接口的超时重传上——那几天是白花的。
还有一条反面提醒:不要用"先加两颗核试试"来充当诊断手段。它一次性改变了并行度、队列格局和任务在核上的分布三个变量,无论结果变好还是变差都无法解释,更谈不上可复现。要做变量验证,调的是并发度或者请求模型,而不是直接扩容。
最后补一句容易被追问的地方:既然上下文切换和那些"看不见的等待"不出现在任何百分比里,那么"排除 CPU"是不是永远排不掉?并不是。正因为这些损耗不进百分比,它们一定会留下别的观测线索:有人在排队会反映到运行队列长度和非自愿切换速率上,锁等待会反映到进程的可运行状态与调用栈分布上,存储等待会反映到 iowait 与设备队列上。看不见不等于不可观测,它只是不以"使用率"的形式被观测。把这几个替代观测口都堵死了,才算真的排除。
第一个坑,把 iowait 当成 CPU 问题。它是空闲的一种形态,加 CPU 对它没有任何作用,真正的堵点在盘、控制器、队列和文件系统那一侧。判断错了不但解决不了问题,还会因为并发能力提升而把队列喂得更堵,延迟反而更差。
第二个坑,拿日均值或者分钟级均值去判断在线延迟。短时打满会被平均抹平,用户真正抱怨的长尾在平均值上留不下痕迹。延迟要看分位数,CPU 要看细粒度采样的分项,两者还得放在同一条时间轴上互相对照。
第三个坑,只看总值不看分项。同样是三成使用率,三成纯 user 和三成 sys 是两件完全不同的事;如果里面还混着 steal,处置路径又要再岔一次。分项构成决定后续所有工作量,跳过这一步必然走弯路。
第四个坑,忘了自己跑在虚拟化环境里。不少人在云主机上把所有可能性都试了一遍,唯独没看一眼 steal 这一栏。这一段说的是"你的时间被别人用了",它不属于你能优化的范围,攒好证据去找宿主机一侧核对才是正解。
第五个坑,把平均负载和使用率当成互相印证的两个副本。它们一个是任务个数、一个是时间占比,统计对象根本不同;正因如此,两者的背离才是有价值的线索,而不是需要被消除的矛盾。
第六个坑,只盯总量不看每核分布。一颗核吃满、其余闲着的画面,经过算术平均之后会变成一个看起来很健康的数字。凡是延迟敏感的服务,逐核视图都应该是常态视角,而不是排查时才想起来看一眼。
第七个坑,把"使用率不高"当成"CPU 有余量"的同义词。两者之间隔着的正是上下文切换的隐性代价、排队等待的时间,以及那些被调度安排推迟掉的工作量。使用率低只能说明核没被占满,不能说明请求没有被耽误。
第八个坑,动手改配置之前不留基线。什么都没记录就先优化一轮,事后既证明不了有效,也没法回滚。排查的第一步永远是采集,第二步是推测,第三步才是动手。
本文的技术判断,来自通用操作系统对 CPU 时间的分类记账口径,以及用户态、系统态、等待 IO、硬中断与软中断、虚拟化环境下被占用的时间、空闲这几项在主流实现中的通行定义;行文里出现的百分比与比例均为说明逻辑关系的示意性算式,或者个人习惯上的观察方式,不代表任何实测数据、具体机型参数或真实客户案例。本文不讨论容器侧的 CPU 配额与节流,不涉及网卡多队列与中断亲和,也不讨论 CPU 频率、睿频与 C-state,以及内存通道与 NUMA 拓扑,这些各自是独立的主题。
本文为行业技术分析,涉及产品、配置与价格以签约时最新报价与合同为准,参考来源 https://www.idc10000.net/ 相关页面。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品