前阵子有个朋友去面字节,回来跟我吐槽,说面试官问了他一个问题:“假如某一天你的系统突然变慢了,变卡了,你会怎么办?”他说自己当时脑子里过了一遍top、free、df这些命令,但回答得没什么章法,面完出来才觉得这题其实考的不是命令背得熟不熟,而是面对一个模糊的“系统变慢了”的反馈,你到底有没有一套清晰的排查思路。
这个问题我太有感触了。做了这么多年后端和运维,系统变慢这种故障,基本是每个技术人都会遇到的噩梦。它难就难在“慢”这个字太笼统了——是用户觉得慢,还是监控发现慢?是新上的功能慢,还是全站都慢?是某个接口慢,还是数据库慢?如果你上来就一头扎进服务器敲命令,大概率会像无头苍蝇一样乱转,最后既浪费了时间,又挨了业务方的骂。
这篇文章我就结合自己的实际经验,把一套经过实战检验的排查思路完整地梳理一遍。不管你是刚入行的运维新人,还是写了几年业务代码的研发,这套方法论都适用。我会把从接到反馈、到定位问题、再到恢复和复盘的全过程拆开揉碎了讲,重点讲清楚每一步为什么要这么做,以及常见的坑在哪里。
先说一个总的指导思想:面对系统变慢,第一原则永远是——先恢复,再排查;先确认影响范围,再深入定位根因。千万不要一上来就纠结于“为什么”,而是要先用最快的方式把损失降到最低。后面所有的步骤,都是在这个原则下展开的。
1. 接到反馈后的第一步:别急着上服务器,先弄清楚“慢”到底指什么
很多人接到“系统变卡了”的消息,第一反应就是登录服务器,跑个top看看CPU是不是爆了。这个动作本身没错,但顺序错了。因为你连问题是什么都不知道,看top也看不出个所以然来。正确的做法是,先花两分钟把信息收集齐,搞清楚你面对的到底是一个什么样的问题。
1.1 先做“问题画像”:谁在说慢、哪里慢、从什么时候开始慢
我在实际处理故障时,通常会像一个医生问诊一样,先问自己几个固定问题:
- 反馈的人是谁?是外部用户打电话投诉,还是运营同学在后台发现数据刷不出来,还是测试环境联调时感觉卡?这直接决定了问题的优先级和影响范围。
- 所谓的“慢”是哪种慢?是打开网页要转圈半天,是个别操作点按钮没反应,还是接口返回数据要等几十秒?这决定了排查的方向。
- 是所有功能都慢,还是只有某个页面、某个接口慢?如果是局部的,那问题大概率出在那个功能对应的代码、数据库或者第三方依赖上;如果是全站的,那就要优先看基础设施和公共组件了。
- 是从什么时候开始变慢的?是刚刚才发生,还是从上一次发版之后就开始的?如果和发版时间点吻合,那大概率就是新代码引入的问题,回滚往往是最快的解决方案。
这几个问题问完,你基本就能把事情从一团乱麻中抽离出来了。这里我特别想强调一点:尽量去拿到“从什么时候开始”这个信息。我在排查故障时,最依赖的抓手就是时间线。只要能把“变慢”和某个时间点关联起来,你的排查范围就能缩小一大半,因为接下来的思路就是直接按时间点去反推那个时间节点前后系统发生了什么变更——是不是发版了、是不是有定时任务在跑、是不是数据库在做备份、是不是有运营同学在批量导数据。
1.2 确认影响面:这是“全站故障”还是“单点故障”
在你准备动手之前,还有一件必须要做的事:确认影响范围。这个步骤看起来很简单,但关键时刻能救命。
我的经验是,先看一眼监控大盘,或者随口问一句同事“你们那边卡不卡”。如果只是一个人反馈慢,可能只是他的网络问题、他的本地缓存问题,甚至是他自己电脑的问题;如果是整个业务线都在反馈慢,那就是全站级别的事故,需要立刻拉群、启动紧急响应流程了。
另外,还要区分是线上正式环境,还是测试环境。如果只是测试环境慢,那很多排查手段可以稍微放开一些,但在生产环境,你的每一步操作都要非常谨慎,因为线上系统经不起折腾。比如,线上环境jstack抓线程栈是相对安全的,但如果你贸然重启应用,那就可能把出现过问题的现场给破坏掉了,这个后面会详细说。
1.3 操作禁忌:千万不要一上来就重启
这一点我必须放在最前面讲,因为这是我见过太多人犯过的错误,包括我自己刚入行时也犯过。很多研发同学一遇到系统变慢,第一反应就是“重启一下试试”。如果是在确认是死锁、内存溢出、线程池耗尽这些问题时,重启确实是最快的止损手段,但如果原因还没定位,你一重启,所有的现场就全没了——线程栈没了、堆内存快照没了、网络连接状态没了、临时文件也没了。
所以,我的铁律是:重启是最后的兜底手段,而不是第一排查手段。除非系统已经濒临不可用,比如页面完全打不开、数据库连接池耗尽导致上游系统连环雪崩,否则一定要先把现场的“证据”保留下来,再做处置。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统资源的快速体检:CPU、内存、磁盘、网络,从这四个维度逐一排查
当你接完信息,确认这是一个真问题,而且影响范围不小之后,就可以登录服务器了。这时候,你的核心任务是快速给系统做一次全身检查,摸清楚四个核心资源的情况:CPU、内存、磁盘和网络。我习惯按照这个顺序来,因为它的排查效率最高。
2.1 CPU排查:关注负载、使用率、上下文切换和运行队列
登录服务器后,我第一个敲下的命令几乎永远是 top。但注意,top 的输出信息量很大,你别光看那个 %CPU 是不是100%,那是典型的只看表面不看本质。作为一个有经验的排查者,你应该重点看另外几个指标:
第一是 load average,也就是系统平均负载。我见过很多新手对这个指标的理解有偏差,负载高不一定代表CPU忙——它其实是一个“运行队列长度”的概念,包含了正在运行的和等待CPU、以及不可中断的进程数。如果你的服务器是4核的,load average 长期超过4,就意味着任务堆积了,一定是有瓶颈的。但如果 load average 高,%CPU 占用却不高,那就要考虑是不是有大量的进程在等待I/O,比如磁盘读写缓慢,这也是一种典型的“系统慢但CPU不忙”的情况。
第二是要看进程CPU有哪些异常高的。top 进入交互界面后,按 P 键可以直接按CPU占用排序,这样你能一眼看到是哪个进程在疯狂消耗CPU。定位到进程之后,如果是Java应用,你需要用 top -H -p 进程号 去查看它内部线程的CPU占用,这一步后面讲Java排查时还会再展开。
第三是上下文切换。你可以用 vmstat 1 3 这样的命令,每隔一秒输出一次,连续看三次。重点关注 cs(context switch)那一列,如果数值异常高,比如每秒几十万次,那很可能说明系统里有大量线程在竞争锁或者频繁切换。上下文切换本身不是坏事,但如果数值过高,意味着CPU的大量时间都耗在切换线程上,真正执行任务的算力反而被挤占了。
2.2 内存排查:重点看swap使用率和实际可用内存
看CPU的同时,我也会顺手按一下 top 的 M 键,按内存排序看看进程内存占用。但在排查系统变慢时,比看进程占用内存更重要的,是看系统的 swap 使用情况。为什么?因为 swap 是磁盘上一块用来临时存放内存数据的区域,如果系统物理内存不够用,就会把一部分内存数据换到磁盘上。而磁盘的读写速度,和内存相比是几个数量级的差距,一旦系统开始频繁使用swap,你的应用性能就会断崖式下跌。
我常用的命令是 free -h,直接看 Swap 那一行。如果 Si 和 So(swap in / swap out)有大量数值持续变动,说明内存不够了,系统一直在内存和磁盘之间搬运数据,那系统慢就是必然的。这种场景下,解决方法无非是两点:一是短期内清理一些无用的缓存进程,或者干掉那些吃内存的大户;二是长期上考虑给服务器扩容内存,或者对应用的内存使用进行优化,比如调整JVM参数。
2.3 磁盘排查:I/O等待和inode耗尽,容易被忽略的隐形杀手
磁盘问题经常被忽略,因为它看不见摸不着,但它在“系统变慢”里出现的概率极高。我用 top 看到 %wa(I/O wait)那一列如果很高,或者用 iostat -x 1 看到 %util 接近100%,就能初步判断磁盘是瓶颈了。
但磁盘问题也要分情况来看。如果是机械硬盘,可能是随机读写能力达到上限;如果是云盘,可能是吞吐量突发被限流;还有一种很隐蔽的情况是inode耗尽——即磁盘空间还有,但是文件数量已经达到上限了,导致系统无法创建新文件。这种问题用 df -i 才能查出来,光看 df -h 是看不到的。我印象很深的一次故障,就是日志文件被疯狂打印,小文件数量爆炸,直接把inode吃光了,应用看起来就是各种“No space left on device”的报错,但实际上磁盘空间还很富余。
处理磁盘I/O高的思路是:先用 iotop 或 lsof 找到是哪个进程在疯狂读写,找到后观察它的行为。如果是一个正常的业务进程因为代码bug在做全表扫描,那你就要去优化SQL了;如果是日志进程在写超大日志文件,那你就要考虑日志切分和清理了。
2.4 网络排查:带宽打满、延迟升高和连接数超限
这四个维度里,网络往往是最后一个被排查的,但这个顺序其实有些本末倒置,因为网络导致的问题同样很多。检查网络,我会分三个层面去看:
一是带宽层面。可以用 sar -n DEV 1 3 或者直接看云监控,查看出入口流量是否打满了带宽。如果带宽流量满是100Mbps,那么无论你的应用优化得多好,用户的请求也进不来,体验自然就是卡顿。
二是连接层面。用 netstat -nat | grep :80 | wc -l 查看连接数是否超过预期,用 ss -s 查看整体的连接状态。如果出现大量的 TIME_WAIT 或 CLOSE_WAIT,可能说明代码里连接没有正确关闭,导致连接泄漏;如果是 SYN_SENT 堆积,可能说明是有一个下游服务连不上了。
三是延迟层面。可以用 ping 去测网络延迟是否正常,用 curl -w 去测接口响应时间。如果网络延迟本身很高,那你就得去排查是不是链路问题,比如是不是跨地域了、是不是公网波动了。
3. 深入应用层:Java应用线程与日志分析的实战技巧
如果说上面的系统资源排查是找到“哪个部件出了问题”,那么应用层排查就是为了找到“哪一行代码出了问题”。很多系统变慢的场景,其实系统资源看起来都没什么大问题——CPU不高、内存够用、磁盘不忙——但业务就是卡。这时候,你就必须深入到应用内部,尤其是如果你面对的是一个Java应用,有一些排查手段几乎是必须掌握的。
3.1 用线程栈抓到“卡住”的现场:jstack的正确使用姿势
处理Java应用变慢,最核心的一个排查动作就是抓线程栈。线程栈就像是系统在某一个瞬间的“快照”,它能清清楚楚地告诉你,JVM里的每个线程此刻正在执行什么代码,以及卡在哪一行上。
我的标准流程是:
第一步,用 top -H -p 进程号 找到CPU占用异常高的那个线程ID,记下来。这里的线程ID是十进制的,但在jstack的输出里,线程ID通常显示为十六进制,所以你需要先把十进制转成十六进制,用 printf "%x\n" 线程ID 就能搞定。
第二步,执行 jstack 进程号 > thread_dump.txt,把线程栈输出到文件里。然后在这个文件里搜索刚才转成十六进制的线程ID,你就能定位到那一个热点线程在做什么,它是否死循环、是否在等待一把锁、是否卡在某个网络调用上。
第三步,也是最容易被忽略的——线程栈不能只抓一次,至少要抓3次,每次间隔几秒。为什么?因为如果只抓一次,你可能看到一个线程正好在等待I/O,误以为它是瓶颈;连续抓几次,你就能看出哪些线程是持续存在的热点,哪些只是瞬时状态,这样判断才更准确。
在分析线程栈时,你还会经常遇到几种典型的状态:一个是线程卡在 BLOCKED 状态等待锁,另一个是大量线程处于 WAITING 状态调用了 park 方法,这说明线程池已经被占满了,新的任务全部在排队等待。这两种情况都是典型的性能瓶颈点,顺着线程栈往下追,通常都能找到问题源头。
3.2 内存与GC日志:Full GC频繁是系统卡顿的重要元凶
如果线程栈没看出明显问题,别急,还有一种高频原因——JVM内存分配和垃圾回收出了问题。举个最典型的场景:老年代空间不足,JVM就会频繁触发Full GC,而在Full GC期间,"Stop The World"会让所有工作线程全部暂停,这个暂停时间可能长达几秒,表现到用户侧就是明显的卡顿和延迟。
怎么排查?第一,看看GC日志。如果你在JVM参数里配置了 -Xloggc:/path/to/gc.log,那就直接去这个文件里看GC频率和停顿时间。如果没有开启GC日志,那就用 jstat -gcutil 进程号 1000 实时查看堆内存各区间的使用率和GC次数,重点看 FGC(Full GC次数)和 FGCT(Full GC累计耗时)这两个值。如果在几秒内 FGC 快速上涨,基本可以确认就是GC问题。
第二,确认GC问题后,如果条件允许,用 jmap -dump:format=b,file=heap.hprof 进程号 抓一份堆内存快照,然后用MAT或JProfiler分析,看是什么对象占满了堆内存。最常见的两种原因:一种是内存泄漏,对象只增不减;另一种是加载了过多的类,比如因为代码不规范,导致某些大对象被反复加载到老年代。这里要特别提醒,jmap -dump 在堆内存很大的环境可能造成较长时间的暂停,生产环境使用要格外谨慎。
3.3 SQL慢查询与数据库连接池:后端性能的另一半战场
很多时候,应用层看着挺健康,但系统就是慢,这时候你很可以把目光往下探一层,看看数据库和连接池。我曾经处理过的一个典型的“系统变慢”案例,最后定位到的根因就是一条SQL没走索引,在数据量增长到千万级之后,单次查询耗时从几十毫秒飙升到了好几秒。
排查数据库问题的标准动作是:打开慢查询日志。MySQL里你可以通过 set global slow_query_log=ON 动态开启,也可以直接查 information_schema 里的进程列表,看看当前有没有长时间执行的SQL。定位到慢SQL之后,使用 EXPLAIN 分析它的执行计划,看是否命中了索引、扫描了多少行、有没有临时表和文件排序,这些都能帮你判断为什么这条SQL慢。
除了SQL本身,连接池耗尽也是一个高频坑。Java应用里常用的连接池有HikariCP、Druid等,你可以通过监控查看当前活跃连接数和空闲连接数。如果所有连接都被占满,新请求就会排队等待获取连接,表现就是接口RT急剧上升。这种情况的根因往往是某个慢SQL占住了连接,导致连接无法及时释放,于是下游的请求越积越多,像滚雪球一样直到整个连接池瘫痪。
4. 从偶发到持续:一次真实故障的完整复盘与排查记录
接下来,我分享一个实际发生的故障案例。通过这个案例,你能更好地看到前面讲到的那些排查手段是如何在真实场景中串联起来的。
4.1 现象与初步判断:从监控告警到第一次误判
那次故障发生在一次大促之前。业务方反馈,后台导出功能特别慢,以前几秒钟就能生成的报表,现在要等几分钟,有时候甚至超时。同时,监控系统开始告警,线上某个核心服务的平均RT从80毫秒涨到了3秒,错误率也在缓慢上升。
收到反馈后,我按前面说的思路,先做画像:影响范围有限,只是后台导出相关接口慢,不是全站变卡。这个信息很重要,它直接告诉我不需要去排查那些公共的基础设施,而可以把焦点锁在这个导出功能上。
登录服务器后,我首先发现系统CPU使用率其实不高,top输出显示CPU空闲率还有80%以上,这意味着“CPU跑满”这个猜测很快被排除了。紧接着看内存,发现 free 显示物理内存还有剩余,也没有使用swap。然后我下意识看了下磁盘I/O,结果 iostat 显示 %util 在90%以上,整个磁盘的I/O压力非常大——那一刻我几乎确信,问题的根源就是磁盘。
但接下来的排查,让我意识到自己还是草率了。我用 iotop 找到了磁盘I/O的主要来源,结果发现并不是应用进程,而是一个系统级的日志清理任务,正在被周期性地执行。
4.2 顺藤摸瓜:追踪I/O背后的元凶,挖出应用与磁盘的关联
为什么日志清理任务会把I/O打满?带着这个疑问,我继续深挖。经过查看,原来这个清理任务是临时配上去的,本来是为了给磁盘腾出空间,结果正好和大促期间的导出任务撞到了一起。日志清理任务会全量扫描并删除大量旧日志文件,整个扫描和删除的过程产生了大量的随机读写,直接把磁盘I/O拖垮了。
但故事到这里还没完。磁盘I/O高,只是一个中间状态。我进一步发现,导出功能本身也有问题——它在生成报表时,使用了大量的排序操作,而排序的临时文件恰恰被分配到了磁盘上。也就是说,导出功能本身就是磁盘I/O的消耗大户,在平时数据量小的时候无关痛痒,可一旦日志清理任务介入,双方的I/O请求叠加,磁盘瞬间就成了瓶颈。
到这里,整个链条就通了:导出功能产生大量临时文件排序 I/O + 日志清理任务全量扫描磁盘 I/O = 磁盘I/O瓶颈 = 导出变慢 = 接口RT飙升 = 用户感知系统变卡。
4.3 修复与优化:短期止损、中期调整、长期规避
定位到根因之后,处理起来就快多了。我当时的处置分三步:
短期止损:立即暂停那个日志清理任务,优先恢复系统正常的I/O水平。同时,快速重启了导出服务,清掉当前积压的排队任务,接口RT很快就降下来了。
中期调整:给日志清理任务加了一个更温和的调度策略,让它避开业务高峰期、错峰执行,并且加入I/O限流,避免一次性全量扫描对磁盘造成的冲击。
长期规避:优化导出功能的排序逻辑,把部分排序操作从磁盘临时文件改成内存中的有序结构,或者在SQL层直接通过索引排序,减少创建临时文件的数量。同时,补上了磁盘I/O水位的监控告警,确保以后在磁盘I/O异常升高时能第一时间收到通知。
这个案例让我特别感慨,很多系统变慢都不是单一因素引发的,而是多个因素在同一个时间窗口叠加的结果。所以排查时一定要搞清楚因果链,不要满足于找到第一个异常指标就草草收场。
5. 常见问题与排查技巧速查:把走过的坑变成你的捷径
最后,我把自己这些年排查系统变慢的经验,整理成一份速查表,方便你下次遇到类似问题时直接对照参考。毕竟故障排查这种事,很多时候拼的就是手速和套路熟悉度。
5.1 症状与排查方向的快速对照表
| 现象特征 | 重点排查方向 | 常用命令/工具 | 可能根因 |
|---|---|---|---|
| 全站接口RT升高,CPU和内存无明显异常 | 数据库慢SQL、连接池耗尽 | 慢查询日志、jstack、HikariCP监控 |
缺索引、锁等待、连接泄漏 |
| CPU使用率接近100%,load很高 | 应用线程CPU占用、GC | top -H、jstack、jstat |
死循环、热点方法、频繁Full GC |
| 系统load高,但CPU占用并不高 | 磁盘I/O、不可中断进程 | iostat、vmstat、iotop |
磁盘饱和、NFS挂载卡死、日志刷盘 |
| 内存有剩余,但系统变慢 | swap使用情况 | free -h、vmstat |
内存碎片、缓存管理策略、JVM堆配置不合理 |
| 页面操作卡顿,但接口响应尚可 | 前端资源加载、CDN、静态资源 | 浏览器DevTools、curl -w |
CDN回源慢、前端JS阻塞、网络带宽不足 |
| 偶发性变慢,无固定规律 | 定时任务、GC日志、依赖故障 | 定时任务平台、GC日志、APM链路追踪 | 大促任务、凌晨全量任务、第三方依赖超时 |
这张表不是说你一定要按行机械地去找,而是给了你一个相对成熟的、被验证过的对照思路,能帮你少走弯路。
5.2 几条独家经验:讲给面试官听的加分项
除了上面这些硬核操作,每个老手都会有一些属于自己的小经验和好习惯。这里分享几条我认为价值最高的。
第一,“慢”是一种需要量化描述的状态,而不能只是定性描述。排查前一定要先拿到基线数值——现在多少RT,平时多少RT,现在多少TPS,平时多少TPS。没有基线,你很难判断问题到底有多严重,这也是面试官考察你是否有数据意识的一种方式。
第二,所有的排查动作都要有记录。这一点在紧急故障时尤其重要。很多人一紧张就乱了阵脚,想到哪查到哪,最后问题解决了,却复盘不出真正的原因。我习惯在排查过程中打开一个记事本,或干脆就用一个支持Markdown的文档,每执行一个命令、得到一个关键结果,就记一笔。故障处理完,时间线梳理一下,“为什么”、“怎么样”都一目了然。这个习惯在面试中提一句,是非常加分的,因为它证明了你不是只会敲命令,而是有完整的方法论。
第三,监控和告警要主动设置,不要等用户报障才发现问题。比如我会给核心接口设置RT和错误率告警,给磁盘I/O设置水位线告警,给频繁GC设置告警。大多数系统变慢其实不是毫无征兆的,而是被一个不合理的告警策略给漏掉了。事后补救,不如事先防御。
第四,如果你是面试者,回答这个问题时要展现出“结构化思维”而不是“背命令”。面试官想听到的是:你先确认影响范围、你先看监控、你先用top看负载、再决定是否深入线程栈和SQL。命令只是工具,能不能讲清楚“为什么是这个顺序”才是真正的分水岭。
写在最后
系统突然变慢这类问题,最考验人的不是技术深度,而是面对压力时的冷静程度和排查节奏的把控能力。我的个人体会是,排查系统卡顿,百分之八十靠的是规范化的流程和平时积累的“手感”,而不是什么高深莫测的黑科技。
在自己没经历过几次真实的线上故障之前,光看文档可能很难建立起这种“手感”,所以我一直建议团队里的新人:一是多看看线上监控大盘,了解自己系统的各项指标平时长什么样;二是多参与故障复盘,跟着老同事走一遍完整的排查链路;三是自己可以拿一台测试服务器,人为制造一些故障场景,比如用工具把CPU压满、把磁盘占满、把网络延迟拉高,再自己反复演练排查方案。只有平时练得多,关键时刻才能不慌不忙,按部就班地把问题逐步逼近到根因,最后干净利落地解决掉。
