故障本身不可怕,可怕的是没有章法地排查。很多人遇到线上问题,第一反应是登录服务器乱敲一通命令,看到 CPU 高就加 CPU,看到内存满就重启服务,运气好蒙对了,运气不好浪费半小时还没找到方向,甚至把问题越弄越糟。真正专业的排查,靠的不是灵光一现,而是一套可以复用的流程。
这篇文章讲的不是某个具体命令,而是排查的方法论:怎么准确描述现象,怎么提出和验证假设,怎么从现象一路定位到根因,怎么分层缩小范围,以及故障解决后怎么做复盘避免复发。文章会给出一张通用的排查 SOP 表格,并用一个真实感的案例走一遍完整流程。掌握了这套方法,遇到没见过的问题也不会慌,因为你有一套固定的动作去逼近答案。
排查的本质:从现象到根因
先建立一个认知:排查的目标不是"尽快让服务恢复",那是止损;排查的目标是"找到导致问题的根本原因",那是定位。这两件事有时可以并行,但绝不能混淆。
很多事故处理得不好,就是因为只做了止损没做定位。服务重启后恢复了,大家松一口气,问题记录也不写,几周后同样的故障再来一次,还是不会查。止损坏,但只有根因定位才能从根本上解决问题。
根因定位遵循一个基本逻辑链条:
现象(用户/监控看到什么)
→ 提出假设(可能是什么原因导致)
→ 验证(用证据证实或证伪)
→ 缩小范围(排除错误方向)
→ 定位根因(找到真正的源头)
→ 解决(消除根因)
→ 复盘(防止复发)
这个链条里最重要的两个动作是提出假设和验证。没有假设,就是盲目地试;没有验证,假设就只是猜测,会把排查带到沟里。专业和不专业的分水岭,就在于每一步是否有证据支撑。
第一步:准确描述现象
排查从描述现象开始,而描述现象的常见毛病是太笼统。"网站很慢""服务挂了""服务器出问题了",这些描述没法指导排查,因为信息量太低。
好的现象描述要回答几个问题:
- 谁受影响:所有用户还是部分用户?哪些地区、哪些运营商?
- 什么表现:报错页面、超时、还是响应慢?慢到什么程度,几百毫秒还是几十秒?
- 影响范围:一个接口还是一个服务?一台机器还是整个集群?
- 何时开始:故障发生的时间点,是突发的还是渐进的?
- 是否可复现:每次都能重现,还是偶发?有没有规律?
- 最近有什么变化:有没有发布、改配置、扩缩容、跑批处理?
举个对比:
| 差的现象描述 | 好的现象描述 |
|---|---|
| 网站很慢 | 从 14:20 起 www 首页加载超过 10 秒,所有用户,多地均如此 |
| 服务挂了 | 订单服务从 14:25 起 502,10 台实例全部不可用 |
| 数据库有问题 | 主库 CPU 14:30 起从 20% 升到 95%,查询延迟从 5ms 涨到 800ms |
后一种描述本身就指向了排查方向。养成"用数字和时间描述现象"的习惯,是排查能力的第一步。
另外一个关键动作是第一时间保留现场。很多人一发现故障就重启、就清日志,把证据毁掉了。正确的做法是在做任何变更之前,先把关键信息保存下来:当时的进程状态、日志片段、连接数、指标快照。top -b -n1 > /tmp/snapshot.txt、ss -s、dmesg -T | tail -50、journalctl --since "10 min ago",这些快照事后非常宝贵。
第二步:提出假设并验证
现象清楚后,要有意识地列出"最可能的几个原因",然后逐个验证。避免两种极端:一是只盯着一个方向查到底,忽略了其他可能;二是漫无目的地到处试。
假设要有依据,不是凭空猜。依据来自:经验(这类现象通常是什么原因)、架构知识(这个组件依赖什么)、变更历史(最近改了什么)、以及现象的特征(是全局还是局部)。
假设的检验要设计成"能证伪"的形式。比如"我怀疑是数据库慢导致 API 超时",验证方法就是直接测数据库响应时间,如果数据库很快,这个假设就被证伪,可以去查别的方向。
一个关键原则是二分法缩小范围。 面对复杂链路,不要从源头查起,而是从中间切一刀,判断问题在网络层还是应用层,在数据库还是缓存,在单机还是全局。每次判断都能排除一半可能性,几轮下来就能定位。
排查时要区分"诱因"和"根因"。比如磁盘写满导致数据库崩溃,"磁盘满"是直接原因,但"为什么磁盘会满、为什么没有提前告警"才是根因。只处理直接原因,下次还会满。
第三步:分层排查
面对"服务异常",从底往上或者从外往里分层排查,是最有效的方法之一。常见的分层和每层的排查工具:
| 层次 | 检查什么 | 常用命令/工具 |
|---|---|---|
| 网络层 | 连通性、DNS、端口、丢包、防火墙 | ping、traceroute、dig、nc、ss、tcpdump |
| 系统层 | CPU、内存、磁盘、负载、OOM | top、free、df、vmstat、iostat、dmesg |
| 服务层 | 进程状态、端口监听、连接数 | systemctl、ps、ss、journalctl |
| 应用层 | 日志、错误码、响应时间 | tail、grep、应用日志、APM |
| 数据库层 | 连接、慢查询、锁、主从延迟 | show processlist、慢日志、监控面板 |
| 依赖层 | 缓存、消息队列、第三方接口 | 各自的管理命令和面板 |
分层排查的操作要点:
网络层先确认"通不通"。 目标机器能不能 ping 通,端口能不能连上,DNS 解析对不对。很多"服务挂了"最后发现是网络不通或防火墙拦了。
ping -c 4 192.0.2.20
nc -zv 192.0.2.20 3306 # 测端口是否开放
curl -v -m 5 http://192.0.2.20/health # 测服务响应
ss -lntp # 看本机监听了哪些端口
系统层看资源是否瓶颈。 CPU、内存、磁盘、负载,这一步的判断前面几篇文章讲过:内存看 available 不看 free,磁盘要区分空间和性能,负载要和核心数对比。特别要注意 dmesg 里的 OOM 记录:
dmesg -T | grep -i -E 'oom|killed process'
如果进程被 OOM Killer 杀掉,日志里会有明确记录,这是内存不足的铁证。
服务层看进程和依赖。 进程在不在、状态对不对、依赖的服务起没起:
systemctl status nginx
journalctl -u nginx --since "30 min ago"
ps -ef | grep myapp
ss -s # 连接统计
应用层看日志和错误。 这是最直接的线索来源。看错误日志的最后几百行,找异常堆栈、错误码模式、时间点(和故障开始时间是否吻合):
tail -n 500 /var/log/myapp/error.log
grep -n "Exception\|ERROR" /var/log/myapp/app.log | tail -50
awk '{print $1}' /var/log/myapp/access.log | uniq -c | sort -rn | head # 看 QPS 突变的时刻
数据库层看慢查询和锁。 应用变慢时经常是数据库的锅:
SHOW FULL PROCESSLIST; -- 看当前正在执行的查询
SHOW ENGINE INNODB STATUS\G -- 看锁等待和死锁信息
SHOW SLAVE STATUS\G -- 看主从延迟
逐层排查的好处是结构清晰、不遗漏。从下往上,哪一层第一次出现异常,问题大概率就在那一层或其下方。从上往下,哪一层第一次正常,问题大概率在其上方。
一张通用的排查 SOP
把上面的方法整理成可执行的流程表,出故障时照着走:
| 阶段 | 动作 | 关键产出 | 常见错误 |
|---|---|---|---|
| 1. 接警 | 记录时间、现象、影响范围 | 现象描述文档 | 直接重启,毁掉现场 |
| 2. 止损 | 评估是否先恢复(重启/切流/回滚) | 恢复动作及时间 | 只顾止损不做定位 |
| 3. 收集 | 保存快照:进程、日志、指标、连接 | 现场证据 | 边查边清,证据丢失 |
| 4. 分层 | 网络→系统→服务→应用→数据库逐层查 | 定位到异常层 | 跳层排查,漏掉底层 |
| 5. 假设 | 基于证据提出候选原因 | 假设列表 | 只猜一个方向 |
| 6. 验证 | 设计能证伪的测试 | 证实的根因 | 把猜测当结论 |
| 7. 解决 | 消除根因,验证恢复正常 | 修复动作 | 只治表面不治根因 |
| 8. 复盘 | 记录时间线、根因、改进项 | 复盘报告 | 不写记录,下次复发 |
这张表的价值在于把"凭感觉"变成"按步骤"。新人拿着它也能系统地排查,不会漏掉关键环节。
实例走查:一次接口变慢的排查
光讲方法太抽象,用一个例子把流程走一遍。
现象描述:从 14:20 开始,电商网站下单接口响应时间从平均 200ms 涨到 8 秒,全量用户受影响,之前一切正常,最近没有发布。
第一步,止损和收集现场。 先确认不是整体故障:首页能打开,商品页正常,只有下单慢。收集快照:下单服务的 top 显示 CPU 不高、内存正常;ss -s 显示 TCP 连接数从平时的 3000 涨到 15000。
第二步,分层排查。 网络层:下单服务到数据库端口能通,延迟正常,排除网络。系统层:CPU、内存、磁盘都正常,排除资源瓶颈。服务层:进程在,但连接数异常高。应用层:日志里大量"waiting for lock"和数据库连接超时。
第三步,提出假设。 结合"只有下单慢、连接数暴涨、日志有锁等待",假设是数据库层面出现锁等待或慢查询。验证:在数据库执行 SHOW FULL PROCESSLIST,发现大量查询处于 Waiting for table metadata lock 状态。
第四步,定位根因。 追查发现,13:50 有人执行了一条对订单表的 ALTER TABLE 语句,该操作要获取元数据锁,被长时间运行的其他查询阻塞,反过来又阻塞了所有后续的订单读写。这就是根因:DDL 操作在没有评估业务影响的情况下直接在生产执行,且和长事务互相等待。
第五步,解决。 立刻 kill 掉阻塞的 DDL 会话和长事务,下单接口秒级恢复。随后规范:生产环境的 DDL 必须走审批、在业务低峰执行、执行前 kill 长事务、设置 lock_wait_timeout。
第六步,复盘。 时间线清楚:13:50 DDL 发起,14:20 影响显现,14:35 定位,14:40 恢复。改进项:上线 DDL 审批流程、给生产库加元数据锁监控、对超过一定时间的 DDL 加超时保护。
这个案例里,如果一开始就重启下单服务,问题只会短暂缓解然后复发,因为根因在数据库。只有按流程走到数据库层,才能找到真正的答案。
常见问题
排查时最先应该做什么?
先判断影响范围和是否要立即止损。如果核心业务中断,优先止损(切流、回滚、重启)保证可用性,但止损前尽量保存现场。如果只是性能下降且可接受,就先收集证据再动手。原则是:可逆的止损动作优先于不可逆的排查动作。
没有监控历史数据,事后怎么找原因?
只能靠系统日志和留下的快照,很受限。所以新机器上线就要装 sysstat、atop 这类能留历史数据的工具,让云监控和自身监控都有至少两周的数据。事后复盘靠证据,没有证据只能推测。这也是为什么监控和排查是配套的。
怀疑是最近发布的代码引起的,但没有直接证据?
用"回滚验证"这个方法:把最近一次的发布回滚到上个版本,如果故障消失,基本可以确认是这次变更引入的。这是生产环境验证变更关联性最有效的手段。前提是有可靠的上一版本可用(这就是备份和灰度发布的用处)。回滚前同样要留好现场。
多个问题同时出现,先查哪个?
先查"能解释其他问题的那个"。如果有根因假定能同时解释几个现象,就先验证它,往往会发现其他问题是它的连锁反应。同时要用现象的时间顺序判断因果,先发生的往往是原因,后发生的是结果。
总结
- 排查的目标是定位根因,不是单纯恢复服务,止损和定位要并行推进。
- 现象描述要具体到时间、范围、数字和变化,第一时间保留现场证据,别急着重启。
- 提出假设要有依据,验证要能证伪,用二分法逐层缩小范围,别从一个方向查到底。
- 分层排查是通用方法:网络、系统、服务、应用、数据库、依赖,逐层往下,第一处异常就是方向。
- 区分直接原因和根本原因,只治表面问题,故障迟早复发。
- 用 SOP 表格把排查流程固化,新人也能按步骤走,复盘要写时间线和改进项,防止同样的问题再次发生。
企业QQ咨询




