
凌晨两点告警电话把我从床上拽起来核心业务机的load average飙到了32平时还不到3。打开监控的第一反应不是去看代码也不是直接重启而是先看这半小时内的数据变化。系统负载与性能排查这件事经历过几次深夜故障之后就会明白关键不是某个命令记得多熟而是让数据替你完成判断。这篇文章就来聊聊遇到CPU飙升、load异常、接口变慢这类问题时我实际使用的定位链路和思考方式适合正在做后端开发、运维或者SRE的朋友参考读完能直接拿去救火。这种问题有个让人头疼的地方表面现象几乎一样都是“机器很忙、服务很慢”但背后原因可能完全不同。可能是CPU真的被打满也可能是磁盘IO在拖后腿还可能是内存回收引发的连锁反应。直接把机器重启或者扩容很多时候只是把问题往后推了几个小时下一个高峰照样出现。所以我会先从最基础、也最容易被误解的load average说起。1. 负载高不等于CPU忙先看懂load average这张晴雨表1.1 load average到底在统计什么很多人看到uptime输出里“load average: 14.23, 12.10, 9.87”这组数字第一反应是“CPU用了百分之多少”。这是最大的误解。load average不是百分比而是一个数量的概念——它统计的是当前处于可运行状态和不可中断睡眠状态的进程/线程总数。这里有个关键点不只是等待CPU的进程会被计入。内核在计算load时会把两种状态都算进去R状态正在CPU上跑、或者排在运行队列里等待CPU的进程。D状态不可中断睡眠绝大多数时候是在等待磁盘IO或其他硬件IO完成。所以load反映的是整个系统里“还有多少活儿等着被处理”而不仅是CPU排队长度。内核每隔5秒采样一次运行队列长度再用指数加权平均的方式计算出1分钟、5分钟、15分钟三档数值。之所以要保留三个时间窗口就是为了让你能看出趋势1分钟负载高、15分钟低说明是刚刚发生的突发状况三个值都高说明问题已经持续了一段时间。判断load是否过高还得结合CPU核数来看。单核机器load到1就已经饱和4核机器load到4才刚满。比较靠谱的判断方式是用lscpu或者nproc确认核数然后看load是否持续超过核数的1.5到2倍。我一般还会多看一个东西load的1分钟值是不是在快速爬升。如果是从2冲到15哪怕绝对值不高也是明确的报警信号。1.2 最容易被误判的三种load反常现象第一种是load很高但CPU使用率很低。这种组合在初学者的排查里经常被误判成“服务器假死”。实际上当大量进程处于D状态等待IO时CPU大部分时间空闲load却会居高不下。可以用ps -eo state,pid,cmd | grep ^D快速确认当前有多少D状态进程。我曾经碰到一台机器load常年10以上CPU idle却有80%最后查出来是某块共享存储网络抖动所有读写请求都卡在等待IO完成上。第二种是CPU使用率很高但load不高。这通常意味着只有少数几个进程在满负荷运行它们占满了CPU但没有形成明显的排队。比如一个单线程进程在4核机器上跑满一个核CPU利用率25%load大约1这种情况不是系统容量不足而是这个进程自身的设计瓶颈。另外还有一种情况很多寿命极短的进程频繁创建退出每一次都消耗一点点CPU时间但在load采样的瞬间并没有堆积在队列里表现出来就是CPU高、load低。第三种是load虚高但业务指标正常。接口平均响应时间没变、错误率没涨可load就是在高位。这种情况我见过几次背后往往有这些可能周期性任务和大批量数据导出在集中时段运行瞬间把运行队列拉长或者内存不足触发内核回收kswapd这类内核线程大量占用调度资源再或者宿主机层面对CPU资源的限制导致调度延迟。所以读load这个数字时永远要站在业务指标旁边一起看单看一个绝对值没有任何意义。为了快速判断我在排障时习惯用下面这张对照表现象组合大概率原因下一步动作load高 CPU使用率低磁盘IO等待、D状态进程堆积查iostat、确认D状态进程CPU使用率高 load低少量进程打满、短生命周期进程过多按CPU排序看进程必要时看线程级load高 CPU使用率高真正的CPU饱和定位热点函数、考虑扩容CPU和load都低但业务卡锁等待、网络延迟、外部依赖慢看线程状态、链路追踪数据2. 数据先行从全局到局部的最小定位命令集2.1 动手之前先花三分钟明确现象边界很多人一上来就是top猛敲然后在几十个进程里瞎猜这是效率最低的做法。我的习惯是先回答三个问题什么业务受影响、从什么时间开始、之前有没有变更。这三个问题能直接砍掉一半的排查方向。“什么业务受影响”指的是确认是全部接口都慢还是只有某个接口慢。如果只有下单接口慢那问题大概率不在机器本身而是下游数据库或者缓存出了问题。“从什么时间开始”非常关键——把告警发生的时间点和发布记录、定时任务时间表拉在一起看你会发现大量问题其实都叠加在某个固定时段上。层我还接过一个案例每天晚上10点半准时负载飙升查了一圈最后发现是另一个团队设置的数据库全量备份任务磁盘IO瞬时打满和咱们自己的代码一点关系没有。“有没有变更”就是查最近一小时内的配置变更、代码发布和扩缩容操作。这三件事确认完再动手敲命令。即使后面走弯路也能随时回来对照边界不会越查越偏。2.2 top、vmstat、pidstat的组合用法与看数标准我通常会用一套固定组合几分钟内把CPU、内存、IO、上下文切换全部扫一遍# 确认核数和load lscpu | grep -E ^CPU\(s\)|Model name uptime # 全局动态指标每秒刷新连续看几轮 top -c # 内存、IO、CPU的快速快照间隔1秒采样3组 vmstat 1 3 # 每个进程/线程的CPU使用情况持续5秒 pidstat -u -t 1 5 # 整体IO和每块磁盘的负载情况 iostat -x 1 3这套命令的价值在于它从不同维度交叉验证同一个现象。top -c能直接看到按CPU排序的前几个进程以及它们的完整命令行vmstat展示的是系统整体的运行队列、IO等待、上下文切换和内存换页情况pidstat -t则能把进程拆到线程粒度找出到底是哪个线程在消耗资源。读这些数据时我给自己定了几条硬标准top里的us高说明用户态程序在忙sy高说明内核态占用了大量CPUwa高基本可以断定IO在拖后腿st高则表示这台虚拟机所在的物理机资源被邻居抢占。vmstat里的r列表示运行队列里的进程数持续大于核数的1.5倍CPU排队已经比较严重b列表示阻塞进程数只要长期大于0就该引起注意。vmstat里的si和so是swap换入换出量一旦持续非零说明内存已经顶不住了这是在用磁盘当内存用性能会断崖式下跌。pidstat里单线程CPU超过100%很正常那代表它吃掉了多个核。重点看CPU消耗是否集中在某一类线程上比如GC线程、日志线程或者业务处理线程。2.3 单核不均衡与内核态热点mpstat和perf的使用时机整体指标正常但业务就是慢这种情况我会考虑两个方向CPU多核负载不均衡或者热点藏在内核态。mpstat -P ALL 1能列出每个物理核的使用率。如果某些核100%另一些核空闲说明有线程被绑核或者单线程应用成了瓶颈。一次排查中我发现某服务所有流量都打在一个核上查下来是启动参数里隐式设置了CPU亲和性去掉之后整个集群的吞吐量直接翻倍。如果用户态CPU不高、系统态CPU高那就得把视角切到内核。perf top可以实时显示内核态的热点函数比如锁相关、内存分配相关的调用。更完整的做法是用perf record采样一段时间再生成火焰图。这个操作成本不高信息量却很大能直接看到CPU时间到底花在哪个函数上。不过要记住perf在部分生产环境可能需要额外权限执行前先在测试机验证一下避免命令本身引发不稳定。一个实用的建议排查时尽量用pidstat -t锁定线程ID然后用perf top -p 线程ID只看这一个线程的热点。数据粒度越细越不容易被无关进程干扰。3. 四类常见毛病的辨识CPU、内存、磁盘IO、上下文切换3.1 CPU用户态打满从进程到函数剥洋葱CPU被打满是最容易定位的一类问题因为方向很明确。先用top找到CPU占用最高的进程再用pidstat -t拆到线程最后用perf top看函数热点一步步从“哪个进程”收敛到“哪个函数”。以Java服务为例我排查过一个持续CPU高的接口top显示某Worker线程CPU 200%perf里热点集中在正则表达式的匹配函数上。进一步翻代码发现热路径里用了一个复杂正则去解析用户输入输入稍长一点就触发灾难性回溯CPU消耗呈指数上升。这类问题在代码review阶段其实很难看出来必须靠数据层面定位到函数才能触发联想。处理方式也不复杂把正则改成手动解析或者加上长度限制再或者把匹配结果做成缓存。修复后同一个接口的P99延迟从800ms降到30ms。所以CPU问题只要肯往下钻根因都不算难找怕的是停在进程层面就去重启机器那样永远得不到教训。还有一类是系统态CPU高top里sy占30%以上。常见原因包括频繁的系统调用、锁竞争和中断风暴。这时候perf热点里通常会出现spinlock、futex之类关键字。可以再结合strace -c -p 进程ID看系统调用的分布和次数确认是哪个调用过于频繁。不过要提醒一句strace在线上会显著放大性能问题建议只在低峰期短时间采样或者直接靠perf定位。3.2 内存回收与swap抖动load升高的隐形推手内存问题引发的负载飙升特别容易误判因为表面看CPU和磁盘都不忙但load就是高。我遇到过一个典型案例每天晚上8点整某搜索服务负载从2跳到11持续十分钟后回落期间CPU使用率不到30%磁盘IO也不高。最后查下来是内存回收在捣乱。业务在晚间有一个集中写入缓存的操作瞬间产生大量脏页内核为了腾出内存空间触发同步回收把一部分应用线程卡在了内存分配路径上。这些线程虽然没有真正执行逻辑但在load统计里它们可能表现为可运行或不可中断状态load自然就涨上去了。判断内存回收压力有几个信号vmstat里si/so持续非零sar -B里能看到pgscank和pgsteal数值明显放大free -h显示cache占用极高。如果swap已经在缓慢增长说明内存压力不是临时的而是进程内存真的不够了优先排查内存泄漏而不是调整回收参数。针对内存回收导致的load抖动常规做法分两种。短期上把集中写入操作打散到更长时间窗口里避免瞬时制造大量脏页长期上评估业务进程的堆内存和缓存配置让热点数据尽可能留在内存里而不是频繁走磁盘。3.3 磁盘IO等待拖累全链路先从指标里认出等待磁盘IO问题最典型的特征是top里的wa指标偏高同时vmstat里的b列持续大于0。这两个信号同时出现时基本可以断定有大量进程在等待磁盘IO完成。进一步用iostat -x 1看每块磁盘。%util是最直观的指标但不建议单独看它——现在很多SSD和NVMe硬盘在多队列机制下util很容易就到100%可实际延迟不一定高。我会更关注await平均IO响应时间和aqu-sz平均IO队列长度。如果await比正常基线高出好几倍同时aqu-sz长期大于磁盘队列深度那才是真正的IO瓶颈。确定是IO问题后用iotop找具体进程再用pidstat -d看进程的读写速率。我见过一个典型的坑某个服务开启了全量SQL日志每写一条业务记录就同步刷盘两次数据库机器负载被日志写入拖垮。把日志刷盘策略改成异步或者其他组件的慢查询日志关掉负载立刻就下来了。磁盘IO这类问题尤其适合对照代码排查是不是存在循环内频繁写文件、是不是日志级别设置成了DEBUG、是不是临时文件没有及时清理。3.4 上下文切换与锁竞争高并发下看不到的损耗这类问题最隐蔽因为所有常规指标看起来都“正常”CPU有闲余、内存充足、磁盘不忙但业务响应就是慢。这时候一定要看上下文切换。vmstat里的cs列表示每秒上下文切换次数。一般来说几千到几万都正常但如果到了几十万甚至上百万系统大量时间都在做进程/线程切换而不是在真正执行业务。再用pidstat -w 1区分自愿切换和非自愿切换非自愿切换高说明线程数远超可用核数大家在抢CPU时间片自愿切换高说明线程频繁在等待IO或锁睡一会儿醒一会儿调度开销被无限放大。我处理过一个实例某网关服务把线程池从200调大到800结果吞吐量反而下降了四成。因为这台机器只有16核800个线程大多数时间在互相抢CPU非自愿切换飙高响应时间直线上升。把线程池调回256同时配合队列限流性能立刻恢复。线程数不是越大越好经验值是“CPU核数乘以(1 等待时间/计算时间)”但最稳妥的做法还是按业务峰值做压测找吞吐量拐点。锁竞争也是上下文切换的一个重要来源。Java服务可以用jstack抓线程快照看大量线程是否阻塞在同一个锁对象上C服务可以用perf的延迟统计看哪些锁被频繁竞争。抓线程快照不要只抓一次隔几秒抓三到四次看哪些线程的状态反复横跳那基本就是热点锁了。4. 一次CPU占用飙高问题的完整定位复盘4.1 从告警到定位先还原现场再谈修复上面讲的是知识框架这里分享一次让我印象很深的完整排查过程。当时某订单系统每天凌晨2:30固定出现负载尖峰load从2一路飙到15持续20分钟左右接口响应时间从30ms涨到2秒。由于是凌晨用户影响不大但批量任务会被拖垮而且连续几天都在同一时段复发。第一次接到告警时我按老规矩先把时间边界拉出来。2:30的高峰正好有两个定时事项重合日志归档任务和报表导出任务。当时团队内部分歧很大有人说日志归档IO太大有人说报表导出SQL太烂占满CPU。我没有急着下结论先用历史监控数据复盘。当时机器还在运行但高峰快要过去所以我直接查sysstat记录的sar数据把尖峰时段拉出来对比。结果显示%user接近80%%iowait不到5%说明CPU确实被用户态程序消耗了IO嫌疑被排除。再看vmstat的r列持续在20以上远超8核机器的承受能力CPU队列已经严重积压。4.2 根因在异常日志的堆栈拼接进程层面的定位用pidstat -t就完成了。尖峰时段里报表导出进程只占了10%的CPU反而是某个日志处理线程占了300%以上。这个结果把排查方向从“批量任务”拉到“日志处理”上和最初的猜想完全相反。用perf top去看这个线程的热点函数排在最前面的几个符号指向字符串操作和异常堆栈获取。翻代码后找到了问题所在业务代码在捕获异常后会把完整的堆栈信息格式化拼进一条日志字符串里。这个服务在凌晨高峰时段正好有一批异常被频繁触发每一次异常处理都在动态采集线程堆栈——每一帧的类名、方法名、行号都要从运行时环境里临时获取并拼接成字符串。听上去好像没什么但堆栈采集本身是相对昂贵的操作高频触发时立刻成为CPU热点。更糟糕的是这条日志还设置了DEBUG级别正常情况下根本不会被持久化但字符串拼接在内存里已经完整执行了。也就是说大量CPU时间花在了一段最终没有任何输出的代码上。这类问题非常典型代码里的每一处“顺手打印”单独看都不起眼一旦跑在热路径上代价就会被放大千百倍。4.3 止损动作与事后加固定位到根因后我们没有立刻改代码而是先做止损把日志级别从DEBUG调高过滤掉大部分异常日志构造同时把报表导出任务的并发数降一半错开日志归档的时段。这两个动作做完第二天凌晨的load尖峰降了一大半从15降到了5左右。正式修复分成两部分。代码层面异常日志只记录异常类型和消息不再全量输出堆栈业务上确实需要堆栈的场景只记录第一帧或者按采样比例记录避免每次异常都触发完整堆栈采集。任务调度层面报表导出整体平移半小时与日志归档完全错开。这次故障复盘之后我给团队加了一条发布Checklist热路径代码不允许拼接并打印完整堆栈同时新增“异常日志触发次数”指标一旦短时内超过阈值就自动告警。这类问题本质上不是技术难题而是合理的规范缺失积累出来的性能炸弹。5. 把性能问题扼杀在变更之前监控与容量评估的门槛5.1 比告警本身更重要的基线数据排查性能问题最怕“没有参照系”。只知道当前load是15却不知道昨天的同一时段load是多少就很难判断这是突变还是常态。很多团队只在出问题时才看一眼监控曲线事后又不知道保留多长时间的数据导致想对比都无从下手。我的建议是核心指标至少按分钟粒度保存三到六个月包括CPU使用率、load、内存占用、磁盘IO、网络流量、接口P99延迟和错误率。保留这些数据平时看似没用关键时刻能帮你快速回答“和上周同一时段比是不是有漂移”。我还习惯在每次重要版本发布前后记录一份基线快照效果和体检报告一样——下次有问题先对比再排查。5.2 变更前压测与代码热路径审查绝大多数负载问题都来源于变更。代码评审里如果只关注功能正确性很容易漏掉性能隐患。我在review时会特意扫几条规则热路径上有没有正则表达式、反射调用、大对象频繁分配有没有在循环里做IO操作线程池参数是否有压测依据日志输出是否有条件开关控制。这些点只要有一个中招线上就有概率出现负载尖峰。压测是最后一道防线。我的习惯是压测目标至少要达到预估峰值的1.5倍并且不只关注吞吐量还要关注延迟拐点——随着并发升高P99延迟有没有突然上扬。如果压测机器出现上下文切换暴涨、运行队列持续堆积就算功能全通这个版本也不该上线。压测环境越接近生产效果越好至少CPU核数、磁盘类型这些硬指标不能差太远。5.3 错峰与异步低成本抵消负载尖峰的手段不是所有性能问题都要靠代码优化解决很多时候调度策略的调整成本更低、见效更快。定期任务尽量避开整点和半点尤其是数据库备份、报表导出、日志归档这些IO密集任务最好分散在不同时段非核心链路的业务流程改成异步处理削峰填谷能显著降低瞬时load。举一个实际的例子某服务每天凌晨的批量消息推送会把load推到10以上影响正常业务。后来把单批推送改成多批小量推送每批之间间隔几秒再结合异步队列限流高峰load从10降到4而且业务完成时间几乎没有变化。负载管理的本质不是在问题发生后救火而是把资源的消耗曲线尽量拉平让系统在任何时刻都有余量应对突发流量。我在实际处理过这么多性能问题之后最大的体会是遇到load飙升先冷静还原现场再逐层定位最后动手处理。性能问题的表象虽然千篇一律但每个现象的背后的数据都不同。只要愿意顺着数据一层层拆下去答案基本都会自己浮出来。最怕的是凭经验猜猜错了方向还在原地绕圈。希望这套方法和案例能让你在下次遇到类似情况时少走一点弯路。