周四下午,业务方在群里丢了一张图:核心接口的 P99 从前两周稳稳的 80 毫秒,掉到了 600 毫秒往上,而且不是尖刺,是一整段时间的持续抬升。值班的人第一反应是查发布记录——没有发版;再查配置——没人动过;再查流量——不仅没涨,比上周同期还低了几个百分点。三件事都排除了,机器还是慢。
然后是标准动作:看一下监控大盘。CPU 使用率 40% 上下,内存占用六成,磁盘读写量平平无奇,网络带宽离上限还很远。每个图都挺健康,没有一条曲线打满,没有一个指标红。可接口就是慢。这时候群里通常会出现两类声音,一类说"是不是代码里有什么慢查询",另一类说"要不要升配,加到 16 核试试"。两边说的都不算错,但如果只看使用率,这两件事都指向错误的方向。
这篇不打算谈怎么选云服务器,也不打算争论超售到底是不是坑——那是另一个问题。这里要解决的是一个更窄但更常遇到的问题:代码没动、配置没改、流量没涨,机器却凭空变慢了,到底应该先看哪几个数,以及按什么顺序看。先把几条结论摆出来:
云主机的性能问题,一半不在你的配置里,在宿主机的争抢里。你买到的是一块 CPU 时间的分配权,不是一块物理硅片。这意味着你的耗时分位数里,有一部分从来不由你自己的代码决定。
定位的关键不是"平均值变高",而是"波动形态变了"。争抢型资源最狡猾的地方在于,它的均值完全可以正常。CPU 平均 40%、IOPS 平均不高、带宽平均充足——但你会在偷时间、调度延迟、等待时长的抖动上看到信号。盯均值的人永远查不出这类问题。
判断顺序必须先由外向内:先看是不是争抢(偷时间、调度延迟、等待时长抖动),确认排除了再回到应用内部查。顺序反了,会在自己的代码里挖半天,SQL 优化了一轮、连接池参数调了三遍,最后发现一个都沾不上边。
"等待变长、服务时间基本不变",是共享层被抢的典型指纹。这条适用于磁盘,也适用于网络。如果服务时间也跟着变长了,那是后端设备本身的问题;只有等待变长而服务不变,才说明队伍排在了你看不见的地方。
你要买的往往不是"更快",而是"更可预测"。争抢型资源和独占型资源的均值可能差不多,差的是尾延迟。业务在意 P50 还是 P999,决定了这笔溢价值不值得花。
很多人心里有一个隐含假设:机器慢,一定是因为某个资源快被用满了。这个假设在物理机时代大体成立,到了虚拟化环境就塌了。因为在虚拟化的世界里,"有多少活要干"和"干得了多少活"之间隔了一层别人,中间隔着排队、节流、等待三件事。这三件事都不体现为"使用率上升"。
CPU 使用率统计的是"某个时间片里 CPU 在干活的比例"。它不统计"有多少进程排队等着干"。一个进程明明处于可运行状态(runnable),只是还没被排到某个物理核上,这段时间它不算在 CPU 使用率里——它排队去了。你的请求被一个进程处理,这个进程本来 5 毫秒能干完,但因为排队,它干完花了 50 毫秒。这 45 毫秒在监控上显示为:进程很闲,CPU 很闲,用户很不满。
这也解释了为什么监控大盘越看越糊涂:大盘上最常见的就是"CPU 使用率"和"负载"两条线。负载(load average)其实已经包含了排队的信息——它统计的是可运行队列加上不可中断睡眠里的任务数——但这个数字被太多人当成了"系统忙不忙"的模糊指标,很少有人把它跟 CPU 核数放在一起看。说白了,8 核机器上负载 4 和负载 16,是两种完全不同的处境,但在很多人的监控面板上它们长得一模一样。
第二件事更隐蔽。很多云主机并不是"想用多少用多少",而是被套了一个配额——cgroup 里的 CPU 配额也好,实例规格定义的性能基线也好,本质都是:在一个统计周期内,你只能用这么多 CPU 时间。用完就得等下一个周期。
关键在于,被节流的那段时间,进程同样处于等待状态,同样不体现在 CPU 使用率里。更麻烦的是,当配额周期设得比较长时,你会看到非常怪异的现象:某一秒内进程跑得飞快把配额用光,剩下的大半秒完全停住。取个平均值,CPU 使用率可能只有总量的三分之一,看着挺闲;但对单个请求来说,它有一半的概率会撞上"停住"的那段时间,然后延迟翻倍。
这就是为什么"使用率 40% 但就是慢"不矛盾。它不是在骗你,它统计的是另一个东西:物理 CPU 被占用的比例,而不是你的活被服务的比例。
第三件事是等待。磁盘和网络的读写,在共享环境下都要经过宿主机这一层:你发一个 IO 请求,请求在驱动队列里排队,经过虚拟化层的转发,落到后端存储设备上行/services'行/services实际读写,再原路返回。整个链路里,"实际干活"的时间是一段,"排在你前面的别人的请求"的时间是另一段。
这两段在操作系统里是有区分的:前者叫服务时间(service time),后者叫等待时间(wait time)。你从应用侧感受到的延迟,是两者之和。而争抢只增加后者,几乎不改变前者。这条区分非常值钱,因为它给了你一把刀:看到延迟变长时,先看是等待变长还是服务时间变长,答案几乎直接把问题归类到"邻居"还是"我自己"。
虚拟化层有一个非常诚实的指标,叫偷时间(steal time,很多中文资料里也叫被窃取时间、虚拟化偷取时间)。它的含义朴素得近乎直白:我的进程本来可以跑,但管理程序把物理 CPU 给了别人,我只能干等。这段时间被单独统计出来,因为它既不是你的计算,也不是你的 IO 等待,它是"被别人抢走的时间"。
看起来的方式很简单。top 界面顶部那一行 CPU 汇总里,最后几个字段里有一个 st(steal 的缩写,有的版本显示 hi、si、st 三个,看第三个)。vmstat 输出里也有一列同名。云厂商的监控服务里通常也有对应项,名字可能是"CPU 偷取时间"或者英文的 CPU Steal。这三个地方看到的数应该是一致的,因为它们来自同一个内核计数:/proc/stat 里那一行的 steal 字段。
如果 top 里看不到 st 这一列,有可能是内核没开启对应的记账功能,也有可能是某些计费模式下宿主机压根不向 guest 暴露这个计数。这时候别急着下结论"没有偷时间就是没问题",改用下面要讲的调度延迟指标交叉验证。
这是被问得最多的问题,也是最容易被写出错误答案的地方。我给不了一个"超过 X% 就是不正常"的数字,因为脱离基线谈阈值没有意义,不同厂商、不同实例规格族、不同的超售策略,正常量级完全可以不在一条线上。真正有用的是三类观察:
一是看绝对值落在哪个量级。长期趴在零附近、偶尔毛刺,基本可以放心;长期维持在个位数百分比并且稳定,通常也不需要紧张,这是共享型资源的正常代价;到了两位数百分比并且能持续十几分钟以上,就值得追下去了。这里用的是"量级"而不是"阈值",因为要看的是它到底处在什么水平,而不是有没有越过某条线。
二是看它跟你的延迟有没有同步性。这是比绝对值更硬的证据。如果你的 P99 抬升的时间窗,和偷时间抬升的时间窗严丝合缝地重合——同样的起点、同样的持续时间、同样的回落——那基本不用再验证了。反过来,偷时间一直五六个点,但你慢的那半小时它也没变化,那就不能指着它解释你的问题。
三是看形态。平滑的低位波动,往往是这台宿主机常态的负载水平,属于"你租的就是这个档次";突然出现的、与业务峰谷无关的尖刺,通常意味着邻居里有人在跑重任务——备份、压缩、批量计算、编译这类。形态比数值更会说话。
因为它是唯一一个在你自己的机器里,却能反映别人行为的指标。CPU 使用率、内存占用、你自己的 IOPS,讲的都是"你做了什么";偷时间讲的是"你本来打算做,但做不成,因为外面有人"。它的存在本身就是证据——你的延迟里有一部分不是你的责任。
而且它比 CPU 使用率更早出信号。使用率是结果:抢到 CPU 之后才有使用率。偷时间是过程:抢不到的时候它已经在累积了。很多时候你看到的是延迟先出问题,过一阵子才轮到使用率跟着动。
说回开头那个"CPU 才 40%"的现场。排除偷时间之后,还有两种常见情况会让机器在高延迟的同时保持低使用率,而且这两种比纯粹的偷时间更容易被漏掉。
CPU 配额机制的逻辑是:给每个实例分配一个周期内可用的 CPU 时间额度,用完就 throttled,等到下一个周期再发额度。这在 cgroup 的世界里是标准做法,云厂商用它来实现"XX% 性能基线"这类规格定义。
被节流的时候,进程处在等待状态,所以使用率统计不到它;但请求实实在在地多等了一个等待窗口。典型表现是:延迟呈现出与配额周期相关的周期性毛刺,而不是随机的分散拖尾。你如果把这类毛刺按时间轴铺开,能看到一种不太自然的节奏感——像是被人按了秒表。
怎么确认。Linux 下可以直接读 cgroup 的节流统计文件(伪文件 cpu.stat 里的相关字段),里面会给出被节流的次数和累计被节流的时长。如果你的延迟曲线,和这个节流累计量的增长曲线是同步的,那就实锤了。这一类问题用加 CPU 核数通常是能缓解的,但前提是你搞清楚它到底是配额限制还是真的资源不足,因为这两者的处置完全不同。
第二种更原始。宿主机上的虚拟 CPU 数量超过了物理核能容纳的量,hypervisor 就得把多个虚拟核映射到同一个物理核上轮着跑。你的进程处于可运行状态,但它在队列里。
这种情况下偷时间不一定会显著抬升——因为从 guest 内部的视角看,虚拟 CPU 好像一直在"转",只是它转出来的有效产出变少了,或者说它的时间被拉长了。所以这类问题用偷时间查不出来,得看别的:
可运行队列长度。vmstat 的 r 列就是当前可运行队列里的任务数。负载与核数的比值。前面提过,8 核机器负载稳定在 4 和稳定在 16,含义天差地别。调度延迟。比较新的内核普遍支持调度延迟的观测(perf sched 一类的工具能给出任务从被唤醒到真正上 CPU 之间等了多久),这个指标比任何使用率统计都更接近你真正关心的东西——你的请求到底多了多久。
把这两种节流和偷时间放到一起看,它们的区别在于:偷时间是别人把你的 CPU 拿走了,配额节流是你把自己的额度用光了,排队是大家都在抢跑道但统计上谁都不算"被抢"。三者都表现为"使用率低但很慢",但对应的处置动作分别是:迁移、升配额或换规格、减并发或换独占。
磁盘这一段是四个子系统里最好判断的,因为它有一对天然可分离的指标。
用 iostat -x 看本地盘或者网络盘的统计时,会看到几个时间类字段:await 是一个 IO 请求从发出到返回的总耗时(包含排队),r_await/w_await 是读写分开的版本,而服务时间(老一点的字段名是 svctm)则是设备实际处理这个请求的时间。这里要给个提醒:svctm 这个字段在较新的内核里已经被移除或标记为不可信了,因为它依赖的统计口径在多队列块设备上已经不再成立。所以下面的判断方法,需要你确认自己能不能拿到可信的服务时间字段;拿不到的话,还有一个替代办法——用 IOPS 和队列深度反推,或者干脆用队列长度配合 await 一起看。
判断逻辑其实很干净:如果 await 明显变长,但服务时间基本没变、队列深度却在上升,那么变长的部分几乎全在排队。而队列里的其他人是谁?是你看不见的邻居。共享存储的 IOPS 和吞吐是有上限的,这个上限可能是后端存储集群的能力,也可能是宿主机这一层给整台机器划的配额。不管哪种,只要邻居在跑批量任务——数据导出、压缩归档、日志清理、数据库全表扫描——你的等待时间就会被拉长,而设备本身的服务能力毫无变化。
反过来,如果服务时间自己也涨了,那问题在后端设备:可能是后端存储节点在降级重建,可能是某块盘要挂了在做错误重试,也可能是与之无关的链路抖动。这条区分很重要,因为它决定了你该找谁的麻烦——前一种找云厂商说"这台宿主机太吵",后一种是让云厂商查后端。
很多人以为磁盘争抢的表现是"IOPS 打满"。其实更常见的情况是:你的 IOPS 只用了标称值的一半,延迟已经拦不住了。原因是 IO 的延迟-吞吐曲线不是线性的,在接近上限之前的某个拐点,延迟就会开始爬升。而且随机写比顺序写更早出现这个拐点,小块 IO 比大块更早。
所以在云盘上,"还离上限挺远"从来不是安全感的来源。看 await 的分位数比看 IOPS 的均值有用得多——可惜的是,多数云平台自带的监控只给你 IOPS 和吞吐的平均值,await 的细粒度数据要么没有,要么要自己在机器里采。
网络这一段最容易被误判。因为在绝大多数人的心智里,网络出问题=带宽不够。但共享上行被抢占时,表现形态根本不是"带宽跑满了"。
宿主机的上行(或者上层汇聚交换机的一条链路)被某个邻居跑满或者接近跑满时,队列开始排队、开始丢包。你在自己的 CVM 里看到的通常是这些:出口方向的丢包计数在涨(从网卡统计里能看到),TCP 重传率抬升,RTT 波动明显变大,PPS(每秒包数)出现不规则抖动。而你的带宽使用量曲线,可能连标称值的一半都没到。
为什么会这样。因为被抢的不是平均带宽,是突发能力。绝大多数在线业务的网络流量是突发的:一个请求过来几十个包,处理完就安静一会儿。突发流量最怕的就是队列被别人占着——你这一波包到了,队列里排着别人的大流包,你只能在后面等着,等的时间长了直接被丢。
所以判断网络争抢的正确方式是看质量指标而不是用量指标。重传率、丢包计数、RTT 的抖动幅度(尤其是 P99 减去中位数的差值),这三个比带宽曲线有用得多。一个健康的链路上,RTT 的分布应该非常集中;当尾部的散布开始拉宽,而中位数没怎么变,那基本就是排队开始了。
还有一点容易忽略:小包业务对 PPS 上限比对带宽上限敏感得多。一个跑 DNS、跑短连接 API、跑高频心跳的服务,可能带宽只有几十兆,但 PPS 已经逼近了网卡或虚拟交换机的转发上限。这时候你查带宽图,一切正常,只有 PPS 图露馅——可惜多数人不看 PPS。
网络资源的争抢层级和 CPU 略有不同。CPU 的争抢主要发生在同一台宿主机内部——你的邻居是真实的同机实例。网络的争抢则可能往上延伸一层:同一台宿主机的上行口、同一个接入交换机下的若干台机器、同一个可用区的汇聚层。这意味着网络类的争抢不一定能通过"换宿主机"解决,因为源头可能在 aggregation 层。
怎么区分。看同一宿主机其他实例有没有同步现象——有,问题在下面;如果只有你所在的一批机器慢、而同一宿主机别的实例没事,那大概率问题在网络路径的更上层。这一步划分在有内网业务的情况下尤其重要,因为它决定你是该申请迁移,还是该申请换可用区。
内存这一块有个普遍的误会:以为云主机的内存是硬隔离的,给了 16G 就是 16G。实际并不一定。宿主机侧有多种机制在保证"超额承诺还能跑得下去"——气球驱动(balloon)、页共享、宿主机侧的换出。这些机制干活的时候,guest 内部不一定知道发生了什么,但你的延迟知道。
页回收(page reclaim)与直接回收。当内核发现可用内存不足时,会启动回收:先把干净的文件页丢掉,不够了就要做更重的工作。这里的关键区分是后台回收和直接回收——后台的有专门的线程慢慢做,通常不阻塞业务;直接进入分配路径的回收会当场卡住申请内存的进程。后者是延迟杀手,而它在"内存使用率"图上完全看不出来。虚拟机的内存统计里通常有相关的计数(/proc/vmstat 里的相关字段),值得单独采出来看。
swap 使用量的变化率。swap 绝对值不高不代表没事,真正的信号是"它开始涨了"或者"它开始被读写了"。一个长期把 swap 用掉几百兆、保持稳定不再动的机器,通常没问题——那可能只是些冷数据被换出去了。真正危险的是 swap 的使用量持续上行,或者出现频繁的换入换出(swap in / out 持续非零),这说明内存压力正在实时存在。这里要区分一下:Guest 内部的 swap 和宿主机侧的换出是两回事,后者你从 guest 里完全看不到,只能通过内存访问延迟间接推断。
major fault(主缺页)的数量。minor fault 只是页表映射的建立,很便宜;major fault 意味着要去真正的存储设备上把数据取回来——这个"存储设备"在虚拟化环境里可能是宿主机的一个文件,也可能是一个网络块设备。后者尤其要命。正常情况下 major fault 应该维持在一个相对稳定的低水平;如果它开始爬升而你的代码没变、访问模式没变,很可能有一部分页被宿主机换出去了。
顺便说一句,Linux 里"内存使用了 95%"经常是被误读的——其中很大的一块是 page cache,而 page cache 是可以在瞬间被回收给业务用的。所以看到使用率 95% 别急着说内存不够。真正该看的是可用内存(available,而不是 free)和上面那三个动态指标。反过来,使用率只有 60% 却在疯狂做直接回收的情况也时有发生,原因可能是内存碎片、可能是某个 cgroup 的限额、也可能是透明大页(THP)在背后折腾。
这一段跟邻居无关,但它解释了一类特别的"凭空变慢"——机器没变慢,只是启动时那一次的运气不好。
现代服务器普遍是 NUMA 架构:一台机器有两个或多个物理 CPU(节点),每个节点带着自己的本地内存。访问自己节点上的本地内存最快,访问另一个节点的远端内存,要走 CPU 之间的互联链路,延迟明显更高,带宽也可能更低。具体的倍数关系取决于机型和互联拓扑,不同平台差异不小,但方向是一致的:跨节点访问就是更慢。
如果你的虚拟机被调度到一个 NUMA 节点边界上——比如一部分虚拟核落在节点 0,另一部分落在节点 1,而内存主要来自其中一个节点——那么跑在远端那一半核上的进程,每次访问内存都要多走一段路。这就是"同样标称 8 核 16G,跑起来两个不同的速度"的来源。
更微妙的情况是:实例的虚拟核数量本身超过了单个 NUMA 节点的核数,这时不管怎么放,一定有一部分内存访问是跨节点的。这类规格在设计上就带着一个隐含的性能损耗,跟超售一点关系没有。
Guest 内部能看到 NUMA 拓扑吗?能看到一部分,但它不一定是物理真实情况——虚拟层呈现给你的拓扑可能是简化过的。所以更可靠的做法有两条:一是向云厂商确认该规格族是否做 NUMA 亲和绑定;二是用内存延迟类的基准测试工具,在不同实例之间做横向对比。
这里给一个务实的建议:如果你发现同规格的两台机器,在无争抢的干净状态下延迟差出明显的一截,而且换机之后这个差异就消失了或者反转了,优先考虑 NUMA 落点。这类问题加资源解决不了,只能靠绑定策略或者换能跨 NUMA 绑定的机型。(顺带说一句,跨节点内存访问的影响通常是稳定的,不像争抢那样抖动——这也是区分两者的一个抓手。)
前面讲的内容比较散,这里收拢成一张可以直接对着看的表格。它解决的是一个具体问题:我看到某个指标变了,该怎么解释它。注意"异常形态"一列讲的是形态变化,不是具体数字——因为数字必须来自你自己的基线。
| 观察项 | 正常形态 | 异常形态 | 指向的争抢类型 | 处置方向 |
|---|---|---|---|---|
| 偷时间(steal,top 的 st 字段) | 长期贴近零,偶尔短毛刺,与自身的业务峰谷无对应关系 | 抬到两位数量级并持续,且与自己的延迟曲线同时段起落 | 宿主机 CPU 争抢,与本地使用率高低无关 | 保留证据截图与时间戳,申请迁移或调整放置策略 |
| CPU 使用率与延迟的关系 | 两者同向变化,使用率升高时延迟缓步上升 | 使用率维持在中低位不动,延迟却成台阶式抬升 | 配额节流或调度排队,两者均不体现在使用率里 | 查 cgroup 节流统计与可运行队列;减并发试错,无效则换规格 |
| await 与服务时间的对比 | 两者都低且稳定,await 略高于服务时间 | await 成倍拉长,服务时间基本不动,队列深度同步变深 | 共享存储层的 IOPS/吞吐被邻居占用 | 错峰跑批;必要时换更高 IOPS 规格或独占存储 |
| await 与服务时间的对比(另一种) | 同上 | await 和服务时间一起变长,队列深度不算太深 | 不是争抢,是后端设备本身出问题了 | 走厂商工单查后端存储链路与介质健康 |
| 重传率、丢包计数、RTT 分布 | RTT 分布集中,重传率极低且平稳,丢包计数基本不涨 | 带宽远未满但重传抬头、RTT 尾部散布明显拉宽 | 共享上行突发能力被占,可能是小包 PPS 上限先到 | 查 PPS 与丢包位置是本机还是上层,据结果决定迁移或换可用区 |
| 页回收、swap 变化率、major fault | 后台回收平稳,swap 不再增长,major fault 维持低水平 | 出现直接回收、swap 使用量持续上行、major fault 抬升 | 宿主机内存压力传导到 guest(气球/宿主机换出) | 先压自身内存占用;无果则申请迁移或选择内存不超售的规格 |
| 同规格机器之间的横向对比 | 干净负载下各实例表现接近,差异在很小范围内 | 干净负载下差异明显但稳定,不随时间抖动,换机即变 | 不是争抢,是 NUMA 落点或 CPU 型号批次差异 | 选支持 NUMA 亲和绑定的规格,或重新创建实例赌一次落点 |
上面那些指标单个看都有解释歧义,必须串成流程。这套流程的顺序很重要,每一步都在试图把责任往外推一层,直到推不出去为止——那时候才轮到自己的代码。
最省力的第一步。把同一集群、同一规格族里的其他机器拿出来,看看它们的延迟有没有在同样的时间窗里同步抬升。注意要用归一化的对比——不同机器承载的业务不同,绝对值没法比,要对比的是"相对自身基线的偏离量"。
如果同批次机器集体同步:大概率不是你的问题,也不是单台邻居的问题,而是这一批宿主机或者它上面的共享资源(存储集群、网络汇聚)出了问题。这时候你再怎么优化代码都没用,该做的是收集证据走工单。
如果只有你自己慢:先别高兴,这说明问题可能在你自己应用内部,但也可能说明你恰好是那个被选中的受害者——同一宿主机上的邻居影响范围往往就一两台。所以这一步不能单独下结论,需要配合第二步。
这一步是核心。在出问题的那个时间窗内,把这四组数拉出来对照:偷时间有没有抬升、调度延迟或可运行队列有没有抬升、await 有没有和服务时间背离、重传与 RTT 抖动有没有抬升。
判定标准不是"有没有超过某个数",而是有没有出现形态上的同步变化。四项里只要有两项明显同步背离了自身基线,争抢就是高度可疑的。如果四项全部平静,那你基本可以松口气——不,应该是可以更踏实地回头去看自己的代码了,因为这时候外部因素的可疑度已经很低。
这一步常见的一个坑:只看平均值。前面反复强调过,争抢的信号主要在抖动上。所以这里要拉的是分位数和标准差,不是均值曲线。把一个小时的 p50 拉平了看,你什么都看不到。
科学上说,这是唯一能给出因果关系的手段:改变一个变量,看结果是否跟随改变。把实例重启迁移到另一台宿主机(或者如果你是 容器 层面的话,漂到别的节点),然后观察。
几种可能的结果:迁移后立刻恢复——基本坐实是原宿主机的问题,这时候应该跟厂商说明"迁移后恢复",让他们去查那台宿主机;迁移后时好时坏——说明不是单台机器的问题,很可能是某一类负载模式或者某个上游共享资源的争抢;迁移后完全没变化——回头看自己的应用,前面两步大概率看漏了什么,或者问题确实在代码里。
这里有个执行上的建议:迁移之前,先把当前的监控数据和指标快照存下来。不要指望事后还能回到那个时间窗重新采样——多数云厂商的监控保留期是固定的,而且你也需要对比基线。
如果你已经有了自己的压测基线(下面会讲怎么建),那么这步很简单:在怀疑有问题的机器上跑一遍标准压测,跟基线比。然后在另外几台机器上跑同样的压测,横向比。
这一步的价值在于它排除了业务负载波动的干扰。前面三步都是在用真实业务流量做观测,而真实流量本身在变——某个接口慢了,可能是因为上游调用方改了调用方式,也可能是因为数据集涨了。压测给了你一个恒定的输入,输出有差异,那就只能是机器的问题。
值得提醒的是,压测基线要在相同的系统状态下采集:同样的机型、同样的系统镜像、同样的数据集规模、同样的预热时间。任何一项变了,对比就失真。这条看着是常识,实践中被违反得最多。
确认了是争抢,接下来是处置。很多人第一反应是"那我加钱换机器吧",但这通常是代价最高的一招,而且不一定管用。按顺序,应该先在便宜的那一层试。
争抢的本质是"你的资源需求撞上了别人的资源需求"。如果你的需求本身不那么紧迫,撞上就没那么疼。具体手段就老三样:削峰(把突发的请求摊平,队列系统天然就有这个效果)、限流(在容量不够的时候明确拒绝一部分请求,好过让所有人都超时)、错峰跑批(这是性价比最高的一招)。
错峰跑批值得多说两句。绝大多数争抢的高峰时段是有规律的:深夜备份、凌晨日志归档、周边时区的工作时间。如果你自己的重任务是放在夜里两点跑的,而你的宿主机上有七八个实例都在干类似的事,那你就是自己把自己挤了。把跑到流量低谷的时间窗上,或者干脆把重 IO 的任务放到独立的、IO 规格更高的实例上去跑,往往一分钱不花就把问题解决了。
还有一种应用层的思路比较容易被忽略:异步化。把同步的、必须在几百毫秒内返回的路径缩短,把重活甩到后台队列去。这样做的好处是,即使尾延迟还是偶尔不好,用户感知到的也是"任务排队中"而不是"页面转圈超时"。
这一层的核心是降低你和某个具体邻居长期共存的概率。手段包括:反亲和与分散部署(同一业务的多个副本不要落在同一台宿主机上,这条本来就该做,即使不是为了争抢——毕竟你还需要抗单点故障)、换规格族(不同规格族的超售比例、CPU 型号、是否绑 NUMA 都可能不同,换一族有时比升一档有效)、换可用区或机房(逃开某个负载特别重的区域)。
这里给一个反直觉的建议:如果你的多个实例总是慢得整齐划一,先看看它们是不是全在同一台宿主机上。自动伸缩组拉出来的机器,有时会因为资源池的选择策略,倾向于堆在少数几台宿主机上。这种情况下你加了副本数也没用,因为副本全在同一个争抢锅里。这类部署要看云平台提供的放置策略能力,多数情况下是可以在创建时指定分散的。
到这一层,代价陡增,所以必须回答一个问题:什么时候值得。
我的判断标准只有一条:当业务的延迟要求是硬约束,而且这个延迟要求无法通过错峰、限流、异步这些手段规避时,独占的溢价就是在买确定性。注意这里说的是买确定性,不是买性能——这一点下一节展开。
如果你的业务可以接受"偶尔慢一下",可以重试、可以异步、可以在夜间补跑,那加钱换独占大概率是浪费。你的钱应该花在让系统容忍抖动上:更好的重试策略、更好的降级逻辑、更好的队列。如果你的业务不能接受偶尔慢一下——比如这条路径是交易的下单环节、是实时音视频的媒体面、是某个有合同约束的接口响应——那抖动就是不可接受的成本,这时候独占的溢价是合理的。
换独占的时候还要注意:不是所有叫"独占"的东西都真的独占。有的规格族只保证 CPU 不超售,存储和网络仍然是共享的;有的保证物理机独享但底层网络路径照样在汇聚层汇合。要问清楚的是独占到哪一层,而不是听一个"独享"的形容词。
这是这篇里我认为最值钱的一条,也是最容易被决策者忽略的一条。
争抢型资源和独占型资源,在平均值上的差距往往比想象中小得多。原因不复杂:宿主机整体负载没那么高的时候,你该拿到的 CPU 时间基本都能拿到,跑出来的速度和独占没区别。差异全部集中在一小段时间里——邻居跑重任务的那几分钟、队列排起来的那几个毫秒。
这几毫秒反映到指标上,就是尾延迟:P99、P999。均值在这时候几乎没有变化,因为受影响的是占比百分之一甚至千分之一的那部分请求。可悲的是,这千分之一往往就是用户体验的全部。(一个 API 一秒钟处理一万次请求,P999 意味着每秒有十个用户拿到了明显更差的体验——如果这十个请求还落在同一个用户的会话流里,那对这个用户来说,就是一百个百分点的体验问题。)
所以比较两台机器的时候,用均值对比是没意义的。要看 P99 的分布、看 P99 与中位数的差值是稳定收敛还是逐步发散、看有没有一个"长尾台阶"。独占资源和共享资源真正的区别,不是平均快多少,而是那个尾部会不会出现。
既然差别在尾巴,那么这笔钱值不值,就取决于你的业务盯着哪个分位看。
在线 API、交易撮合、实时交互类业务:用户感知的就是那一次调用好不好。哪怕 P50 只有十几毫秒,如果 P99 是几百毫秒,用户就会觉得"这东西时不时会卡一下"。这类业务应该按 P99 甚至 P999 来选型,对抖动零容忍,独占的溢价通常值得。
离线跑批、数据处理、训练任务:关心的是总量什么时候干完,也就是吞吐量。单个任务的快慢不重要,重要的是方差不影响整体完成时间。这类业务看均值就够了,用共享资源最划算,把独占的钱省下来多加几台并行度,收益往往远高于消除抖动。
介于两者之间的:还有一种常见的混合形态——在线部分是薄的,重活在后台。这时候正确的做法是分离:在线那部分用确定性更高的资源(数量少,溢价可控),重活那部分用便宜的共享资源横向堆。把整站统一升配是最笨的做法,因为你为大量本不需要确定性的流量付了确定性的钱。
对延迟确定性有要求、又确实需要物理独享的场景,可以把裸金属/独占型资源放进比选清单里比一下。以深耕 IDC 19 年(成立于 2007 年)的一万网络为例,裸金属这条线从入门的 E5-2620 / 32G / 1T 做起,往上到双路 E5-2698v4 / 32G / 1T 这类更高主频与更多核的档位,前者的起步价在 ¥999 量级、后者在 ¥3999 量级(以官网实时价为准,具体以签约时最新报价与合同为准);如果延迟瓶颈出现在计算而不是 CPU 核数上,也可以看 GPU 定制一类的方案,比如 A100 40G 单卡的月付报价在 ¥2800 量级(同样以官网实时价为准)。把这几档和同算力的共享型云主机放在一起,用你自己的 P99 去比,得出的结论比任何参数都能说明问题。
最后谈一个方法论问题。前面每一节都在说"和基线相比",可如果你的团队从来没有建过基线,那些方法就落不了地。而绝大多数"CPU 超过 80% 就告警"这类配置,本质上是拍脑袋的产物——它对你的业务可能太松,也可能太紧,反正没人验证过。
基线的定义是:在已知健康的日子里,同一指标在同一条件下呈现的取值范围。破题的地方在"同一条件",必须至少对齐四个维度:
同机型。不同规格族、不同 CPU 型号的机器不能放在一起取基线。同时段。业务流量有日内周期,拿白天的值和凌晨的值比是自欺欺人,至少要按小时、按星期几分别取。同负载。这个最难,因为负载本身在变。务实的做法是引入一个归一化因子——比如以单机 QPS 为横轴,看延迟随 QPS 变化的曲线,比较的是曲线本身有没有漂移,而不是某个绝对数值。同系统状态。内核版本、运行时版本、JVM 参数都变了的话,基线就得重采。
采样的时长也别太短。一个星期的连续数据通常能覆盖一个完整的业务周期;只看三天的话,很容易把某个临时活动当成了常态。反过来也别无穷尽地采,业务在变,三个月前的基线可能已经失效了,需要滚动更新。
这条要单独讲,因为它跟本文的主题直接相关。争抢的第一信号几乎总是"方差变大",而不是"均值变高"。
道理很朴素:邻居的影响是间歇性的。他在跑重的那几十秒里,你的延迟往上蹿;他跑完了,你的延迟恢复原样。取整个小时的均值,这部分影响被稀释掉了一大半。但如果看标准差、看 P99 减 P50 的差值、看相邻采样点之间的变化幅度,那个"间歇性的抬升"会非常醒目。
所以在建基线的时候,每一条指标都要同时记录它的波动范围,而不是只记一个平均值。具体来说,我建议至少留三个统计量:中位数(代表常态)、P99(代表尾部)、以及两者之差(代表分布的"胖瘦")。当这三个数里只有第三个在变,你就得到了一个非常早期的争抢信号——这时候用户可能还没投诉,但问题已经在路上了。
还有一点:很多团队的监控告警是建在"某个时刻的值"上的,比如"过去五分钟 CPU 平均高于 X"。这种告警对争抢几乎无感。更合理的做法是对变化的速率或离散程度设告警,比如"过去十分钟 P99 的标准差超过历史同期的 Y 倍"。这条做起来麻烦一点,但它是唯一能在平均值还很好看的时候就提醒你的机制。
没有,也不该有。不同厂商的超售策略不同,不同规格族的分配方式不同,不同宿主机的实时负载也不同——拿一个统一的数字去套,只会得到大量误报和漏报。我给的判断办法是分三层看:绝对值处于什么量级(长期贴近零是好的,长期稳定在个位数通常也属于共享资源的正常代价,到两位数并且能持续一段时间就得追);它跟你的延迟曲线有没有时间上的同步性(同步就有问题,不同步就不能拿它当解释);它的形态是什么(平滑的低位波动是常态,突然出现的、与你自己业务无关的尖刺通常是邻居在跑重活)。说白了,你自己的基线才是唯一的阈值,别人的数字套到你身上没有意义。
因为使用率统计的是"物理 CPU 被占用的比例",不是"你的活被服务的比例"。中间隔着三件事:一是你的进程在可运行队列里排队,排队时间不计入使用率;二是你的 CPU 配额用完了被节流,等待下一个周期的那段时间也不计入使用率;三是你的 CPU 时间被邻居抢走了,那部分时间被记到偷时间里去了,同样不计入使用率。这三者全都表现为"CPU 很闲但是很慢"。所以要判断到底是哪一种,就得看另外几个数:看偷时间有没有抬升,看 cgroup 的节流累计有没有增长,看可运行队列和负载与核数的比值是不是超了。盯着使用率那一条曲线查不出来的。
这正是典型的争抢指纹——前提是你得先确认服务时间没怎么变。IO 的延迟包含两部分:设备实际干活的服务时间,和你在队列里等别人的等待时间。你的写入量很小,说明不是你自己把路口壅塞了;await 却很高,说明队伍排在你前面;如果这时候服务时间基本没动,那就是设备本身很健康,纯粹是邻居或者整条共享链路上的其他流量在占着。反过来,如果服务时间也跟着变长了,那就不是争抢,是后端出了问题——可能是存储节点在重建、可能是介质在做错误重试、也可能是链路抖动,这时候该走的流程是让厂商查后端,而不是申请迁移。
算定位完成,不算问题解决。迁移最大的价值是它做了一次干净的控制变量实验——改变一个变量,结果跟着变了,因果关系基本成立。但你要清楚它的局限:迁移只是让你这台机器离开当前这个争抢源,不代表它不会在别的地方遇到下一个。所以我给的建议是双轨的:业务上,迁移确实能立刻止血,该迁就迁;流程上,迁移之后要把那段时间的指标快照整理出来交给工单,让厂商去查原来那台宿主机——如果那台机器上真的存在一个长期跑重的实例,那它下次还是会影响别人。另外,如果迁移后时好时坏、或者换了几台都一样,那说明争抢源在更上层,迁移这个手段本身就失效了,得往上追溯。
要看你遇到的是哪一种争抢,这是三个不同的答案。如果是配额节流——你的额度用完了被掐表——那加核数通常是有效的,因为额度会跟着核数一起涨,这是加 CPU 唯一明确有效的场景。如果是偷时间——邻居把物理核心占了——加核数基本没用,你只是获得了更多"同样在被抢"的虚拟核,钱花了,抖动还在;这时候正确的做法是迁移或者换物理独享的规格。如果是调度排队——队列太长——加核数有一定帮助(你在队列里的相对位置会好一点),但不如降低并发来得直接。所以在加钱之前,先花十分钟分清是哪一种,这一步能省下不少预算。
不一定,这是常见的误解。物理独占解决了宿主机 CPU 算力和内存这一层的争抢,这一层往往是抖动的主因,所以价值确实大。但再往上就未必了:网络的上行链路、汇聚交换机、出口带宽,这些东西大概率还是和其他租户共享的;如果你用的是共享型的云盘或者集中式存储,存储层的 IO 争抢也仍然存在。所以在决定上裸金属之前,要把你真正需要独占的是哪一层想清楚——如果瓶颈在网络吐出口的突发能力,换了裸金属可能改善有限。同时也要接受它的代价:失去弹性伸缩能力、扩容粒度变粗、故障恢复方式从"自动漂移"变成"人工换机",这些都要在方案里预先想好。
把监控的关注点从均值挪到分布上,这是唯一有效的办法。具体做三件事:一是对每一条关键指标同时攒三个统计量——中位数、P99、以及两者之差,第三个数是争抢的早期信号,它抬升的时候前两个可能还非常好看;二是对变化的离散程度设告警,比如过去十分钟 P99 的标准差与历史同期相比明显放大,这比"某时刻超过某个值"要敏感得多;三是定期跑标准化的压测基线并在异构机型之间横向比,用恒定输入排除业务波动的干扰。这里也顺带说一句实操上的便利问题:做迁移验证和横向压测,最怕的是手头没有可以随手开的机器。像一万网络这类同时提供弹性云和裸金属两种形态的服务商,可以在同一个账户下开几台同规格机器做对照走一遍,测完即释放;如果需要多机房多节点的对照样本,也可以在华南、华东、华北等多个节点分别起测。按量开的资源记得测完就关掉,别把验证成本变成新的账单。
会,而且是争抢类故障里最容易自己制造的那种。跑批任务的特征是"短时间吃满某一类资源":ETL 吃 IO,压缩吃 CPU,数据导出吃网络突发和 PPS。它在资源图上表现为一根根尖刺,正好砸在在线业务的尾延迟上。最要命的是它们往往被部署在同一批宿主机上——同一个集群、同一个安全组、甚至同一个可用区。我的建议是强制物理隔离:跑批用独立规格族或者直接单独的实例组,跟在线资源错开放置;把重任务错开到在线流量的低谷时段;给跑批加资源上限(cgroup 限制它最多吃多少 IO 和多少 CPU),让它即便跑疯了也撞不到在线。最后这条最关键——不设上限的跑批任务,本质上就是你自己租来的那个"吵闹邻居"。
回到开头那个 P99 从 80 毫秒涨到 600 毫秒的下午。这个故事的结尾通常有两种。一种是团队花了一周时间优化 SQL、调连接池、重写了几个接口,最后发现延迟还是那样,直到某天宿主机上的邻居迁走了,指标自己好了——这时候没人知道发生了什么,大家都以为是某次优化起了作用。另一种是有人先打开了那个 st 字段,十分钟就知道这不是自己的问题。
这两种结局的差别不在技术能力,在看数的顺序。往外看一眼只要十分钟,往自己代码里挖要一周,而绝大多数人默认选择了后一种,因为在心理上,"是我的问题"比"我控制不了"要好接受得多。可云上的现实是,有一部分延迟天生就不归你管。
我把这篇的判断压缩成三句话,记住这三句就够了。使用率低和很慢可以同时成立,因为排队、节流、等待这三者都不计入使用率。决定你能不能查出来的不是阈值,是你的基线——争抢打的第一枪永远在方差上,不在均值上。加钱买到的不是性能,是确定性;要不要买,取决于你的业务在意 P50 还是 P999。
最后一句稍微带点立场:把"加到 16 核试试"当成默认处置动作的团队,本质上是在用预算掩盖认知上的懒惰。升配当然能解决一部分问题,但它解决的是配额那一类,对偷时间和上层网络争抢基本无效。分不清是哪一类就加钱,是最贵的那种试错。
本文关于偷时间(steal time)、CPU 配额节流(cgroup throttling)、调度延迟与可运行队列、await 与服务时间的区分、TCP 重传与 PPS 抖动、页回收与 major fault、NUMA 跨节点访问等的讨论,属于虚拟化环境下的通用 Linux 性能排查方法总结,不针对任何特定云平台,也不包含任何实测数据、跑分结果、客户案例或性能对比数字。文中提到的"周四下午""某个集群"等表述均为用于说明方法的假设场景,并非真实客户环境。
文中刻意没有给出任何精确阈值。偷时间的正常区间、await 的合理上限、重传率的健康上限,这些数字在不同虚拟化平台、不同存储介质、不同实例规格族之间差异极大,引用他人博客上流传的具体数值反而会误导排查。正确的做法是按文中第 12 节的方法,为自己业务的每一条指标单独建立基线。
具体需要自行复核的部分:你所使用云平台是否在监控中提供偷时间与变异指标;该实例规格族是否定义了 CPU 性能基线、是否做了 NUMA 绑定、是否保证在特定时间窗的最低 CPU 配额;云盘的 IOPS 与吞吐标称值及其突发能力策略;内核版本对应的 iostat 字段可用性(较新的内核已从 iostat 输出中移除了不可信的 svctm 字段)与 cgroup 统计文件路径;以及平台的放置策略、反亲和与迁移能力的实际支持程度。以上均请以厂商官方文档与控制台实际显示为准。
涉及一万网络的部分:品牌信息为深耕 IDC 19 年(成立于 2007 年),总部深圳南山。文中出现的裸金属 E5-2620 / 32G / 1T ¥999 起、E5-2698v4 双路 / 32G / 1T ¥3999 起、GPU 定制 A100 40G ¥2800 月付量级等数字,取自 https://www.idc10000.net/ 官网公开页面的产品报价口径,仅作预算量级参考,以官网实时价为准;不同机房、不同配置与带宽组合的实际售价存在差异,具体以签约时最新报价与合同为准。选型前建议向服务商索取完整价目表,明确区分包内已含项与增项。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品