
先讲个真实场景。上周有同事半夜发消息说服务器卡成PPT怀疑被入侵了。我让他先别慌登录上去敲了下top第一屏扫过去就发现问题了某个Java进程的%CPU显示成1280%RES吃掉60多G内存旁边的状态列是D配合CPU那行的wa值一看明显是磁盘IO把整台机器拖死的。整个过程五分钟不到根本不用上什么重型监控。这些年排查Linux性能问题top命令始终是我的第一站。它有多老上世纪80年代的产物到今天仍然是每个运维、后端、SRE的肌肉记忆。但说句实话大部分人对top的用法停留在“看一眼哪个进程占用高”然后就开始瞎猜或乱杀进程。它的输出里藏着大量能快速定位问题的信息只要会读很多故障根本不用等监控平台报警。这篇文章不讲那些花里胡哨的替代品就围绕top本身从头部指标、交互操作、线程级排查到OOM实战和脚本化采集把我平时是怎么用它救场的思路完整拆一遍。不管是刚入行的新人还是写过几年脚本的老手我相信多少都能挖到点东西。1. 先分清负载、CPU和内存top头部那几行决定你的排查方向1.1 第一行隐藏着拥堵预警top的顶部第一行不只是一堆无趣的数字它包含系统当前时间、开机时长、登录用户数和load average。很多人一看到load average高就直接去杀进程这是最常见的第一大坑。先看load average: 3.50, 2.80, 2.20这三个数字分别代表过去1分钟、5分钟、15分钟的平均负载。这个“负载”不是CPU使用率而是处在Rrunning运行中和Duninterruptible sleep不可中断睡眠状态的进程平均数量。这区别很重要一个进程如果卡在磁盘IO上也会挂在D状态里但它的CPU占用是0传统意义上的“CPU负载”完全体现不出来。那么load average到底多少算高最粗略的判断标准是拿它和CPU逻辑核数对比。比如一台4核虚拟机load average超过4就意味着可能出现排队如果是一台64核的物理机load average到20可能还很正常。更准确的做法是配合我们后面会讲的CPU状态行来看如果负载高但us、sy都很低wa很高那就不是算力不够而是IO在拖后腿。我自己判断拥堵的习惯是先看15分钟均值是否持续超过核数再对比1分钟和5分钟的趋势。如果1分钟远高于15分钟说明是刚刚一下涌入的任务太多可能歇一会儿就缓过来如果三个数都差不多并且都高说明这是一个持续了至少一刻钟的稳定拥堵状态该认真查了。1.2 Tasks、CPU状态、内存行一秒看穿瓶颈种类top的第二行是Tasks统计会列出进程总数以及running、sleeping、stopped、zombie四个状态的数量。这里我重点看两个值一个是running数量是不是远超CPU核数另一个是zombie有没有在缓慢增长。如果僵尸进程数量一直在涨通常是父进程没有正确回收子进程比如写Python脚本的人忽略了waitpid或者Java的某些框架没处理好线程退出这本身可能不是性能问题但积累多了会占用PID资源最后导致无法创建新进程我遇到过不止一次。第三行是CPU状态这是全篇信息量最大的一行。常见字段包括字段含义观察重点us用户态CPU占比业务计算占大头时这个值高sy内核态CPU占比频繁系统调用、锁竞争时会高ni被调整过nice值的进程CPU占比一般不会太高id空闲比例过低说明计算资源吃紧waIO等待占比高的话优先查磁盘和网络hi硬中断占比硬件设备异常时可能高si软中断占比网络包处理、锁衰减会让它升高st被虚拟机管理程序偷走的时间云服务器上如果持续高考虑邻居争抢举个例子如果us和sy加起来占了90%以上那CPU本身是真不够用要么扩容要么优化代码如果wa居高不下那加CPU核数纯属浪费钱该考虑换SSD、排查慢查询或者优化IO调度。再往下是Mem和Swap行按我习惯的顺序是最后看因为单看Mem里的available不够还得结合进程列表里每个进程的RES来判断具体是谁吃的内存。1.3 进程列表字段每个数字背后藏着什么top默认显示的进程列表列很多最核心的是这几个列名含义我的解读方式PID进程ID配合top -p定位USER启动进程的用户一眼看出是业务进程还是莫名闯入的进程PR / NI内核优先级 / nice值NI为负表示优先级被调高VIRT进程虚拟内存总量不等于真实占用参考意义有限RES常驻物理内存真正占着物理内存的量SHR共享内存多进程共享库会重复显示别直接累加S进程状态R运行 S睡眠 D不可中断 Z僵尸%CPUCPU使用率这是瞬时值不是平均值%MEM物理内存占比相对RES而言TIME累计CPU时间判断一只进程长时间吃CPU很有用COMMAND命令名配合完整路径看这里特别要强调两点。第一VIRT的数值通常比RES大很多不代表它真的占用了那么多内存。比如一个JVM进程的VIRT能轻松超过10G但它真正的常驻物理内存看RES。我看到有人看到VIRT很高就吓得想重启其实没必要。第二%CPU这一列是瞬时值它代表top本次刷新周期内的采样结果。如果刷新间隔是3秒它反映的也就这3秒内的均值所以单看一次很难判断趋势。真正的长期判断要看TIME它显示的是这个进程自启动以来累计消耗的CPU时间。如果TIME五分钟翻了一倍那这个进程是实打实在持续烧CPU而不是什么偶发波动。1.4 别把“负载高”和“CPU高”直接画等号再展开一个很常见的误判场景load average很高Tasks里running也很多但CPU那行的us、sy都很低id反而挺高。这时候如果你按P键给进程排序会发现排前面的进程%CPU也很低看起来哪都不对劲。这种情况八成是磁盘IO或者不可中断的等待造成的。进程可能在等磁盘写回、等NFS响应、等锁释放它没法被调度执行也不能被杀掉于是只能堆在D状态里。而D状态是计入负载的所以负载自然就高了。排查这时候别死盯top配合iostat看磁盘使用率或者dmesg看是不是有硬件报错往往能更早看到真相。2. 交互操作才是top的灵魂几分钟内锁死问题进程2.1 排序切换P、M、T各有各的用途很多人打开top就盯着默认排序看其实是没摸到它的交互快捷键。top运行中直接按键盘字母就能切模式最常用的三个排序键我列在下面。按P是按CPU使用率从高到低排序。CPU问题优先看它瞬间就能把最烧CPU的进程提到第一行。按M是按内存占用从高到低排内存问题看它。按T是按累计CPU时间TIME从高到低排这个我喜欢用来找那种“功耗不高但一直在跑”的常驻进程。比如一个服务看起来%CPU只有0.1%但TIME已经累计了几千分钟说明它从启动起就一直没闲着适合去分析一下是不是有死循环或者过度轮询。还有一个细节按P或M后top顶部那行会显示current sort field提示如果你在脚本里用批处理模式这个排序字段也能通过-o参数指定。比如top -o %MEM -b -n 1就是直接按内存排好输出。2.2 多核展开与线程视图别被进程层的假象骗了现在的服务器基本全是多核top默认只显示一行总的CPU状态想看清每个核的负载是按数字1它会展开成CPU0、CPU1、CPU2……每行一个逻辑核。这个展开对排查“单核打满”问题很有用。举个例子你看到一个进程%CPU显示50%开始以为没什么。但展开多核后发现只有某一个核显示100%其他核都空着。这说明这个进程是单线程密集计算瓶颈在单核性能上光加机器节点对它是没用的得改代码并行或者减少它独享的串行任务。按H可以切换线程视图。切换后进程列表里的PID那一列不再按进程展示而是会展示线程IDCOMMAND列的线程名也可能不同。这个功能在排查Java应用或者Go服务时尤其关键。Java进程本身不会直接告诉你是哪个线程出的问题但切到H视图后按P排序你就能看到具体是哪个线程吃了CPU再通过jstack把线程栈打出来谁在搞事一目了然。我还习惯用top -H -p PID组合只盯某一个进程内部的线程消耗。比如线上有个服务进程总CPU到了800%想确认是不是同一个线程的反复执行导致的就用这个命令观察。2.3 过滤、刷新频率、字段定制让数据只给你看关心的部分top默认把全机器所有用户所有进程都摊在你面前进程一多其实很乱。这时候用u加用户名可以只显示某个用户的进程。我之前排查进程经常忘了它的完整用户名只记得是某业务模块跑的就用u一边敲一边Tab自动补全非常方便。按c可以切换完整命令行和短命令名这帮助很大。默认COMMAND列显示的是java但你根本不知道是哪个服务按c后变成/usr/local/jdk/bin/java -Xms2g -Xmx2g -jar app-demo.jar马上能对应上业务。新版本top里路径很长时会自动截断但可以用方向键横向滚动查看。按f进入字段管理界面这个界面看起来简陋但功能很强你可以选择显示或隐藏某个列甚至更改列的显示顺序。比如不关心SHR的人可以直接把那个字段关掉列表宽度一下就舒服了。改完记得按q退出并保存。按d或者s可以调整刷新间隔默认是3秒。如果正在做短时间内的即时验证比如看某个进程的CPU会不会在几秒内波动可以把间隔调到1秒如果只是在旁路观察调到5秒更省资源。顶部还有个按l、t、m的细节这三个键分别控制load那一行、Tasks和CPU行、Mem和Swap行的显示与隐藏。隐藏行只是为了让屏幕看起来更干净不影响实际采样。2.4 k键和r键不止是看看还能直接干预top不只能看还能发信号。按k它会提示输入PID然后提示输入要发送的信号默认是15也就是SIGTERM。如果你确认一个进程必须要立刻处理可以直接在这里发9杀它而不用再切到另一个终端敲kill。这个功能我在排查失控脚本时会用因为top里已经看到了PID顺手就处理了。按r可以修改一个进程的nice值。通过调高nice值让某个不那么重要的进程让出CPU给其他业务是一个很低成本的临时手段。比如后台跑着一个吃CPU的批处理任务同时线上高峰来了就可以把批处理任务的nice值从0调成10让它继续运行但优先级降低。这里有个现实约束普通用户只能调高自己的nice值调低需要root权限。用交互模式操作最怕的就是手误。k的时候输错PID或者r的时候输错nice值可能直接误杀业务。我自己的习惯是只读排查用top原生的交互模式真到要干预的时候宁可切出去单独确认一下再动手不省那几秒。3. 服务器总是OOMtop找占用最大进程之后还要怎么追3.1 先从dmesg和free还原OOM现场服务器隔三差五OOM每次都是内存耗尽被内核杀进程这种情况只靠top看当下一瞬间意义不大因为OOM发生的那一刻top已经来不及刷新了。我的标准流程是三步dmesg看内核的OOM记录free -h看当前内存分配和可用空间top -o %MEM -b -n 1看现在还在跑的进程谁占用最大。dmesg -T | grep -i out of memory能打出一段类似Out of memory: Killed process 1234 (java)的日志后面还会带上该进程当时的内存占用估算。这是一个非常关键的第一现场内核认为哪个进程该被牺牲以及它当时的物理内存规模。但这里有个坑内核选中的“victim”不一定是内存占用最大的那个它还要考虑进程的oom_score、运行时长、是否是init等一堆因素。所以看到一个无辜的进程被杀不代表它就是吃内存的元凶顶多说明当时的全局内存压力已经到极限了。接着看free -h不能只看free那列重点看available。available是估计的可用内存包括了可回收的页缓存。如果available长期低于几百兆哪怕free显示还有一点也需要警惕。3.2 TOP的%MEM和RES只能帮你找到大方向用top的M排序找到RES最大的进程这只是一条线索。例如你看到某个叫node exporter的进程RES只有几十兆但机器的内存还是被吃干抹净那说明问题不在“某个单进程巨大”而在“很多进程各自吃一点加起来爆炸”或者“页缓存被大量不可丢弃的脏页占住”。这种场景下光在top里翻前几个进程是不够的得把所有进程内存加起来和Mem行对比。我写的简单加法命令大概是top -b -n 1 | awk NR7 {sum $6} END {print sum/1024/1024 GB}这个脚本只能作为粗算因为RES里包含共享内存不同进程如果依赖同一个共享库直接累加会出现重复计算。真要精确分析共享内存还得靠pmap -d PID去看单进程的详细映射。放在这里提供的意义在于让你先有一个“是不是很多进程之和打爆了内存”的快速判断。3.3 top -H找线程问题可能出在线程身上而不是进程很多JVM或Node.js服务是多线程模型你看到进程%CPU很高、内存很大但想进一步定位“这是哪个线程在发疯”就必须用top -H。我习惯的组合是top -H -p 12345 -d 2这个命令把PID为12345的进程内所有线程都展示出来并且每2秒刷新一次。观察哪些线程的%CPU稳定偏高然后再用jstack 12345 thread_dump.txt导出一份线程栈在dump里搜索对应线程ID的十六进制表示就能直接看到这个线程正在执行的代码位置。举个真实例子。某次排查一个网关服务CPU居高不下进程%CPU稳定在300%左右用top -H看到三个线程各占100%随后导线程栈发现三个线程都卡在同一个正则匹配方法上。问题就出在日志脱敏用的正则表达式太复杂面对某些异常长文本会回溯爆炸。整个过程全靠top的线程视图把范围缩小了。多线程还有一个很容易看错的细节。如果进程的%CPU显示500%不要理所当然地以为它把5个核占满了因为这500%可能分摊在好多个线程里每个线程只有不满一个核的用量。对这种场景线程视图才能暴露真正的热点。3.4 从top链路走到真正的内存根因找到吃内存的进程后如果不解决“为什么吃”下次还是会OOM。top只能告诉你表象更深层的问题大概率来自程序本身。拿Java来举例进程RES持续上涨常见原因包括堆内存设置过大、各种本地缓存没有过期策略、直接内存使用不受JVM堆参数管理、String.intern太多等。这种情况下top看到RES高后还得用jmap -heap看堆使用情况用jstat -gcutil看垃圾回收趋势甚至用MAT分析dump文件。top是领路的不是终结答案。另一个容易踩的坑是cgroup内存限制。现在很多容器环境里宿主机内存看着还很充裕但容器内部触发了OOM。你会看到系统日志里报的是cgroup的memory压力而不是整机内存不足。这时候top里看到的可用内存是宿主机的不能代表容器视角。如果业务跑在容器里排查内存不能只看顶部的Mem行还要看cgroup当前使用量和限制。3.5 僵尸进程和线程堆积top里不易察觉的隐性内存杀手除了单个大进程还有个问题也很常见就是进程或线程数量疯狂增长。top默认只能看到列表里有限的几十条但如果你按ShiftF开启一些扩展过滤或者直接用管道看统计会发现一个服务的线程数已经上千甚至几千。每个线程默认栈大小可能是1MB或2MB即便它是虚拟内存也会占用一定的地址空间和内核资源累积多了照样出问题。排查线程数量往往不是top主界面的强项我常用的是top -H -p PID | wc -l或者直接在top里按用户过滤再数行数。更精确的做法是用ps -eLf | grep keyword | wc -l。如果发现线程数目异常增长再去排查代码里有没有频繁创建线程但不释放的逻辑比如使用了没有最大线程数限制的线程池。4. 把top写进脚本告别人工盯屏的批处理用法4.1 批处理模式才是自动化采集的正确姿势top真正的隐藏武器是-b批处理模式。这个模式下没有交互界面不会刷新覆盖而是把输出刷成一帧一帧的文本可以直接重定向到文件。比如top -b -d 5 -n 100 top_capture.log这个命令每5秒采集一次共采集100次持续约500秒。把这些文本留下来再做事后分析能够还原故障发生前后负载的完整变化过程。但用批处理模式有几个细节要注意。第一批处理模式默认依然会输出很多冗余信息每轮采样都会有头部和进程列表文件会很快变大。一般我会加-p PID只盯特定进程或者用-u username只关注某个用户的进程。第二字段分隔符不固定默认进程列表是行列式布局直接用awk按$4、$6取字段很容易因为不同版本的top输出差异而错位。第三批处理模式下默认也是按CPU排序如果想按内存排序就加-o %MEM。我实际经常跑的命令是把top和其他指标配合起来top -b -o %MEM -d 5 -n 120 | gzip top_oom_10min.log.gz这样即使过了很久再翻出来也能通过zcat解压排查。4.2 从采样数据里看趋势瞬时百分比救不了你再说一遍%CPU是瞬时值在批处理模式下它也就是那一个采样周期内的快照。你采集了100个点如果只想看峰值可以用grep java top_capture.log | awk把%CPU那一列拉出来统计最大、最小、平均值。但我其实更建议直接记录TIME因为它是累计值前后两个采样点差一下就能算出这段时间内的真实CPU消耗增量。例如第5秒采样时某个进程的TIME是100秒第15秒再采样时变成130秒那在这10秒窗口里这个进程消耗了30秒的CPU时间。换算成百分比就是300%平均占用。这种算法比你直接看采样点上的%CPU稳定多了尤其适合判断一个进程是不是真的长期吃CPU。简便的差减法可以这么写top -b -n 2 -d 10 -p 12345 | grep 12345 sample.txt然后用前后两行TIME列做差。这个方法不需要任何监控系统遇到紧急情况非常实用。4.3 top之外的组合拳pidstat、mpstat、sar不能缺席top负责给出第一印象但生产环境定位问题很少只靠top一个命令就收工。我自己常用的组合是pidstat 1 10按固定间隔输出每个进程的CPU、内存、线程上下文切换详情字段非常规整脚本解析比top友好得多。mpstat -P ALL 1看清楚每个核的使用情况和中断分布。sar -r 1看内存页的换入换出、内存提交速率。vmstat 1 10看运行队列和IO块写入读出的变化。iostat -x 1确认磁盘的%util和await是不是压在瓶颈上。有人会觉得只要top就够了但top的短板很明显它没有历史趋势能力没有跨节点对比能力采样周期自己不可控。它适合“此刻发生了什么”不适合“过去几小时发生了什么”。后者请交给监控平台或者定时把top输出存到日志里。4.4 三个我踩过的top脚本坑第一个坑是不同发行版的top版本输出不一样。procps-ng版本和老的sysstat版本字段排列有差异在CentOS 6上写好的awk脚本到Ubuntu 20.04上可能字段全错。最好在解析前先人工看一帧输出确认列位置。第二个坑是批处理模式下如果指定了-p参数进程退出后top可能直接不输出整个脚本空转。我遇到过用top监控一个频繁重启的服务pid一换后半程什么都没抓到只好改用pgrep动态更新PID再重新起top。第三个坑是top -b -n 1和交互看到的CPU占比可能不一样。因为批处理模式下第一轮的CPU百分比经常是自启动以来到现在时间范围内的平均值而不是最近一次间隔的值因此分析时建议丢弃第一帧或先跑几秒再正式开始采样。5. 顺便聊聊所谓的“窗口置顶”top命令和桌面置顶不是一回事搜索“top命令”的朋友里总有一部分其实是想找Ubuntu窗口标题栏右键菜单里的“Always on Top”是怎么实现的。这个置顶和终端里的进程排行不是同一个东西但在日常排查里又经常被放在一起讨论我借最后这段把机制说清楚免得大家搜错方向。桌面Linux里的“窗口置顶”本质是窗口管理器层面的事。现在的GNOME用的是MutterKDE用的是KWin它们各自维护窗口的Z轴顺序。当你在标题栏右键选择“Always on Top”窗口管理器会给这个窗口设置一个特殊属性也就是X11下的_NET_WM_STATE_ABOVE。状态置上以后窗口管理器在调度绘制时就把这个窗口放到Z轴更上层之后无论其他窗口怎么穿插它都保持在最前面。Wayland下机制略有不同但逻辑也很相似通过窗口协议单独设置层级。这个“置顶”和top命令完全是两个实现路径但思路有共同点都是把“重要的东西”固定在最显眼的位置。我在服务器出问题的时候也会用tmux把top摆在固定窗格里旁边放两个日志窗口一旦有异常视线始终不需要到处跳。配合tmux的set-window-option把某个窗格的输出固定住不让日志翻滚把它顶走简单又高效。顺便分享一个我的使用习惯在任何一个故障工单开着的时候我都会让top常驻一个桌面并且把刷新频率调到3秒。不是为了装样子而是因为很多性能问题在监控系统里是一张折线图但在top里是一连串实时的进程快照。折线图告诉你“当时很严重”top快照告诉你“当时具体是谁在作恶”。这两种信息互补谁也别想替代谁。如果你刚接触这些也不用把每个字段都背下来先记住一件事遇到服务器不对敲top后先看头部三行再按P或者M排序再按H看线程。这三步走完80%的性能问题已经能圈定方向了剩下的是去代码日志里找那句为什么。