做Linux运维和开发这些年,我最怕的不是系统直接宕机,而是那种“明明没挂,但所有业务都在卡”的状态。CPU居高不下只是最明显的信号,更多时候负载看着不高、内存显示还有几十G、磁盘也没满,可接口就是慢得像蜗牛。这种问题之所以难搞,是因为CPU、内存、磁盘IO这三个子系统互相牵连,你单独看哪一个都不觉得异常,合在一起就是“系统很吃力”。这篇文章就围绕Linux系统性能瓶颈定位这件事,把CPU、内存、磁盘IO三大核心子系统的诊断方法完整拆开,从命令输出到指标含义、从定位思路到处置手段,都按实战经验来讲。我尽量不堆砌教科书理论,字字都来自一线的排查记录,适合正在被“系统到底慢在哪”折磨的运维、后端开发,以及所有想系统掌握Linux性能排查方法的人。
1. 诊断前先建立全局观
1.1 性能瓶颈定位的三步法
我刚入行那会儿,一遇到性能问题就喜欢直接按load average和CPU使用率判断,结果经常被“假象”带偏。后来踩的坑多了,总结出一套三步走的思路,现在遇到任何性能问题都先按这个顺序走。
第一步是确认现象。先回答几个问题:是整体反应变慢,还是某个接口变慢?是偶发性变慢还是持续性变慢?是最近变更后才开始的,还是一直如此?这几个问题看似简单,却能帮你过滤掉一大半无关信息。比如业务方说“系统慢”,但你一查CPU和IO都正常,那问题可能根本不在本机,而在于数据库的连接数满了、或者依赖的下游服务超时,这就是典型的排查方向错误。
第二步是观察全局指标。用uptime、vmstat、free、iostat这组命令先看机器整体水位,而不是一上来就盯进程。因为性能问题往往是多个子系统共同导致的,只看进程容易被局部数据迷惑。全局指标能帮你快速判断瓶颈的方向:是CPU忙不过来,还是等IO,还是swap在疯狂换页。
第三步是收敛范围。确定了方向后,再用pidstat、perf、iostat -x这类工具精确到进程、到线程、到盘、到文件。这一步是真正的定位过程,前两步做的越扎实,这一步就越快。很多老手最快能在五分钟内定位问题,靠的就是前两步判断精准,第三步根本不需要反复试错。
1.2 工具清单:每一条命令解决什么问题
Linux自带的性能工具其实已经非常够用,不需要一上来就装各种花哨的监控平台。我常用的工具就这十几个,每一个我都试着用大白话给你讲清楚它到底能解决什么问题。
- uptime:看机器过去1分钟、5分钟、15分钟的平均负载。这是最粗粒度的“红绿灯”。
- top:交互式查看全局CPU、内存概况,按CPU或内存排序看进程占用。适合第一眼扫描。
- mpstat -P ALL:按CPU核心分别统计,能看出是单核瓶颈还是整体瓶颈。
- pidstat:按进程或线程输出CPU、内存、IO、上下文切换的详细数据,是精确定位的利器。
- vmstat:输出线程数、上下文切换、CPU状态、内存换页、块设备读写等综合指标,性价比极高。
- free:查看物理内存和swap使用情况,注意区分buff/cache与真实占用。
- iostat -x:输出每个块设备的IOPS、吞吐量、等待时间、队列长度、利用率等详细指标。
- iotop:类似top,但按进程显示IO读写速率,能揪出“谁在偷偷占用磁盘”。
- sar:系统的历史数据记录器,可以回顾过去一段时间CPU、内存、IO的走势。
- slaptop:查看内核slab缓存占用情况,定位内存被内核占用的场景。
- perf:内核级别的采样分析器,能定位CPU热点函数。
- dmesg:查看内核日志,OOM、磁盘IO错误、硬件异常都在这。
你不需要把每个工具都背下来,但至少要知道:粗查用top、vmstat,细查用pidstat、iostat -x,查历史用sar。后面我会按子系统逐层展开,每一条命令该读哪个数字、那个数字该怎么理解、落到实际操作里怎么处置,都会讲到。
2. CPU瓶颈的定位与分析
2.1 用uptime和top把“CPU是否真忙”搞清楚
先说uptime,这是最容易被误读的命令。它的输出长这样:
$ uptime 14:32:10 up 120 days, 3:22, 2 users, load average: 5.02, 4.56, 4.01load average后面三个数字分别是过去1分钟、5分钟、15分钟的平均负载。很多人一看load超过CPU核数就认为CPU瓶颈,这个判断太粗暴了。load average统计的是处于可运行状态(R状态)和不可中断睡眠状态(D状态)的进程数,也就是说,它不仅包含正在用CPU的进程,还包含正在等磁盘IO、等网络IO的不可中断进程。这就是为什么数据库服务器上经常出现load很高但CPU空闲很多的情况——大量的进程在等磁盘读,根本没在用CPU,但load照样被顶上去。
所以我的建议是:先看趋势,而不是看绝对值。如果15分钟前负载还在3,现在突然到5,说明有个什么东西在往上冲,该查;如果负载一直在5上下浮动,但业务没感知到明显卡顿,可能只是机器上有固定批处理任务,不一定需要处理。另外还要看核数,四核机器的load=5和十六核机器的load=5完全是两回事。判断CPU是否真的忙,不能只看load,要看top里us和id的占比。
top命令第二行会输出CPU使用率,这行信息量很大:
%Cpu(s): 12.5 us, 3.2 sy, 0.0 ni, 80.2 id, 3.5 wa, 0.3 hi, 0.3 si, 0.0 stus是用户态,sy是内核态,wa是等待IO,id是空闲,hi是硬中断,si是软中断,st是被虚拟化宿主抢走的时间。判断思路很简单:id只有20%以下,说明CPU确实在忙;如果us占比很高,说明用户态程序在大量消耗CPU;如果sy占比很高,说明内核在忙系统调用、锁竞争或中断处理;如果wa很高,说明CPU在等磁盘,真正的瓶颈在IO而非CPU。
2.2 用mpstat和pidstat精确锁定“谁在消耗CPU”
top能告诉你整体CPU忙不忙,但“整体很忙”和“某几个核心很忙”是两种完全不同的情况。有些应用是单线程密集计算的,比如部分老旧的Java应用、Redis的某些版本、或者你写的某个单线程脚本,它们只会把一个核跑满,其他核都闲着。如果你只看top的总体%CPU,可能只有25%(四核机器),会误判为“CPU没有瓶颈”,但实际上你的业务可能已经被这个单核瓶颈卡死了。
这时候必须用mpstat把每个核心分开看:
$ mpstat -P ALL 1 Linux 5.15.0 (hostname) 07/11/2024 _x86_64_ (8 CPU) CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle all 20.13 0.00 3.52 1.90 0.00 0.38 0.00 0.00 0.00 74.07 0 30.15 0.00 5.26 0.65 0.00 0.75 0.00 0.00 0.00 63.19 1 5.62 0.00 1.25 2.31 0.00 0.20 0.00 0.00 0.00 90.62如果某个核心的%usr明显高于其他核心,说明确实有单线程瓶颈。接下来用pidstat把这个进程找出来:
$ pidstat -u 1 5 Linux 5.15.0 (hostname) 07/11/2024 _x86_64_ (8 CPU) Average: UID PID %usr %system %guest %wait %CPU CPU Command Average: 1001 12345 89.66 2.44 0.00 15.40 92.10 2 java这里要重点看最后一行的Command和%CPU。如果某个进程的%CPU接近100且长驻,基本就锁定目标了。再往下,如果你想看这个进程到底是哪个线程在烧CPU、对应的代码在哪,需要先找到线程号。用top -H -p PID看线程,或者直接用perf top去采样热点函数:
$ perf top -p 12345 --stdioperf的输出会直接告诉你调用栈的顶层函数,比如lock_wait、sched_yield、某个哈希计算函数,对于定位代码级别的热点非常有用。这一条在实际工作中救过我很多次,特别是你拿到一个陌生的Java进程时,perf能帮你快速确认它到底是真在计算,还是卡在某种锁等待上。
2.3 上下文切换、中断与st值的隐藏信号
有些时候,CPU的us和sy看起来都不高,id也还剩不少,但系统就是“感觉很忙”。这时要去查两个经常被忽略的指标:上下文切换和中断。
上下文切换用vmstat看cs列,或者用pidstat -w按进程来看:
$ vmstat 1 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 8123456 0 4096 0 0 0 0 1234 5678 15 6 77 2 0cs列就是每秒上下文切换次数。如果这个数值持续上万,就要警觉了,特别是一些线程数暴涨的Java应用,或者大量I/O密集的小线程并发场景。上下文切换本身不是罪,但它是系统开销的放大器——每次切换都要保存和恢复寄存器状态,切换越多,CPU的实际有效工作时间越少。
再看中断。vmstat里的in列记录每秒中断次数,其中硬中断可以通过/proc/interrupts查看,软中断通过/proc/softirqs查看。如果你发现某个网卡的中断数异常高,同时sy占比上升,那很可能是网络包量过大,触发了大量的软中断处理。我在一次排查中遇到过类似问题:机器负载不高,但sy一直占20%以上,用cat /proc/softirqs一看,NET_RX这一项突破了千万,最后定位是某个服务在疯狂接收小包,把CPU的内核态时间给吃掉了。
最后说st,也就是steal time。很多朋友用虚拟机、云主机时没注意这一项,结果CPU高的时候全归咎于业务,其实问题出在宿主机上。st占比高意味着宿主机资源超卖,你的CPU时间被其他虚拟机抢走了,这种情况你无论怎么调优业务都没用,只能和云平台沟通迁移或升配。
3. 内存瓶颈的定位与分析
3.1 free命令背后的机制:buff/cache不是“已用”
内存问题要比CPU更容易误判,因为Linux对内存的管理机制和Windows差别很大。很多人一看到free -h输出的used很高就慌了,觉得内存不够了。实际上Linux的内存管理有一个核心原则:闲着也是闲着,不如拿来缓存。所以那占比很大的buff/cache部分是系统用来加速磁盘读写用的,在进程需要时会自动释放。真正能反映“还能再用多少”的指标是available列,它在free的基础上加上了可回收的缓存部分。
$ free -h total used free shared buff/cache available Mem: 31Gi 18Gi 1.2Gi 256Mi 12Gi 12Gi Swap: 2.0Gi 100Mi 1.9Gi当available低于总内存的10%甚至更低时,才需要紧张。真正的内存压力信号不在free里,而在于是否开始使用swap。Linux会在内存紧张时把不常用的内存页换到磁盘上的swap分区,这一操作极其昂贵,因为它会让后续的内存访问变成磁盘IO,性能下降是数量级的。
很多人会直接把swappiness设成0来禁用swap,这个做法我不太推荐。swap在系统设计上是一个安全兜底,直接禁掉可能导致OOM杀进程。更好的办法是把swappiness调到一个较小值,比如10,让系统只在内存极度紧张时才启用swap:
$ sysctl vm.swappiness=10 $ echo "vm.swappiness=10" >> /etc/sysctl.conf3.2 swap和内存回收:vmstat里的si/so到底说明了什么
vmstat的si和so两列,是判断内存是否真正吃紧的金标准。si表示每秒从swap换入内存的数据量,so表示每秒从内存换出到swap的数据量。只要这两个值持续非零,说明内存已经不够用了,系统正在不断做内存页的换入换出,这会直接把磁盘IO拖垮。
$ vmstat 1 10 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 3 1 20480 100000 0 5000 100 250 512 2048 1234 5678 20 15 40 25 0注意看上面这个样本:so=250意味着每秒都有250KB的内存被换到swap上,同时wa=25说明CPU已经有四分之一的时间在等IO。这种状态就是典型的“内存不足引发IO雪崩”——程序需要的数据被换到磁盘,读的时候要从swap读回来,同时又要换走新的内存页,磁盘在没完没了的换页中空转,系统自然慢得离谱。
遇到这种状态,优先考虑给业务进程加内存、减少同时运行的进程数量、或者直接调整编码层的内存使用策略。在排查期间可以用一个快速判断:看看哪个进程的RSS最大,直接用ps aux --sort=-%mem找到前几个大内存占用者,然后回到业务层面问自己一个问题——“它真的需要占这么多吗?”
3.3 内存泄漏、OOM和进程级内存定位
内存泄漏是Java和C++应用最容易出问题的地方,也是最难排查的类型之一。判断是否存在泄漏,方法其实很简单:持续观察某个进程的RSS(物理内存占用)是否只涨不降。用pidstat -r可以实时看:
$ pidstat -r 1 10 Average: UID PID minflt/s majflt/s VSZ RSS %MEM Command Average: 1001 12345 123.50 0.00 4200000 3500000 11.3 java如果RSS在数小时内持续攀升,而且重启进程后又恢复正常,基本可以锁定为内存泄漏。有些同学会问,minflt/s和majflt/s是干嘛的?minflt是“小缺页”,表示进程访问了不在物理内存但在虚拟内存中的页;majflt是“大缺页”,表示要访问的页不在内存里,必须从磁盘读取。majflt/s过高说明进程确实在频繁读磁盘,通常和内存不足或文件映射有关。
当内存被吃光后,Linux的OOM killer会启动,根据一套打分机制选一个进程杀掉来释放内存。判断OOM的方法很直接:
$ dmesg | grep -i oom内核日志会告诉你什么时候触发的OOM、杀掉了哪个进程。但我在实际处分里发现,OOM日志晚了——当你在dmesg里看到信息时,进程已经被杀了,业务已经受损。所以更靠谱的做法是提前监控:留意内存available掉到警戒线、或用/proc/meminfo里的MemAvailable来做阈值告警。还有一个常被忽略的点是内核自身的slab内存占用,用slabtop看一眼,如果某些对象数量异常膨胀,往往和内核模块或容器相关。
4. 磁盘IO瓶颈的定位与分析
4.1 iostat -x:磁盘读写的体检报告
磁盘IO瓶颈的定位,一半的答案都在iostat -x的输出里。这个命令能看到每个块设备最细的读写指标:
$ iostat -x 1 5 Device r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await %util sda 12.00 45.00 128.00 512.00 22.72 3.45 50.23 15.00 60.50 55.60先看%util,它表示设备忙闲程度,100%表示在整个采样周期内设备一直都在干活。但我要提醒你一句,%util=100不一定就是瓶颈。对于底层有RAID阵列或SSD的设备,后台做得很多合并和缓存工作,%util会高于真实压力。反过来,有些单块机械盘%util只有60%,但业务已经卡得不行,因为机械盘的寻道时间占了大头,等盘的时间早就把请求拖跨了。
重点需要看的是await,也就是平均IO响应时间。机械硬盘在负载适中时,await应该在10ms以内;SSD应该在1ms量级。如果await到了几十甚至上百ms,说明磁盘已经撑不住了。还要把读等待r_await和写等待w_await拆开,两种场景的处理方式完全不同。读等待高通常是数据没有命中缓存,比如数据库冷启动、频繁的随机查询;写等待高往往是日志同步、fsync刷盘过多,或者写放大。
avgqu-sz是平均排队长度,如果持续大于存储设备允许的队列深度(机械盘往往在32,SSD在几百),说明IO请求已经排队拥堵,设备处理不过来了。
4.2 IOPS、吞吐、延迟和队列:四个数字怎么关联
排查IO性能问题时,很多人只盯着一两个数字看,很容易漏掉关键信息。其实IO性能有四个互相关联的核心指标:IOPS(每秒IO次数)、吞吐量(每秒传输的字节数)、延迟(单次IO的耗时)、队列长度(有多少IO在等待处理)。这四个数字必须放在一起看,才能还原真实情况。
先说IOPS和吞吐量。IOPS对应的是iostat里的r/s和w/s,吞吐量对应rkB/s和wkB/s。这里存在一个经典误区:如果单次IO的数据量特别大,比如数据库做全表扫描,每秒IO次数看起来不多,但吞吐量已经打满了;如果单次IO数据量很小,比如随机小数据块读写,吞吐量看起来不高,但IOPS可能已经顶满。所以你不能只追求IOPS高或者吞吐量高,要先搞清楚业务到底是随机小IO还是顺序大IO。
延迟是和队列长度连着的。IO请求发到磁盘后,等待时间就是await的一部分。队列越长,每个请求的等待时间就越长,延迟自然一路飙升。判断方式很直观:如果avgqu-sz不大但await很高,说明磁盘本身出问题了,比如坏道、盘片老化;如果avgqu-sz和await同时很高,那就是磁盘被压爆了,需要扩容或优化IO访问模式。
还有一个实用技巧:用blktrace或perf直接跟踪IO请求在内核中的流向,但这属于进阶玩法,一般生产环境用iostat加iotop就够定位大部分问题了。
4.3 按进程定位IO与常见IO瓶颈场景
iostat能告诉你哪块盘慢,但它没告诉你哪个进程在制造这些IO。要找出“IO的罪魁祸首”,得用iotop:
$ iotop -oP TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND 1234 be/4 mysql 0.00 B/s 2.50 M/s 0.00 % 55.00% mysqld-o只显示实际有IO的进程,-P按进程显示。就从这一行你就能看到,mysqld每秒写2.5MB,并且它自己占用了55%的时间在等IO完成。这种情况下你完全不用再猜,直接冲向数据库层去调慢查询、加缓存、或者调整刷盘策略。
常见的IO瓶颈场景有几种,我遇到的频率从高到低排一下。第一是数据库类应用的刷盘压力,mysql和postgres的binlog、redo log每次事务提交都在fsync,磁盘慢的话整体事务能力就被卡死。第二是日志写得多但没合理轮转,应用日志从早写到晚,同一块盘还被业务数据和备份共用。第三是备份和大数据导出任务,凌晨的备份任务往往把整块盘的IO吃光,还连累白天高峰期的业务。
每个场景的对策不太一样。刷盘压力大的,考虑把日志和应用数据分离到不同物理盘、调整fsync策略或使用SSD;日志多的,用logrotate压缩归档,必要时把日志写到独立磁盘;备份任务和业务高峰错峰执行,这是成本最低最容易见的优化。
5. 综合排查与踩坑实录
5.1 三个容易误判的经典场景
排查工作做久了,你会发现大部分时间不是在找瓶颈,而是在排除“伪瓶颈”。下面三个场景是我周围同事包括我自己都踩过好几次的典型坑。
第一个坑是load高、cpu idle高。你一定见过这种机器:top里load average已经到20了,但CPU的idle还有70%,谁看了都觉得不对劲。原因我刚才说过——load包含D状态(不可中断睡眠)进程,大量D状态进程通常是磁盘IO等待导致的。你可以用ps aux列一下,看看STAT列有没有成片的D状态进程,如果有,方向就指向IO而不是CPU。我当时排查一个数据库集群时,就靠这一招定位到是某共享存储的网卡故障引发了IO卡死,进程全部堵在D状态。
第二个坑是内存显示used高就慌,却没意识到可用比fully可用。我记得有个老朋友半夜打电话说服务器内存满了,业务要挂了。我看了一眼free,buff/cache占了30多个G,available还有20多个G,直接告诉他机器稳得很,别动。这就是对Linux内存机制不熟的表现,我前文也强调过,先看available,再看swap利用率,这两个没异常就别慌。
第三个坑是iostat的%util不高但应用卡。这个场景常出现在使用网络存储的环境。本地磁盘的%util不高,但是业务确实在等IO——因为慢的是NFS或SAN的远程存储,本地iostat根本看不到。排查时需要看nfsiostat、看网络延迟,或者直接把业务IO目录挂到本地试一把。这个坑最容易让人怀疑人生,明明“磁盘”没问题,怎么业务就是卡呢。
5.2 快速诊断速查表与“一套组合拳”
实际排障时,拿到一台机器我不会一条一条命令敲,而是按套组合拳来一遍,五分钟内就能对系统状态心里有底。这套组合拳是这样的:
uptime top -bn1 | head -20 vmstat 1 5 free -h iostat -x 1 3 ps aux --sort=-%cpu | head -10 ps aux --sort=-%mem | head -10第一遍看uptime的负载趋势,第二遍看top的CPU各态占比,第三遍看vmstat里的r、b、cs、in、si、so、us、wa,第四遍看内存available和swap,第五遍看磁盘await和%util,最后看CPU和内存占用前10的进程。这一套下来,95%的问题是哪种类型基本能判断出来。
我把判断逻辑整理成一张速查表,方便你对照着看:
| 观察现象 | 可能原因 | 下一步动作 |
|---|---|---|
| us高,id低 | 用户态程序占满CPU | pidstat定位进程,并用perf查热点 |
| sy高,id低 | 系统调用或中断过多 | 查/proc/interrupts、软中断、锁竞争 |
| wa高 | 磁盘IO跟不上 | 用iostat -x定位盘,iotop定位进程 |
| st高 | 宿主机资源超卖 | 联系云平台或迁移 |
| si/so持续非零 | 内存严重不足,swap换页 | 加大内存、降低进程内存占用 |
| available低 | 内存紧张 | 用ps aux --sort=-%mem找大内存进程 |
| await高,avgqu-sz高 | 磁盘队列拥堵 | 优化IO模式、换更快的盘 |
| await高,avgqu-sz低 | 磁盘盘体或链路异常 | 检查磁盘健康状态、RAID状态 |
| load高,id高 | 大量D状态进程等IO | ps aux看STAT列,定位IO瓶颈 |
这套对照表的本质是“现象到方向的映射”。记住一点,不要拿着一个指标死磕,要把多个指标串起来看,形成一个“现象链”,才能尽量避免被单点数据误导。
5.3 建立自己的监控基线
诊断做完、瓶颈解决之后,很多人会直接收工,然后下次问题复现时再从头排查一遍。我的建议是,每一次排障都应该沉淀成监控基线,这是新手到老手最快捷的路径。
所谓基线,就是这台机器在正常状态下的指标范围。比如一个普通的四核16G的Web服务器,正常运行的时候,CPU的id通常在80%以上,load不超过核数,available内存不掉到4G以下,iostat的await基本在5ms以内。这些数字不是凭空设定的,是你连续观察一周以上总结出来的。当你有了基线,再遇到今天这个指标突然飙升的情况,对比起来就一目了然。
具体做法是:把这一次排查中用到的命令和关键输出记录下来,标注当时的业务状态和结论,整理成一份文档或笔记。以后每次看到类似指标,直接翻出来对照,处理速度会快很多。我自己就是这样从“遇到问题翻资料”,慢慢变成“遇到问题翻自己的笔记”的。
另外,有条件的话尽早部署监控。我用过Zabbix、Prometheus+Grafana,也用过纯脚本采集,但核心诉求是一致的——把CPU、内存、磁盘IO、网络的历史数据保存下来,并设置合理告警阈值。有了历史数据,你就不需要“等到问题发生那一刻正好在现场”才能看到那块盘的%util暴涨了,因为曲线会替你记录一切。
我个人在实际操作中最深的体会是:性能排查不是数学题,不是哪个指标超标就一定是哪个瓶颈。三大子系统之间会互相“传染”——内存不足引发swap导致磁盘IO飙升,磁盘IO飙升导致CPU wa升高,wa升高又让应用吞吐量下降,最终表现成全面卡顿。只有把CPU、内存、磁盘IO放在一起看,沿着“现象链”反向追根,才能真正找到问题根源。这套思路我沿用至今,也希望你读完这篇文章后,下次遇到类似问题能少走几条弯路。