干运维这行十年了最怕的就是半夜手机响。系统故障毫无征兆地扑过来就像防火警报突然拉响你得在最短时间内判断火势、找到火源、控制蔓延。这些年我经手过不少线上事故也看过太多新人在故障面前手忙脚乱上来就是top、kill、重启三连症状没了根因却还在原地等着第二次爆发。这个手册从一个朴素的问题出发当系统出问题时怎样用一套可复制的流程快速定位根因并把临时补救变成长期治理。全文围绕问题排查与系统优化展开覆盖CPU、内存、磁盘、网络、应用日志五个核心方向的排查方法以及故障后的优化路径。适合的对象很明确刚接触系统运维的开发者、需要独立扛事的值班负责人以及所有把生产环境看得比命还重要的人。接下来我从四层展开先建立排查思维再逐个拆解核心指标然后给出一套命令行实战流程最后聊聊从临时补救到长期治理的优化路径。1. 排查思维先别急着动手把问题定义清楚1.1 救火和防火是两件事很多人一收到告警就冲到服务器上见到进程异常就kill见到内存偏高就重启。这其实是最大的坑。救火和防火是两件事排查问题的本质是定位根因不是消除现象。我见过太多案例某个进程CPU飙高重启后恢复结果第二天又飙。这就像家里烟雾报警器响了你把电池拆了火还在。正确做法是先搞清楚三个问题这个问题是偶发还是持续从什么时候开始的影响范围是单个节点还是整个集群如果一问三不知后续所有操作都是盲目的。排查的第一原则就是在没搞清楚问题是什么之前不要轻易动任何东西。这里的“动”包括重启、kill、卸载、扩容。你可能觉得线上故障时间宝贵不能干等着。不是干等而是用最短的时间做信息收集。一个训练有素的排查者初期几分钟全部花在“确认事实”上而事实包括故障现象是什么形式呈现的是报错还是变慢还是彻底不可用精确到分钟的发生时间最近是否有发布、配置修改、数据迁移等变更动作影响面是单实例、单机房还是全集群。这四个事实能确认排查方向就锁定了一半。1.2 问题复现与影响面评估说回升的问题偶发性故障最让人头疼。比如内存泄漏导致的OOM可能要跑三天才触发一次你盯着的半小时里它偏不出现。碰到这种情况不能靠运气抓现场要主动创造复现条件。我的做法是先做影响面分级再决定复现所需的投入。影响面分级我给个参考标准P0全站不可用用户完全无法访问必须立即召集所有相关责任人。P1核心链路受损部分用户受影响优先处理且要保持通报。P2非核心功能异常存在可用替代方案纳入常规排期。P3体验类小问题不涉及业务正确性记录后跟进即可。很多新手容易把P2当成P0折腾一晚上第二天发现只是某个边缘接口超时。分级不是官僚主义是让有限的人力用在正确的位置上。P0和P1的核心差异在于P0必须拉齐上下游所有团队并行排查P1则可以先由一线人员独立定位避免一窝蜂涌上去反而制造混乱。复现分两步走。第一步是尽量从日志和监控中找到触发条件比如特定请求参数、特定时间点、特定数据量。第二步是构造最小化复现环境把流量复制或者压测脚本跑起来观察指标变化。有些问题在测试环境怎么都复现不了但一到生产环境就冒出来原因往往在于数据规模和并发模型不同。这时可以尝试在生产环境的非核心节点上用灰度流量逐步放大配合日志观察但前提是风险可控且有快速回滚方案。1.3 黄金时间内的信息收集清单故障发生后的第一个十分钟是“黄金信息收集期”。很多线索会随着时间快速丢失服务一重启内存里的信息就没了日志被滚动覆盖历史就没了。所以优先级是先收集能保留下来的证据再谈修复动作。我建议把下面这份清单打印出来贴在工位上或者存成笔记软件里的模板故障现象的描述原文告警信息截图或复制。发生时间以及最近一次确认正常的时间点。最近一次发布或变更的详细内容包括代码提交记录和配置变更记录。监控面板上异常指标的截图至少覆盖CPU、内存、磁盘IO、网络入出流量。关键日志的最新100行先截下来放进本地文件。当前进程列表所有关键进程的PID、CPU占用、内存占用、启动时长。这些信息在复盘时价值极高。我可以直言很多故障报告写得稀烂就是因为这些基础信息在事发时没有留存。不要依赖记忆力不要觉得“待会再补”先存下来再说。2. 核心指标拆解CPU、内存、磁盘、网络的排查要点2.1 读懂top输出别被load average吓住top是绝大多数人的第一命令但很多人根本没看明白。第一行的load average有三个数值分别对应1分钟、5分钟、15分钟的平均负载。这个值的单位不是百分比而是“处于可运行状态或不可中断状态的进程数量平均值”。判断是否过载要看CPU核心数8核机器load超过8才是饱和load到4只能说明一半核在忙还有余量。实际排查时我更关注的是这些点%Cpu(s)行的us、sy、wa、id四个值。us是用户态CPUsy是内核态wa是IO等待id是空闲。wa高说明磁盘或网络IO在拖后腿这时候加CPU核心数没有意义。进程列表的STAT列。R表示正在运行D表示不可中断的睡眠通常是等待磁盘IOS是睡眠Z是僵尸进程。D状态一多基本可以断定是IO层面的问题。进程的RES和%MEM。RES是常驻内存不含swap部分。看到RES持续上涨就要警觉内存泄漏。一个常见的误判动作看到某个进程CPU 99%就立刻kill。但你得先问它是计算密集型的正常任务还是死循环还是被某个特定请求触发了异常路径。按top交互键P可以按CPU排序看具体是哪个进程、哪个线程。如果进程内多线程还要继续用top -H -p PID查看线程级别的CPU分布。2.2 内存问题别把buff/cache当内存泄漏free -h输出里有个buff/cache字段很多新手看到used高、free低就慌了。其实buff/cache是内核使用空闲内存做的缓存用于加速磁盘读写在内存紧张时会被自动回收。真正需要警惕的是swap使用量以及是否存在持续的swapping进出。我踩过的一个典型坑是某服务内存持续上涨看起来像泄漏其实是堆外内存没有设上限传输框架不断分配缓冲区。用free看总量是够的但进程RES在缓慢爬升最后触发OOM killer。排查这类问题光看free不够要用jstat看Java堆内存的分区变化用pmap查看进程的地址空间映射找到内存增长的具体归属。关于OOMLinux的OOM killer会选择分数最高的进程杀掉分数和进程占用内存、运行时间、进程优先级都有关系。如果你的关键进程经常被误杀可以通过调整oom_score_adj来降低被杀概率但生产环境更要紧的是找到内存消耗的真实来源否则只是把炸弹推给别的进程。2.3 磁盘与网络的隐形瓶颈CPU和内存是显性资源磁盘和网络更像隐形杀手。我处理过一个问题应用响应慢CPU和内存看起来都正常只有磁盘util接近100%。用iostat -x 1看await从正常的几毫秒飙到几百毫秒说明磁盘IO等待相当严重。而util高不一定是硬盘坏了可能是某个进程在全量扫描也可能是RAID重建甚至可能是文件系统碎片过多。网络方面很多开发只盯着带宽排查时更要注意连接状态。ss -s能看到socket统计如果TIME_WAIT连接上万说明连接频繁建立和释放可能触发了端口耗尽问题。这类情况常见于HTTP短连接场景单纯调大文件描述符只是治标治本方向是在应用层用连接池或开启keep-alive。四类核心资源的排查要点我整理成一张速查表供日常参考资源首要指标异常信号优先排查方向CPUload average、us/sy/wawa高、运行队列长磁盘IO、锁竞争、死循环内存free、swap、进程RESswap持续增长、RES只升不降内存泄漏、堆外内存、缓存配置磁盘iostat的await、utilutil接近100%、await飙升慢查询、全表扫描、日志量突增网络连接数、重传率TIME_WAIT积压、端口耗尽短连接模式、连接池缺失、防火墙规则3. 工具链实战从定位到恢复的完整排查流程3.1 一套可复制的命令序列很多新手刚到服务器上不知道先跑什么命令。我习惯按下面这个顺序来一分钟左右能拿到一张比较完整的体检报告uptime top -c vmstat 1 5 free -h iostat -x 1 ss -lntp df -h df -i有的放矢地解释一下这些命令各自的职责。uptime看负载均值趋势如果1分钟负载远高于15分钟说明故障正在发生或刚刚发生。top -c按CPU排序同时展示完整命令行能快速找到占用排前的进程。vmstat 1 5采样五次观察r运行队列、b阻塞进程、si/soswap换出换入、wa四列可以判断是否存在资源竞争或IO阻塞。free -h先看总量和swapiostat -x 1只有在前面发现wa高时才需要深入避免无谓的信息轰炸。ss -lntp查看端口监听和连接状态df -h和df -i分别检查磁盘空间和inode这两个都可能成为隐雷。这套组合下来大部分外围信息就齐了。之后再根据线索定向深入比如用strace跟踪进程的系统调用用perf做性能剖析或者用tcpdump抓包分析网络交互。3.2 日志分析的三种打法日志是故障排查的黑匣子也是第一现场。我总结出三种打法按场景灵活选择。第一种是时间窗过滤。定位到故障发生时间点后只查这个时间点前后五分钟的日志。如果日志格式支持时间戳用grep匹配或用awk筛选范围效率很高。很多备查日志量大全量检索不仅慢还容易看花眼。第二种是关键字关联。把告警里的错误关键字带到日志里搜索顺藤摸瓜找上下文。比如报错是connection refused就去查服务监听端口是否正常、防火墙规则是否有变化、连接数是否打满、后端节点是否健康。第三种是跨组件串联。一个请求链路会经过多个服务单一服务的日志往往看不出问题。这时要按traceId或requestId把所有日志串起来看时间消耗在哪个环节。没有traceId的旧系统可以先用调用方IP加上时间范围去串联虽然粗糙但往往也能定位到第一跳异常的服务。日志采集和存储也是一个值得提前投资的方向。我遇到过不少系统只在本地写文件故障时得一台台机器去捞日志效率极低。统一收集到日志平台并建立索引日常排查会省下大量的时间。没有条件上完整平台的团队至少要做到日志按天分目录、统一命名规范、保留最近三十天。3.3 临时缓解和根因定位不能顾此失彼线上出了大事所有人的本能反应都是尽快恢复服务。我的习惯是任何破坏性操作之前先留好现场。比如某个Java进程CPU飙高想jstack可能已经晚了但即便如此也要先执行jstack把线程栈输出下来再决定是否重启。比如进程内存异常先记录/proc/PID/status里的内存信息或者确认系统是否配置了coredump。这些现场记录是事后分析根因的宝贵素材没了就是没了。具体到生产环境的CPU异常我推荐这套定位流程用top -H -p PID查看该进程下所有线程的CPU占用。将CPU最高的线程的PID转成十六进制得到线程id。执行jstack PID thread_dump.log在输出中查找对应nid。重点看该线程的栈判断是业务死循环、锁等待、GC线程还是其他问题。这套流程跑下来大部分死循环和锁竞争问题都能定位。当然如果进程已经完全无法响应优先重启恢复服务但重启前能收集的信息必须收集完毕不要白白错过唯一的第一现场。4. 系统优化从临时补救到长期治理4.1 内核参数调整先理解再动手系统优化绕不开内核参数。比如文件描述符限制默认1024对高并发服务远远不够。修改/etc/security/limits.conf增加nofile限制需要和进程的实际需求匹配不是越大越好过大会浪费内核内存。再比如vm.swappiness。这个值控制内核在回收内存时对swap的偏好程度取值范围0到100默认值60在数据库服务器上偏高容易导致不必要的swap使用。很多运维会把swappiness调到10甚至0原因在于调低之后内存回收时会更积极回收页缓存而不是把进程内存换到swap。但前提是物理内存确实充足如果内存本来就不够调低swappiness只会加速触发OOM。TCP参数也是重灾区。net.ipv4.tcp_tw_reuse这类参数可以加快TIME_WAIT状态连接的复用但有一个被很多人忽略的坑在NAT环境下tcp_tw_reuse可能导致旧连接的序列号与新建连接冲突造成数据错乱。所以在生产环境调整任何内核参数前我都要反复确认当前值是多少默认值是多少调整的目标是什么业务场景是否匹配。改完后观察一段时间不要改完就走。这里也有个新内核的提醒较新版本的Linux内核里部分TCP参数的默认行为已有调整不能拿网上十年前的文章直接照抄。最靠谱的方式还是看内核文档和官方变更记录。4.2 应用层优化三板斧连接池、线程池、缓存内核参数只是外层多数性能问题的根源在应用层。我把它概括成三板斧连接池、线程池、缓存。连接池的典型问题我见得最多服务到数据库的连接数没有上限高峰时请求量翻倍数据库连接被打满应用全线超时。优化方向是给连接池设置合理的最大连接数和获取连接的超时时间同时在数据库侧加上连接数告警。至于合理值是多少没有标准答案要根据核心数、业务IO特点和数据库能力压测得出。线程池的典型问题是线程数设置过大大量线程在等待IOCPU上下文切换开销飙升。这里有一个工程经验IO密集型任务的线程数可以适当调大参考值是CPU核心数乘以某个系数而CPU密集型任务的线程数最好接近核心数。别迷信网上所谓“万能公式”一定要用自己业务的压测结果说话。缓存是数据库压力的解药但也是新的风险源。缓存穿透、击穿、雪崩三个问题任何一个都能让系统比没有缓存时更惨。穿透要用布隆过滤器或空值缓存拦截根本不存在的数据击穿要用互斥锁控制热点数据失效后的并发回源雪崩要给缓存过期时间加随机扰动避免大量key同时失效。引入缓存之前先想清楚这三点否则上缓存等于给自己埋雷。4.3 优化效果的量化验证优化做得对不对要用数据说话不能靠感觉。我习惯每项调整都做三轮对比优化前、优化中、优化后每轮都记录核心指标。举个例子某接口平均延迟从500ms降到200ms怎么证明参数调整的功劳看P99、P95、P50的延迟曲线同时记录服务端CPU和负载。如果P99降了而P50变化不大说明是长尾请求的问题被解决了重点看超时和异常分支。如果所有分位数都降了通常是整体资源瓶颈解除了。如果P50降了但P99反而升高那可能是缓存把平均延迟拉低但个别大请求仍然不稳定需要继续深挖。再强调一遍任何优化在发布前都要有回滚方案。参数级别的修改相对好办配置文件备份好一条命令就能还原。应用层代码级别的优化则务必做灰度发布。优化效果验证的周期也要合理至少覆盖一个业务高峰和一个低谷才能排除时间因素的干扰。5. 常见故障场景与排查技巧实录5.1 场景一应用响应慢CPU却不忙这类问题最迷惑人CPU不高、内存不高但接口延迟明显。优先查三个点第一是数据库慢查询。打开慢查询日志看是否存在SQL没走索引、锁等待、全表扫描。很多慢查询不会立刻打满数据库的CPU但会让应用线程卡在获取连接上整体响应被拖垮。第二是外部依赖超时。某个下游接口或第三方服务响应慢会顺着调用链把延迟传染给所有上游。排查方式是看调用链中每一跳的耗时找到延迟暴涨的那一跳。第三是锁竞争。用jstack抓取线程栈看是否有大量线程处于BLOCKED状态。锁竞争问题在低并发时表现不明显高并发下资源利用率并不高但延迟却在滚雪球。5.2 场景二服务频繁重启先是收到告警然后发现进程没了重启后过阵子又没了。第一步执行dmesg | grep -i oom查看内核日志确认是哪个进程被OOM killer杀掉再看当时内存总量。常见根因有三类堆内存不足、堆外内存泄漏、cgroup内存限制。cgroup限制是个容易遗漏的盲区。容器里的free看到的是宿主机内存但容器实际可用内存由cgroup限定。要确认容器真实可用上限用cat /sys/fs/cgroup/memory/memory.limit_in_bytes查看。很多容器应用在宿主机内存充裕、但容器限额不足时被反复误杀排查时务必先看限额。5.3 场景三磁盘写满但df还有空间df -h显示没有满应用却报磁盘不足这种情况查inode。执行df -i看inode使用率如果接近100%说明文件系统里小文件太多block空间仍有富余但inode耗尽。常见是小文件类型的临时文件、日志碎片、缓存文件堆积。清理时不要直接rm容易误删正在写入的文件。更稳妥的做法是先标记确认无人使用后再异步清理。另外日志轮转策略也值得检查很多系统默认只按大小不按数量轮转时间一长小文件堆积成灾。5.4 场景四大量TIME_WAIT连接TIME_WAIT积压是短连接场景的经典问题。先用ss -s统计连接状态再用netstat或ss查看TIME_WAIT数量。临时缓解手段是扩大本地端口范围例如检查/proc/sys/net/ipv4/ip_local_port_range如果范围小且峰值高扩大范围能缓解端口耗尽。但临时手段治标不治本。真正的解法是让连接复用应用层启用连接池或者HTTP层开启keep-alive让长连接替代频繁建连。调整内核TCP复用参数要谨慎前面提过的NAT场景风险必须纳入评估。还有一点容易被忽略检查负载均衡器的空闲超时配置如果超时过短即使客户端想复用连接也会被会话回收打断。5.5 场景五内存持续上升疑似泄漏这种问题不能靠重启敷衍。Java应用用jstat看堆内存各区的变化比如Eden、Survivor、Old区是否持续增长如果是堆外内存问题用pmap查看进程地址空间定位到具体的内存映射文件。处理过一例看起来是内存泄漏最后发现是日志框架的异步缓冲队列没有设置上限积压的日志对象全堆在内存里。解决了队列上限内存曲线立刻走平。内存问题的另外一个排查维度是分配速率与GC趋势。频繁的Full GC会放大内存问题先看GC日志确认是否存在大量对象晋升到老年代。如果晋升速率远高于预期大概率是某个缓存对象没有清理机制或者线程局部变量保存了本该释放的引用。6. 写在最后故障后的复盘才是真正提升的开始每次故障处理完我都会组织一次复盘。复盘不是追责而是把整个排查过程完整回放一遍找出系统性的漏洞。我的复盘清单包含八个要素现象、时间线、影响范围、根因、临时措施、根治措施、监控盲区、改进项。这里面最关键的是最后两项特别是监控盲区。我见过太多团队反复踩同一个坑根因就是监控没有补漏。如果一个指标早就可以捕获这次故障的苗头那么把它加进监控面板并设定阈值就可能避免第二次P0。在故障结束后我第一件事就是把这次事件中能反映异常的指标全部纳入监控体系哪怕这个指标看起来有点冷门。最后再分享一个多年的习惯持续维护一份自己的故障笔记。我电脑里有个纯文本文件记录了这些年遇到的所有典型案例每条一两百字包含现象、原因、处理手段。新同事来问问题我经常先翻这个文件很多“没见过”的问题翻完笔记才发现几年前就遇到过。这份手册其实就是这类笔记的系统化整理希望它也能成为你们手中一件称手的工具。
阅读完成 · 觉得有帮助?