
周二下午三点出头群里开始有人反馈上传图片一直转圈页面要十几秒才打得开。刚开始我没太当回事以为是哪个实例又到了高峰期顺手看了一眼监控面板——CPU 90%以上负载一路爬到了20以上。这时候我才意识到事情不是“偶发抖动”这么简单。更麻烦的是拉出进程列表看没有任何一个进程吃掉大量CPU每个Java进程都只占百分之十几可系统就是卡得不行。这其实是很典型的Linux系统排障场景服务变慢、业务受损、但第一眼看不到元凶。后来我花了整整两个小时绕了好大一圈才把问题定位到一个被忽视的定时任务上。整个过程里踩了不少判断上的误区也让我把这些年处理线上服务器问题的一些方法重新梳理了一遍。这篇就当作一次完整复盘把我当时怎么查的、为什么走弯路、最后怎么锁定真凶一条条写清楚。如果你也经常跟Linux服务器打交道或者负责维护线上业务这篇里的思路和命令组合应该能帮你省下不少时间。1. 故障现场表象是怎样一步步升级的1.1 告警与第一波误判那天的第一波告警是监控平台自动发出来的通知写得很简单CPU使用率连续5分钟超过90%load average超过核数的4倍业务接口P99延迟从80ms飙升到12秒。消息一响常规操作先做了一遍登录跳板机连上服务器敲uptime、free -h、df -h看是不是资源不够用了。结果很有意思——内存还有大量空闲磁盘也没满但uptime显示load average已经在20多。这就说明系统确实在超负荷运转可内存和磁盘都不是瓶颈。于是第一反应是“是不是线上Java服务出了什么幺蛾子”比如Full GC频繁、连接池被打满、某个热点接口出现死循环之类的。我按这个方向查了很久jstat看了GC情况、jstack看了线程栈、arthas也试过但Java进程内部其实挺健康的。这时候才慢慢意识到问题可能不在应用层而在系统层。这里有个很重要的经验线上服务变慢第一步要分清楚是“某个业务进程自己出了问题”还是“整台机器环境出了问题”。判断标准很简单——如果只慢一个服务那大概率是应用层的问题如果整台机器上所有服务都慢包括ssh登录都迟钝、命令敲下去要停顿那基本可以断定是系统层级的资源竞争。当时我连敲命令都偶尔卡一下这已经是一个很强的信号了。1.2 从“服务慢”到“系统慢”的排查转向当你意识到是系统层问题之后排查方向就变了。最先要弄清楚的是load average里那20多的负载到底是CPU计算任务太多还是进程在等待I/O还是说进程陷入了某种不可中断的内核态操作。Linux的load average统计的是处于R状态运行中或等待CPU和D状态不可中断睡眠通常是等待磁盘I/O等的进程数量之和。所以负载高不代表CPU忙也可能是一堆进程都在排队等磁盘、等网络、或者卡在内核驱动里。当时我下一步执行了top按CPU排序、按内存排序都试过结果很迷惑排在最前面的进程CPU占用也只有20%左右整机CPU使用率却超过90%。这种“每个进程都不高但系统总CPU很高”的现象通常说明CPU时间被分散到了大量进程/线程上典型的场景是某个任务拉起了大量子进程或线程每个子进程/线程分到一点CPU时间片合起来就把整机资源吃完了。但top默认只显示进程维度的汇总如果几百个短生命周期进程反复出现你在top里根本盯不住它们。于是我从“看进程”切到了“看行为”——不再执着于找到一个高CPU进程而是去找系统里到底发生了什么频繁且大量的操作。方向上我把注意力转向了I/O等待和系统调用层面。2. 常规排查三件套为何没有一击命中2.1 top/htop的视角盲区CPU不高但负载高先说top。很多刚接触Linux排查的人拿到top就看一眼%CPU这一列觉得CPU占用高的就是元凶。但在负载高、CPU高、却又找不到明显高占用进程的时候就需要看下面的Tasks区域尤其是僵尸进程数量和运行状态分布——zombie越多说明可能有进程管理混乱大量进程处于D状态说明I/O在排队。我当时还犯了另一个错误top默认是刷新一次的快照而定时任务这类短时突发的行为可能在你两次刷新之间就启动又结束了。所以后来我习惯性地关掉了top的“单次快照”模式改用top -d 1让它每秒刷新并且反复盯了十几轮。观察发现总会出现一些名字看起来跟业务无关的进程比如tar、mysqldump、python3 xxx.py跑几秒就消失。但当时我没有立刻把它们和系统变慢联系起来因为它们的生命周期太短每次看到的CPU占用都不高。htop比top好看一些能显示树状进程关系还能用F键筛选。但本质上它依然是快照式工具对瞬时进程的捕捉能力有限。真正想让这种“一闪而过”的进程现形要么靠高频采集的监控数据把时间线拉出来要么就得用atop这种带历史回放能力的工具。atop会在后台持续记录系统资源使用情况事后能按时间点回放某一时刻谁在跑、跑的时候吃了多少CPU、多少I/O。那次我要是在一开始就用atop估计半小时内就能定位而不是绕两小时。2.2 内存与swapfree能看到什么、看不到什么内存这块也给我造成了一点干扰。free -h显示可用内存有30多Gswap基本没动看起来非常健康。但服务器慢有时候跟“可用内存大小”没关系跟“内存回收路径”有关——当某个进程突然申请大量内存触发了内存回收机制系统会花大量CPU时间去扫描和回收page cache这个过程中load会飙高而free的统计看起来依旧“正常”。当时我看到内存充裕就把内存因素排除了。但后来反思其实应该执行vmstat 1多看几组数据重点看si和so是否在持续交换、cs上下文切换是否高得不正常。那次之后我遇到了不止一次“free正常但机器卡顿”的情况套路基本都是某个程序频繁分配/释放内存、触发page cache回收、导致上下文切换飙到几十万、CPU耗在内核态上。这些在free一眼看不出必须动态观察才能发现。2.3 磁盘I/O与文件描述符容易被忽略的配角第三个容易忽略的点是磁盘I/O和文件描述符。我查了df -h根分区还剩40%多以为磁盘没问题。但分区剩余空间和磁盘I/O繁忙程度是两个概念。一个磁盘可能空间很足但每秒读写次数已经接近硬件上限尤其当大量小文件同时被读写时I/O等待会急剧升高。我当时运行iostat -x 1看了几轮%util确实到了一百左右await延时很高说明磁盘确实在承受压力。可是什么操作在读写磁盘当时还没有线索。这种场景下lsof是很有用的可以通过lsof D /目录路径查看某个目录正在被哪些进程打开也可以直接用lsof | grep deleted找那些“已经删除但仍被占用”的文件。我后来查到有大量被打开的日志文件说明有程序在疯狂写日志这成了重要的线索之一。文件描述符方面可以看看cat /proc/sys/fs/file-nr当前使用的句柄数是否接近上限。如果某个脚本把句柄耗尽业务进程会出现“too many open files”异常这种情况我也会在排查时顺手排除掉。那次虽然没有触发句柄问题但这些“配角”因素一个个排除掉也不冤枉。3. 两个小时里走通的定位路径从进程到定时任务3.1 看进程树和父子关系锁定可疑血缘常规工具没有一击命中我只能上更“细”的手段。第一步是用ps -ef --forest看进程树。为什么要看进程树因为很多问题的启动源都能从血缘关系里找出来——一个Python脚本是谁拉起来的一个tar命令的父进程是谁能直接反映它属于哪个应用或哪个定时任务。当时我在进程树里翻到一个很不起眼的sh -c进程它的子进程是tar而父进程居然是crond。那一刻我心里基本就有数了这是一个由cron拉起来的定时任务正在做压缩打包。之前用top看到的零星tar进程就来自这里。但为什么一个打包任务能把整机拖垮我当时还没想明白因为从表象看它占的CPU并不高。这里补充一个排查小技巧如果怀疑系统里某个进程在短时间内反复出现可以用这个shell循环快速捕捉现场for i in $(seq 1 60); do ps -eo pid,user,ppid,etime,cmd --sort-%cpu | head -20 /tmp/top_ps.log sleep 1 done它会把1分钟内每秒钟CPU占用最高的20个进程全部记到文件里之后你再去分析时间线就能看到哪些进程是“闪现”出现的。实战里这一招比死死盯着top要靠谱得多。3.2 系统日志、cron日志与审计线索锁定父进程是crond之后下一步就是去翻cron自己的执行记录。这里先给大家提个醒Linux的cron日志路径不是所有发行版都一样。CentOS/RHEL一般在/var/log/cronUbuntu/Debian默认未必开启独立的cron日志cron的相对信息可能会跑到/var/log/syslog里。如果你不知道自己的系统在哪可以直接用journalctl -u crond或journalctl -u cron查systemd管着的cron服务日志。我翻了/var/log/cron找到了当天执行过的任务清单很快就锁定了一个每天14:30执行的备份脚本。这个脚本做的事情是把某个业务目录做全量打包然后压缩再同步到备份机器上。执行时间正好和用户开始反馈卡顿的时间点高度吻合——14:30开始打包14:35左右页面开始打不开。时间线对得上这只是“嫌疑”成立的第一步还需要更充分的证据证明它就是元凶。当时我还遇到一个干扰项日志里显示这个脚本“执行完成”退出码是0。很多人的误区就在这——看到任务执行成功就觉得“那应该不是它的问题”。但退出码是0只能说明脚本内部命令最终没报错完全不能代表脚本运行期间没有造成资源争用。我们要找的不是“执行失败的任务”而是“执行过程中抢占大量资源的任务”。所以日志里显示成功不但不能洗清嫌疑反而可能因为它执行太久、占用太多资源而成为重点嫌疑对象。3.3 用strace/lsof补刀定位行为特征为了进一步确认我决定对那个tar进程使用strace做短时间的行为采样。有人可能会问直接strace -p PID不行吗其实线上环境要非常小心strace默认会严重拖慢被跟踪进程如果直接在压死骆驼的服务器上给关键业务进程做全量跟踪可能直接把业务搞挂。但因为有crond这个父进程顶着我选择跟踪一时间段内新出现的tar进程然后把输出重定向到文件里做分析。strace -f -tt -T -e tracefile,read,write -p tar进程PID -o /tmp/tar_strace.log等了几秒之后通过跟踪结果看到脚本在打包时不停地read一些小文件、然后调用write写压缩文件单次I/O很小但频率极其密集。也就是说这个任务并不是在“读大文件写大文件”这种高效的流式处理而是在对小体量、数量庞大的文件做逐个遍历压缩大量的系统调用和磁盘寻址让I/O设备几乎饱和。这时候再把lsof的结果串进来之前发现的大量被打开的日志文件其实就是这个业务目录下的历史日志和临时文件。目录里堆积了上百万个几十KB级别的小文件打包脚本一跑全部要被处理一遍。这下证据链完整了定时任务扫描海量小文件、产生密集I/O与系统调用、磁盘等待拉高、load飙升、整机所有业务一起变慢。耗时两小时的问题至此终于定性。4. 元凶落地定时任务背后的批量脚本4.1 脚本做了什么、为什么慢——资源占用模型很多排查问题的人走到“找到脚本”这一步就停了觉得“反正是它干的我把它停了就行”。但如果不停下来把资源占用模型分析清楚下次换一个场景同样的坑还会踩。我们看这两个变量文件数量和文件体量。打包100个100MB的文件和打包100万个50KB的文件总数据量可能差不多但对系统的压力模式完全不同。前者是连续的、大体块、低系统调用次数的I/O后者是随机分散的、高频小文件的I/O。对机械盘或者网络存储而言小文件随机读写本身就是最极端的性能杀手。那台服务器恰好把业务数据落在了一块非SSD的数据盘上海量小文件让磁盘寻址开销被无限放大。更关键的是这个脚本没有互斥锁。也就是说如果上一次执行因为I/O太慢拖长了时间下一次cron触发时间到了新进程不会等老进程跑完而是直接再起一个。两个打包任务同时开跑I/O压力直接翻倍。当时虽然没有检查是否发生了重叠执行但很多“服务器越到后面越卡、最后完全卡死”的案例都是这种cron任务叠加导致的。4.2 为什么定时任务常被忽略回头看定时任务之所以难定位主要有四个原因。一是偶发现象它不像常驻进程那样一直在列表里看top一眼就能找到二是有周期性而监控大屏的“当前状态”视图很少把时间维度的负载曲线和cron调度时间做对齐三是脚本本身可能没报错、退出码为0给人一种“一切正常”的错觉四是现场痕迹会被覆盖cron日志里每一行都长得差不多如果不带着“疑似时间段”去翻根本看不出异常。这里想特别强调一个排查思维当一台机器出现“整体性变慢”而你找不到一个稳定的高占用进程时不要只盯着“现在谁在跑”而是要问“这一时刻前后谁被调度起来了”。定时任务的特点就是周期性、短时性它的影响就像有人每隔一小时往你桌上倒一盆水——水会退但你桌面一直是湿的。如果你只在“退潮”的时候看现场自然会一无所获。4.3 修复方案与验证修复动作分了四步走。第一步立即止血把那个cron条目注释掉同时kill掉正在运行的打包进程让系统先恢复稳定。操作后大概两三分钟top里load就开始往下掉iostat的%util从接近100回落到正常水平接口延迟也跟着恢复正常。第二步给脚本加锁使用flock确保同一时间只有一个实例在运行。* 14 * * * /usr/bin/flock -xn /tmp/backup.lock -c /opt/backup/run.sh-x表示排它锁-n表示拿不到锁就直接退出等于给任务加了一道“只能串行跑”的闸门。这是所有定时任务都应该有的基本保护。第三步优化打包策略不再每天全量打包整个目录改为增量备份定期全量。增量部分用find结合mtime筛选当天变动的文件显著降低每次任务的扫描量。同时把打包目标从同一块数据盘换到独立的备份分区避免读写争抢同一块盘。第四步调整调度时间把任务从业务高峰时段挪到凌晨低峰期错峰执行。改完之后我持续盯了一周每天14:30的负载曲线都变成一条很平稳的线再没出现过load飙升的尖峰。那次的经验也让我深刻意识到定位问题只是第一步改完之后如何防止复发才算把事情真正做完。5. 把这次经验固化成排障清单5.1 快速定位资源问题的命令组合一次踩坑长记性但更有效的是把经验沉淀成可以复用“套路”。下面这套是我后来处理类似“线上服务变慢”问题的固定命令组合按顺序执行基本能覆盖大部分场景。排查目标使用命令关键输出注意事项系统整体负载uptimeload average 1分钟/5分钟/15分钟对比三值变化趋势判断是突发还是持续CPU与进程快照top -d 1各进程CPU、状态、僵尸进程数需要多次刷新观察“闪现”进程动态资源趋势vmstat 1 10r、b、si、so、cs、us、sysi/so不为0说明内存回收压力大cs高说明上下文切换频繁I/O瓶颈iostat -x 1%util、await%util长时间接近100说明磁盘饱和await高说明排队严重进程行为追踪ps -ef --forest进程树的父子关系重点看父进程是否为crond或systemd高频进程捕捉shell循环记录top快照短生命周期进程的出现频率适合定位“一闪而过”的任务系统日志与cron日志/var/log/cron/var/log/syslogjournalctl -u croncron执行记录注意不同发行版的日志路径不同系统调用跟踪strace -f -p PID系统调用与耗时线上生产环境要谨慎可能拖慢进程打开文件排查lsof被打开的文件与进程对应关系lsof D可查目录下文件被谁占用文件句柄总量cat /proc/sys/fs/file-nr已用句柄数/总句柄数接近上限会引发too many open files这套组合全部跑一遍其实也就是十几分钟的事但覆盖了CPU、内存、I/O、进程树、定时任务、系统调用这些主要维度。尤其当你已经锁定了“某个时间段”之后再拿这套命令去定向回看效率会高很多。5.2 定时任务规范化锁、日志、超时、资源限制经过这件事我把团队里所有服务器的cron规范彻底重新梳理了一遍现在每一条新加的定时任务都要满足几个基本要求。互斥锁是必须的。不管脚本本身多简单都建议用flock包一层避免周期触发时发生重叠执行。拿不到锁就退出宁可这一次不跑也比两个进程互相把自己拖死要好。日志必须独立且按天轮转。很多cron任务默认把输出发到root的邮箱里没用的人根本不会去看要么就输出到不固定的日志文件里导致出问题时翻不到记录。规范做法是每个任务一个独立日志目录日志文件名带上日期脚本里自己判断超过一定大小就rotate。超时机制要有。对于一些可能长时间卡住的任务可以在cron前面加上超时控制比如用timeout 3600 /opt/backup/run.sh超过1小时直接杀掉防止任务异常时无限抢占资源。资源限制也可以设。如果某个任务是已知的重资源任务建议配合cgroup或者sytemd-run给它设定CPU、内存、I/O的配额让它即使出了故障也“闹不大”。比如用systemd-run --scope包装一下限制它对整机的影响范围。5.3 监控与告警让下次故障更早暴露事后我在监控平台里补了几条新的预警项专门针对这次暴露出来的盲区。一是load average和CPU使用率的关系告警。单纯看某一个指标容易漏但如果“负载持续超过核数3倍而CPU不高”就要立刻提示排查I/O等待或D状态进程。二是D状态进程数和iowait时间的趋势监控。这两个指标能直接反映I/O瓶颈早于业务延迟恶化之前就发出预警。三是cron执行时长的监控。我实现了一个很简单的方案让每个脚本最后写一个心跳文件记录开始时间和结束时间监控端采集这些心跳数据一旦某个任务的执行时长超过历史均值的2倍就直接触发告警。这样下次就算有某个定时任务性能劣化也不会等它把整台机器拖垮才发现。四是文件数量与目录大小的趋势监控。这次问题的根源之一就是某个目录下小文件数量失控把打包任务变成了“扫雷”。现在我会定期统计重点目录的文件总数和总大小设一个阈值一旦超过就提前告警从源头避免定时任务触发时的性能雪崩。5.4 个人体会关于排障顺序和心态最后说点代码清单之外的东西。这次两个小时里其实有一大半时间都花在了“错误方向上打转”。我一开始太相信“高CPU必然有高CPU进程”这个直觉绕在Java层查了很久而真正有效的转机是从“看快照”转向“看时间线”之后才出现的。排障这事最怕的不是不会用命令而是用“猜测”代替“验证”。每一次判断都最好有监控数据、日志记录或者命令输出做支撑。你看到tar进程就急匆匆把它当成元凶要是没有后面的straceI/O分析和时间线比对很可能中途又被别的干扰信息带偏。保持“先收集信息、再假设、最后验证”的顺序看起来慢其实是最不容易走弯路的方式。另外还有一点排查时心态要稳。遇到线上故障大家天然会着急、会想找个“明显的问题”赶紧切掉。但越是这时候越要耐心把时间线拉齐、把证据链补全。那次之后我把“用时间线对齐业务异常与系统行为”写进了自己的排障习惯里。现在再遇到类似问题我会先翻开监控系统把负载曲线、I/O曲线、业务错误曲线叠在一起看然后再上服务器逐个排查。多数情况下元凶都藏在某条你看过却忽略了的交叉点上。如果你也遇到过类似“整机变慢但找不到头绪”的情况不妨先别急着重启或扩容按着上面的顺序把证据收集齐。很多时候问题就藏在一个不起眼的定时任务里。