做站久了,总会有一个时刻:凌晨两点,手机告警响起来,说站点打不开了。这时候真正决定损失大小的,不是你的技术有多深,而是你手上有没有一套提前准备好的排查顺序。这篇文章不是讲某个具体故障,而是把我这几年在告警夜里反复用到的顺序,整理成一套可以照着念的东西。
一、先分清楚:是「全站不可用」还是「某个页面不可用」
告警文案经常是「站点异常」,但这个词太模糊。第一分钟要做的事情只有一件:确认影响范围。我会依次跑三条命令,从外到内:
# 1. 域名解析是否正常(本地 DNS 与公共 DNS 各查一次)
dig +short www.example.com @8.8.8.8
dig +short www.example.com @1.1.1.1
# 2. 从本机到源站的 TCP 与 TLS 是否通
curl -sS -o /dev/null -w '%{http_code} %{time_connect} %{time_appconnect}\n' \
https://www.example.com/ --resolve www.example.com:443:1.2.3.4
# 3. 服务器本机是不是活着
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1/ -H 'Host: www.example.com'三条命令的结果组合起来,基本能把问题定位到四层里的一层:DNS 错(解析到旧 IP 或解析失败)、网络错(TCP 连不上,一般是防火墙或云厂商安全组)、TLS 错(连上了但握手失败,多半是证书过期或 SNI 配置错)、应用错(本机 200 但外网 502)。
一个反直觉的经验:绝大多数「站点打不开」最后都落在应用层,但在排查的最初五分钟,人很容易往 DNS 和 CDN 上想,因为在控制台点几下比登录服务器舒服。别这样,先把上面的三条跑完,用数据说话。
二、五分钟内必看的四个现场
确认是源站问题时,登录服务器后不要先 tail 日志(几万行刷过去什么也看不出)。按固定顺序看四样东西,每一项都只需要十几秒:
uptime:三个负载值如果远大于 CPU 核数,说明进程在排队等 CPU;如果负载极低但站点不通,则是阻塞型问题(等 IO、等锁、等上游)。free -m:看 available 而不是 free。available 接近 0 且 swap 在用,说明内存压力已经很大,随时可能被 OOM。df -h与df -i:磁盘满和 inode 满的症状几乎一样(写不进日志、session 存不了、MySQL 报错),但处理方式完全不同。inode 满通常是大量小文件,比如某个目录里堆了几百万个缓存文件。ss -lntp:确认 80/443 还在监听,以及监听队列的 Recv-Q 是否顶到上限——顶满说明应用已经处理不过来,连接在内核队列里排队。
这四项看完,你能给出一个方向:是 CPU、内存、磁盘还是连接数。方向错了,后面所有操作都是浪费。
三、日志要「掐时间看」,不要「从头看」
定位到应用层之后,日志的正确读法是围绕告警发生的那一刻,取前后几分钟的窗口,而不是 tail 最后 100 行。Nginx 的 access log 里,我会先筛出状态码分布:
# 告警时刻前后 5 分钟的状态码统计
awk '$4 >= "[28/Oct/2026:02:10:00" && $4 <= "[28/Oct/2026:02:15:00"' \
/var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -rn如果 502 和 504 同时飙升,指向后端 PHP-FPM 或上游服务;如果只有 499,说明是客户端主动断开,往往是响应太慢被用户关掉,此时该看的是 request_time 的分布而不是状态码。如果 200 占绝大多数但用户还是说打不开,那就是页面体积或前端资源的问题,和服务器无关。
PHP 侧的日志更要注意区分:php-fpm.log 里的 WARNING: [pool www] server reached max_children 是明确的容量不足信号,而 ERROR: Unable to create lock file 往往只是磁盘或权限问题。看到 WARNING 就去改 pm.max_children,先确认内存能不能承受——很多「调大之后更慢」的案例,都是把机器推进了 swap。
四、动手改之前,先留一份现场
这是最容易被跳过、但事后最庆幸的一步。在生产机上做任何重启之前,花 30 秒把当前状态存下来:
ts=$(date +%Y%m%d_%H%M)
mkdir -p /root/incident/$ts
cp /var/log/nginx/error.log /root/incident/$ts/ 2>/dev/null
dmesg -T | tail -200 > /root/incident/$ts/dmesg.txt
ps aux --sort=-%mem | head -20 > /root/incident/$ts/ps.txt
ss -s > /root/incident/$ts/ss.txt
mysqladmin -uroot -p processlist > /root/incident/$ts/mysql.txt 2>/dev/null为什么值得:service php-fpm restart 之后,ps 里的进程、MySQL 里的长事务、内核的 OOM 记录,全都消失了。第二天想复盘根因时,手上只有「重启后就好了」这一句,等于什么都没学到。而这种事一年可能发生三五次,攒下的现场就是最好的教材。
五、真正有效的恢复动作,只有少数几个
告警夜里最忌讳的是「什么都试一遍」。按经验,能用最少动作恢复现场的,通常是下面这几条,而且顺序不该颠倒:
- 重启单个服务而不是整机。
systemctl restart php8.2-fpm影响面小、恢复快;reboot会让所有连接断开、缓存全空、MySQL 冷启动,很多时候重启完之后问题更大了(比如热点查询瞬间打满数据库)。 - 先摘流量再重启。如果有多台机器或者有 CDN,先把这一台的权重摘掉(或把源站切到备用 IP),再重启,用户的感受是「慢了」而不是「挂了」。这比任何技术手段都更能降低损失。
- 确认后再动手。重启前先看一眼
systemctl status和进程数,确认服务真的异常。有时候站点不通只是因为上游某个 API 挂了,重启本机服务反而制造了新的故障。 - 恢复后立刻验证而不是看日志。用自己的手机 4G 网络、不经 CDN 直连源站各打开一次页面,比看服务状态更接近真实。
六、把「排查顺序」写成脚本
人在紧张的时候会忘事,所以最有价值的投入是把上面这些检查固化成一条命令。我自己的服务器上放着一个 check.sh,输出不超过两屏,看一眼就知道有没有异常:
#!/bin/bash
echo "== load $(cut -d' ' -f1-3 /proc/loadavg) cores=$(nproc)"
echo "== mem $(free -m | awk '/Mem:/{print "used="$3"M avail="$7"M"}')"
echo "== disk $(df -h / | awk 'NR==2{print $5" on "$6}') inode=$(df -i / | awk 'NR==2{print $5}')"
echo "== nginx $(systemctl is-active nginx) php $(systemctl is-active php8.2-fpm) mysql $(systemctl is-active mysql)"
echo "== 5xx $(tail -500 /var/log/nginx/access.log | awk '$9>=500' | wc -l) / 500"
ss -lnt | awk 'NR>1{print $4}' | sort -u | tr '\n' ' '; echo
dmesg -T | grep -i -E 'oom|killed process' | tail -3这个脚本的价值不在于它多聪明,而在于它把「每次都要想一遍」变成「每次都一样」,凌晨三点半的时候不需要思考,照着输出就能做判断。
七、复盘:比修好更重要的是不再犯
故障处理完了,第二天要有一次不超过半小时的复盘,回答四个问题就够了:触发点是什么(哪一个变更、哪一次流量、哪一个依赖挂了);为什么没被提前发现(监控缺哪一项);为什么恢复花了这么久(哪一步卡住);下次怎么让它不重演(一个具体的改动,而不是「加强监控」这种空话)。
我自己从这四问里攒下来的改动包括:给证书到期加了 20 天和 7 天两道提醒;给磁盘使用率加了 85% 的告警;把「先摘流量再重启」写进了操作手册;给所有定时任务加了 flock 互斥,因为有一次备份脚本并发跑了两份,把磁盘填满了。这些改动单看都很小,但每一个都对应一个真实的失眠夜。
按这个标准,我把自己服务器上的告警砍到只剩五类:站点从外部探测连续三次失败、证书剩余不足 20 天、根分区使用率超 85%、MySQL 连接数超过上限的 80%、备份任务超过 26 小时没有成功记录。CPU 和负载不单独报警,因为它们是症状不是结果;把它们留给报表,第二天早上看一眼就够了。
每条告警的正文里要写清楚三件事:主机名和时间、判据的具体数值、以及一句「先看什么」。比如证书告警的正文直接写「剩余 18 天,先跑 openssl x509 -enddate -noout -in 证书路径,确认是续期失败还是部署没生效」。这句话在凌晨三点比任何监控图表都管用,因为它省掉了「我现在该干什么」这一步思考。
另外要给告警设置恢复通知。只有「出问题」没有「已恢复」,你会一直不确定事情到底结束了没有,然后反复登录服务器确认。恢复通知里带上故障持续时长,长期积累下来就是一份很好的可用性统计。
八、恢复之后的两件小事
服务恢复、用户能打开页面,这时候人最想做的就是关掉电脑去睡觉。但有两件事必须在当场做完,因为第二天一定没有现场了。第一件是把这次用到的命令按顺序记下来,哪怕只是复制粘贴到一个文本文件里。第二件是确认「恢复」是真的恢复:从外网、不经 CDN、换一个网络各访问一次,看的是完整页面的首屏而不是一个 200 状态码。
很多人吃过这个亏:首页返回 200,但静态资源全部 404,用户看到的是没有样式的白板;或者首页正常但搜索页、登录页依然 502。这些都不体现在「站点是否可访问」的探测里。最省事的验证方式是拿三个不同的 URL——首页、一个内页、一个需要连数据库的页面——各请求一次,三个都 200 才算真的恢复。
记下来的命令序列还有一个额外用途:下一次同类故障发生时,你手上已经有一份被验证过的处理路径,不用再从头推理。这套东西的价值随时间增长,而写它的成本只有十分钟。
九、把告警本身也调成一个能用的东西
告警夜里手忙脚乱,很多时候不是故障多难,而是告警本身做得不好。最常见的是「阈值型告警」发得太细:CPU 超过 80% 报一次、负载超过 4 报一次、某个接口响应超过 1 秒报一次,一天几十条,最后没人看。真正有价值的告警应该满足三个条件:它代表用户已经在受影响、它不会重复发、它附带足以判断的第一手信息。
十、小结
告警本身要少而准、恢复之后要当场留下证据,这两件事和排查顺序一样重要。告警夜里的从容不是天生的,是被流程训练出来的:先定范围,再看现场四件,然后掐时间看日志,动手前留证据,动手时只做最小改动,最后一定要复盘。这套顺序真正的用处是让你在信息最少、时间最紧的时候依然有下一步可做,而不是盯着黑屏的浏览器反复按 F5。