凌晨两点半手机在床头柜上连续震了七八次。我摸过来看了一眼告警群里已经刷了三十多条消息清一色的红色“Critical”。说实话即使干了这么多年运维那一瞬间心跳还是会加速。深夜告警最折磨人的地方不是问题本身有多难而是你在精力、判断力都在低谷的时候被迫要快速做决策。这种状态下最容易被带偏节奏要么手忙脚乱到处乱查要么对着一个表面现象反复折腾最后把故障时间拖得更长。所以我一直跟团队里的同学强调做运维尤其是负责监控值班的同学一定要有一套自己的 Linux 故障排查“作战地图”。所谓作战地图不是一本命令大全而是当你被一条告警从睡梦中炸醒的时候能按照固定套路快速给问题定性、隔离、止血、定位根因的一整套流程。这套流程练熟了深夜排障就能从“凭感觉碰运气”变成“按图索骥”沉稳度和成功率完全不一样。这篇文章我就把自己这几年沉淀下来的这套地图完整梳理一遍覆盖 CPU、内存、磁盘、网络、负载这几个最常出事的方向把排查链路、命令组合、以及那些实战中才能积累的判据都写出来。无论你是刚接手值班任务的运维新人还是经常需要半夜处理线上问题的后端开发这篇文章都值得收藏。1. 深夜告警的黄金三分钟先定性再动手1.1 告警分级哪些必须立刻爬起来哪些可以按掉继续睡深夜处理告警最大的危险不是告警太多而是分不清轻重缓急。我见过太多次这样的情况某个非核心服务抖动了一下值班同学紧张得不行爬起来又是重启又是回滚折腾半小时之后发现业务根本没受影响反过来真正核心链路出问题的时候又被淹没在一堆低级别告警里等发现已经晚了。所以动手之前先花三分钟给告警定性。我常用的判断依据就三个影响面多大、是不是核心链路、趋势是向好还是恶化。影响面看告警的主机和实例数量如果几十台机器同时报错往往是集群级或者依赖出问题如果只有一台多半是个体故障。核心链路判断起来更依赖你对业务的熟悉程度最简单的办法是看告警涉及的模块有没有直接对用户请求提供服务或者是不是其他模块的公共依赖。趋势则可以瞄一眼监控面板上的曲线方向是直线往上冲、还在高位震荡、还是已经开始回落。三个问题问完基本就能定级了。P0 是核心服务不可用或者大规模故障必须立刻处理P1 是核心服务降级或者非核心服务大面积异常需要尽快介入P2 是单个实例异常、非核心抖动、指标越限但业务无感可以观察一段时间再决定动不动。很多运维团队吃亏就吃在把 P2 当 P0 处理深夜人的判断力本来就差一慌更容易做多余的操作反而制造出新的故障。1.2 雪崩式告警的处置顺序止血永远先于定位根因一夜之间收到几十甚至上百条告警的时候最忌讳的就是一头扎进去逐条排查因为雪崩式告警通常是由一个根因引发的连锁反应。这时候你一条一条看大概率会被表象绕晕——比如数据库连接池打满、接口超时率飙升、负载一路走高等现象同时出现但其实真正的问题可能只是上游某个依赖节点挂了。正确的顺序是先止血再定位根因。所谓止血就是用最小的操作把影响面控制住如果是某个节点异常先把它的流量摘掉或者重启服务如果是集群整体容量不足优先扩容或者降级非核心功能如果是依赖的外部服务出问题考虑切到备节点或启用缓存兜底。等我写这篇稿子的时候想起某次故障就是这样——缓存节点集体失效导致后端存储压力暴涨值班同学第一反应是去调数据库参数结果越调越乱后来把流量切到备用集群系统很快就稳定了根因是缓存服务的发布配置出了问题和数据库半毛钱关系都没有。止血完之后系统恢复稳定了再慢慢查根因。这个阶段心态可以从“救火”切换到“破案”该抓包抓包、该看日志看日志时间压力小很多分析也准确得多。1.3 我自己的“告警定性”速查清单做值班培训的时候我经常给同学发一张简易自查表让他们收到告警之后先按表格走一遍再决定要不要喊人。这张表我一直觉得很有用整理出来给大家参考看到的现象首查命令判断依据系统负载高top、vmstat 1看 CPU 使用率高还是大量进程处于 D 状态单个服务 CPU 飙高top -Hp pid定位到具体线程再结合线程栈找代码热点内存告警 / OOMfree -h、dmesg -Tavailable 是否告急OS 是否杀过进程磁盘空间满df -h、df -i是容量满还是 inode 耗尽两者处理方式完全不同接口超时 / 连接失败ss -s、ss -antp是连接表满、端口耗尽还是对端无响应应用假死但 CPU 很低ps -eo stat、vmstat 1有大量 D 状态进程的话方向在磁盘/IO 而非 CPU这张表不解决所有问题但能帮你避免半夜睡眼惺忪的时候做出离谱判断。记住一个原则能通过一条命令判断方向的绝不上来就重启实例。重启虽然能暂时解决很多问题但也会把犯罪现场毁掉。2. 负载飙高先分清是 CPU 忙还是进程在挨饿2.1 load average 到底在看什么三个数的背后是“运行队列”uptime和top第一行都会显示 load average后面跟着三个数字比如load average: 8.52, 6.31, 4.07。不少新手把它当成 CPU 使用率来看这是一个很常见的误区。load average 统计的其实是处于“可运行状态”和“不可中断睡眠状态”的进程平均数你可以把它理解为系统里正在排队等着被处理的任务数量。这三个数分别是过去 1 分钟、5 分钟、15 分钟的平均值。为什么这个指标容易误导人因为“排队任务多”既可能是因为 CPU 忙不过来也可能是因为进程在等磁盘 IO。我见过最典型的一个场景某台机器 load 飙到 60但top一看 CPU 使用率才 10%很多人直接就懵了。其实进程并没有在占用 CPU而是卡在 IO 等待上全部处于 D 状态不可中断睡眠排队排了一大串。这种情况下如果你一头扎进 CPU 优化方向就全错了。有一个比较通俗的理解方式把 CPU 想象成收费站窗口load 就是等待过站的车辆数。窗口少车多load 高是 CPU 忙但如果窗口明明闲着车却全堵在路上过不来那就说明道路磁盘/网络 IO出了问题。2.2 CPU 打满的定位链路top → top -Hp → perf top如果你判断的方向确实是 CPU 忙那就要走下面这条链路。第一步top看整体。按 P 键让进程按 CPU 使用率排序找到最靠前的那个进程记下它的 PID。很多情况下你第一眼就能看到某个进程的 CPU 使用率是 100% 甚至 800%那它就是嫌疑人。第二步top -Hp pid看进程内部的线程。这一步很关键因为对于多线程服务CPU 高烧的往往只是其中某几条线程而不是整个进程。执行之后同样按 CPU 排序记下异常线程的 TID同时把线程号转成十六进制printf 0x%x\n tid后续很多工具都要用这个十六进制的线程 ID。第三步用perf top或者perf top -p pid -t tid看内核采样出来的热点函数。perf是 Linux 自带的性能剖析工具它通过对 CPU 进行周期采样告诉你在那个时间窗口里CPU 到底在执行哪些函数。如果你看到某个函数名占用比例特别高在配合代码仓库定位到具体逻辑就基本实锤了。如果看的是 Java 应用还需要用jstack pid thread_dump.txt从线程栈里找和 TID 对应的线程看它在跑什么业务代码。2.3 用户态高 vs 内核态高两个完全不同的排查方向top里 CPU 行有 us、sy 这几个数值分别代表用户态 CPU 占比和内核态 CPU 占比。这两个数高对应的排查方向差异非常大。us 高说明 CPU 在跑业务代码通常是死循环、热点计算、正则回溯、GC 频繁这类问题。定位手段就是我上面说的线程栈加 perf。之前某个后端服务 CPU 突然飙到 300%最后用perf top看到热点集中在一个正则匹配函数上查下去发现是某条线上数据触发了灾难性回溯解决方式是给正则加上执行超时和更严格的匹配规则。sy 高说明 CPU 花在内核态常见诱因是系统调用过于频繁、上下文切换太多、锁竞争激烈、中断处理负载高。这时候你要重点看vmstat 1里的cs上下文切换次数和in中断次数如果每秒上下文切换几万甚至几十万次多半是线程数量过多、锁粒度太粗或者业务在疯狂创建和销毁线程。这种情况光看业务代码不一定能找到根因要往并发模型和锁的方向去思考。2.4 一个死循环定位全过程拿一个我印象比较深的案例走一遍完整流程。某天晚上收到告警某核心服务的实例 CPU 使用率持续 100%接口平均响应时间从 50ms 涨到了 5 秒P0 告警。我看了一眼top确认是 Java 进程占了接近一个整核。随后执行top -Hp pid拿到 CPU 最高的线程 TID再转换成十六进制用jstack导出线程栈搜索那个 TID 对应的线程发现它卡在 JSON 序列化库的一个方法上。顺着代码往下找定位到一个日志打印语句——某个字段在异常场景下被填充了一个超大字符串序列化逻辑陷入循环分配内存导致 CPU 连续饱满。处理方式是先临时降低该接口的超大字段打印级别再修复序列化逻辑增加长度保护。从收到告警到定位根因整个链路走了不到十分钟。如果没有这套自上而下的定位方法而是面对一个上百 GB 的堆转储文件去分析大概率要折腾到大天亮。3. 内存告警与 OOM别再急着改 oom_score_adj3.1 先看 free再看谁在偷偷吃内存接到内存告警之后我习惯先跑free -h。注意这里重点看的不是 used 那一列而是 available。Linux 的内存管理策略是“闲着也是闲着”会把空闲内存尽量用作页缓存来加速文件读写所以你经常看到 used 很高、buff/cache 也很高但系统依然活得很健康。available 列估算的是当前真正可以分配给新进程的内存它已经把缓存可回收部分算进去了是更有参考价值的指标。如果 available 确实在持续往下掉那就看谁在吃内存。ps aux --sort-%mem按内存占用排序前几行基本就是嫌疑人。但是我提醒一句RSS常驻内存高不一定代表内存泄漏很多框架、缓存中间件本来就会主动占用大量内存作为缓冲。关键要看趋势连续多次采样观察 RSS 是不是只涨不跌、每次重启之后又缓慢爬升。如果内存像漏水的桶一样补了又漏那基本可以断定是泄漏。3.2 dmesg 里的 OOM Killer 日志系统已经把凶手写出来了如果系统内存被耗尽Linux 内核的 OOM Killer 会启动它按照一套评分机制挑选一个进程杀掉把内存释放出来。很多人遇到“服务进程突然消失”第一反应是看服务日志、看系统日志但其实最直接的证据在dmesg -T | grep -i oom里。OOM Killer 的日志会明确告诉你哪个进程被杀了它在被杀时的 RSS 占用是多少系统当时内存总量和剩余量各是多少甚至连评分顺序都会列出来。这基本等于内核帮你完成了第一轮排查。比如日志里如果显示被杀的进程是某个数据库进程但它的内存占用其实并不大那真正的问题可能是同机的其他进程把内存抢光了数据库只是当了替罪羊。这里有个常见的坑看到 OOM 之后有人习惯性地调整oom_score_adj让某些关键进程免于被杀。我不建议在没搞清根因之前就这么做。oom_score_adj这个参数只能改变内核“优先杀谁”的决策并不能解决内存不足本身。如果物理内存真的不够你调低了关键进程被选中的概率结果就是别的进程挨刀系统依然不稳定。正确的姿势是查清楚是谁耗光了内存要么扩容要么修泄漏。3.3 用 /proc/pid/smaps 追内存泄漏怀疑某个进程泄漏但又不想上重量级的分析工具时/proc/pid/smaps是个好东西。这个虚拟文件记录了进程每一段内存映射区的详细信息包括私有内存、共享内存、页表开销等。配合两次采样对比可以判断哪些映射区在增长。更简洁的做法是直接读VmRSS和匿名页相关字段。匿名私有内存Anonymous持续增长往往是堆内存分配malloc/new没有被正确释放的典型信号。虽然/proc/pid/smaps内容非常长、肉眼读起来很吃力但在应急场景下grep -E ^(Size|Rss|Anonymous) /proc/pid/smaps | sort -k2 -n | tail -20能快速告诉你哪一段内存增长最凶缩小排查范围。3.4 swap 与内存回收的常见误区关于 swap 有一些流传很广但仍然模糊的说法。swap 本身不是坏东西它是内存的“应急车位”。系统内存不够时把冷数据换到磁盘上腾出物理内存给热数据这是正常机制。真正需要警惕的是 swap 的频繁换入换出swap in/out 抖动因为磁盘 IO 比内存慢几个数量级一旦系统进入“疯狂换页”状态整体性能会断崖式下跌。vmstat 1里的si、so两列就是换入换出的流量。如果这两个数值持续居高不下说明内存压力已经到了系统不得不频繁交换的地步。这时候与其纠结vm.swappiness内核使用 swap 的倾向程度改成 10 还是 0不如先把内存大户的问题解决掉。对了vm.swappiness我向来建议在确认有实际内存压力时再调很多机器默认配置下 swap 几乎不会被动用你调来调去也就是个心理安慰。4. 磁盘与 IO 拖垮系统R 状态和 D 状态的真相4.1 磁盘满 vs inode 满两种“满”的排查命令完全不同磁盘问题有两种“满”症状都是“写不进去文件了”但排查思路和解决方案完全不同。第一种是容量满。df -h输出里如果某个挂载点 Use% 到了 100%文件系统写不进去应用开始报磁盘空间不足。这种相对好排查du -sh /*层层往下找大目录、大文件就行常见的坑是日志文件忘开轮转或者某个临时目录积累了海量中间文件。第二种是 inode 满。df -i输出里 Ifree 为 0明明磁盘还有空闲空间但新文件一样创建不出来。inode 是文件系统用来记录文件元数据的存储单元每个文件、目录都要占一个 inode。小文件特别多的场景最容易把 inode 耗尽之前某公司有个任务调度服务每天往一个目录里写几十万个几 KB 的小临时文件监控只看了磁盘容量没看 inode某天磁盘还有 30% 空间却突然全部任务失败查了半天才意识到是 inode 爆了。所以磁盘告警务必两个命令一起看缺一个都可能误判。4.2 iostat 的正确打开方式util 并不是唯一指标定位到磁盘方向之后iostat -x 1是必备工具。很多教程会让你盯着%util看这个指标代表设备忙的时间占比接近 100% 确实说明设备“很忙”但它解释不了“忙得有没有效”。我习惯把%util、await、avgqu-sz三个指标连起来看。await是 IO 请求的平均处理时间毫秒包含排队时间和处理时间。如果%util很高但await不算离谱说明磁盘虽然在高负荷工作但吞吐正常可能是业务量增长导致的正常压力。如果%util一般但await很高说明 IO 请求大量卡在等待队列里设备的响应能力已经跟不上了这种往往是硬件降级、云盘异常或者后端存储抖动。avgqu-sz是平均队列长度队列越长说明积压的 IO 越多延迟就越明显。这里插一句不要用dd去测磁盘性能来判断生产环境故障。dd测的是裸设备的顺序读写能力和实际业务的随机读写模式差异很大数字好看不代表业务不卡。4.3 一个被 IO hang 拖到“假死”的案例分析有一种故障最有迷惑性服务进程看起来完全没死但请求全部超时top显示 CPU 占用很低load average 却高得离谱。有次某个核心服务的uptime出来 load 已经到 80但 CPU 空闲 90% 以上所有进程都像被施了定身术。第一步跑vmstat 1看到b列阻塞进程数接近满值、waIO 等待占比长期 80% 以上方向基本就指向磁盘 IO 了。接着跑ps -eo pid,stat,cmd | grep D看到几十个进程全部处于 D 状态不可中断睡眠通常是在等 IO 返回实锤是 IO 卡死。然后iostat -x 1确认某块磁盘的await已经飙到几千毫秒请求根本得不到响应。这种场景下服务本身没有代码问题问题出在底层存储。处理上先通知上层业务摘流量确认存储侧异常后等待设备恢复或切换备机。很多时候这块磁盘最终被运维标记为故障盘剔除出集群。事后复盘最重要的经验是load 高不立刻关联 CPU先把进程状态看清楚方向和结论完全不同。4.4 定位 D 状态进程与 IO 瓶颈的完整命令链把这条链路完整地整理在这里按顺序走就行ps -eo pid,ppid,stat,cmd | awk $3 ~ /D/ {print}找出所有处于 D 状态的进程。vmstat 1观察procs的 b 列和cpu的 wa 列确认是否有大量进程阻塞在 IO 上。iostat -x 1定位具体是哪个设备await、%util异常。lsblk//proc/diskstats确认设备类型和挂载关系判断是哪块磁盘、哪个目录对应的存储。结合dmesg -T | tail -100检查有没有磁盘 I/O error、reset 这类内核日志。这套组合拳走完磁盘/IO 层面的故障基本都能圈定在一个很小的范围内。5. 网络连接打满间歇性失败比彻底断网更磨人5.1 排查顺序ss → 连接表 → tcpdump网络故障有个特点彻底断网反而好查拨不通就是拨不通链路一目了然最磨人的是间歇性失败——时好时坏偶发超时很久才出现一次错误。这种故障最需要系统化的排查顺序。第一个命令永远是ss -s它直接输出当前主机的 TCP socket 汇总总共多少连接、各个状态各多少、端口使用情况。这个命令的价值在于帮你宏观判断——连接数是不是已经逼近上限TIME_WAIT 是不是堆积如山SYN 队列是不是溢出如果没有这种宏观视角直接上去抓包就像大海捞针。第二步是ss -antp看具体连接重点观察状态分布大量 SYN-SENT 说明主动连接失败大量 SYN-RECV 说明被动接受队列溢出大量 ESTABLISHED 可能只是正常业务量大量 TIME_WAIT 则要看服务类型下面我会单独说。第三步才是抓包。tcpdump -nn -i any port 端口 -c 数量重点看三次握手是否有重传、是否有 RST 包。RST 包的出现位置很关键是建立连接时报错还是传输过程中被对端强制断开对应的问题完全不同。5.2 TIME_WAIT 堆积知道它是什么才知道要不要处理TIME_WAIT 状态可能是网络排障里被误解最多的一个。它出现在 TCP 主动关闭连接的一方是 TCP 协议为了确保迟到的报文不会串扰到新连接而设置的一个等待定时器通常要等 2 个 MSL报文最大生存时间。这不是什么 bug而是协议设计的正确行为。但为什么有些服务会出现几万个 TIME_WAIT原因很简单大量短连接频繁创建和关闭主动关闭的一方积累了太多等待中的 socket。这会带来实际问题每个 socket 要占一个本地端口端口总数是有限的默认大概 28000 多个如果 TIME_WAIT 堆积速度超过释放速度本地端口会被耗尽新连接就建立不起来了。处理 TIME_WAIT 的正确姿势有两个层面。业务层面尽量让服务使用长连接、连接池减少连接创建销毁次数这才是治本。内核层面可以适当调大本地端口范围net.ipv4.ip_local_port_range同时开启net.ipv4.tcp_tw_reuse——注意这个参数只在客户端主动发起连接时复用 TIME_WAIT 状态的连接有它的适用边界不是银弹。至于tcp_tw_recycle这个老的调优项早已因为 NAT 环境下会引发严重问题而被废弃了不建议在任何现代系统上开启。5.3 /proc 网络参数调优的几个安全姿势除了端口范围还有几个网络参数在排障时经常用到我把它们列一下参数作用安全调整思路net.ipv4.ip_local_port_range本地发起连接可用的端口范围确认当前值按需放宽但要避免和业务监听端口冲突net.core.somaxconn全连接队列accept 队列上限后端服务处理不过来时配合应用层的 backlog 一起调大net.ipv4.tcp_max_syn_backlog半连接队列上限SYN 洪水或握手慢时考虑调整但要看整体链路fs.file-max/ulimit -n文件描述符上限连接数多时经常先撞这里注意进程级和系统级一起改调网络参数的通用原则一次只调一个先记录当前值调整后观察指标变化和业务影响不要为了“看起来更优”而一次性把一堆参数全改了。改完用sysctl -p生效但生产环境一般建议把改动写进/etc/sysctl.conf以持久化不然重启就丢了。5.4 案例端口耗尽导致的间歇性 connect 失败某次某公司一个内部服务报“大量调用失败”奇怪的是故障不是持续的而是每隔一段时间就出现一波。查看应用日志报错集中在 connect timeout业务方的第一反应是怀疑网络链路不稳定联系了网络团队排查了一整天没有结论。我们接手之后我先跑了ss -s看到 TIME_WAIT 数量高达两万八几乎和本地端口范围的上限差不多。再查cat /proc/sys/net/ipv4/ip_local_port_range默认范围大概是 32768 到 60999总共约两万八千个端口。结果一下子清楚了调用方是个高频短连接客户端连接释放后大量 socket 进入 TIME_WAIT在峰值速率下把本地可用端口挤光了新连接自然建立失败。临时缓解是把端口范围下限调低到 10000 附近把端口池扩大一倍根治措施是让调用方改用连接池复用连接从源头消灭短连接。上线之后间歇性的 connect 失败再也没有出现过。这案例想说明的是很多网络层“玄学”问题的根子不在网络硬件上而在 socket 的生死循环里。6. 把作战地图沉淀成自己的 SOP深夜少踩坑的底层逻辑6.1 复盘要记录什么别只写“重启了服务器”每次故障处理完之后我都会花点时间写复盘记录。但不是简单写一行“某年某月某日某服务故障重启解决”这种记录对以后没有任何价值。真正有用的复盘至少包含四个部分完整时间线、现场数据、操作记录、结论与后续。时间线要精确到分钟告警何时触发、何时开始处理、何时恢复中间每一步操作的时间点都尽量还原。现场数据是最容易被忽略的也是最难补的——很多同学处理完故障才发现当时没留top输出、没存ss -s结果而进程已经重启当时的现场早就没了。所以我给自己定了个强制要求动手做任何变更之前先把当前的关键指标快照存到一个临时文件里哪怕只是top -bn1和ss -s的结果复盘时这些几百字节的文本比什么都珍贵。6.2 阈值与监控项的调优方向告警阈值这事很多团队默认用监控平台自动生成的规则比如 CPU 超过 80% 就告警、磁盘超过 80% 就告警。这类全局统一阈值在真实场景里很容易失效因为不同模块对资源消耗的敏感度完全不一样。某个批处理集群 CPU 常年 90% 以上它本身就是设计成吃满 CPU 的某个核心数据库磁盘利用率到 60% 就得预警因为业务峰值一来会上涨十几个点。我的建议是每个关键业务的告警阈值都要单独梳理一遍结合它的历史指标曲线、容量水位、业务周期来定。同时重要的不只是阈值还有“持续多久才告警”——瞬时尖峰一般不用处理持续 5 分钟以上才值得拉响警报。设置不当的结果是值班同学被狼来了磨掉了警觉真正的大故障来了反而没人当回事。6.3 沉淀脚本与 CheckList 的实操建议最后说说把晋升为日常资产。前面所有命令链路都可以沉淀成可复用的脚本和检查清单。比如我维护了一个脚本库里面有几个核心工具一键采集系统概览的脚本执行后输出负载、CPU、内存、磁盘、网络连接状态到一个文本文件里一键检查 D 状态进程与 IO 瓶颈的脚本一键导出关键 socket 统计的脚本。这些脚本逻辑都不复杂就是把我手工排查时的一串命令按顺序执行并格式化输出但它带来的价值是巨大且稳定的——哪怕是一个没有经验的新人收到告警后先跑一遍通用采集脚本再带着完整数据找人求助沟通效率会高非常多。团队轮值的时候我也会准备一张纸质版其实是云端文档的排障 checklist从“确认告警影响范围”到“完成现场快照采集”再到“按对应故障类型走排查手册”。这样做不是要给操作者添麻烦而是确保每个人在精力最差的深夜也能用最小的心智成本把该走的流程走完。这几年带团队下来我越来越觉得深夜排障最可怕的其实不是故障本身而是人没章法时的那种慌乱。你越是凭感觉操作越容易错过真正的根因也越容易扩大故障面。这世上没有哪张作战地图能覆盖所有故障但有了这套方法论和命令链路之后你在绝大多数场景里都能快速稳住阵脚把故障时间控制在合理范围内。哪怕最后没有定位到根因至少你留住了现场数据、做对了止血操作保证系统先恢复——这就是深夜告警场景下最大的胜利。