服务器上跑着的东西,早晚都会坏。真正决定一个站能不能长期活着的,不是「坏了修得多快」,而是「坏了多久之后你才发现」。绝大多数个人站长栽的跟头,都不是技术难题,而是三件事:不知道故障发生了、知道了不知道该怎么办、按预案做完了却漏了某一样没恢复。这篇文章讲的是把这三件事固定成一份发布前检查单和一份故障预案,让它不依赖当时的记忆和状态。
一、检查单要写「验什么」,不写「做什么」
很多人写过检查单,但大多数检查单最终都变成了摆设。原因几乎都一样:写成了操作步骤。比如「执行 systemctl restart php-fpm」——这一条不是检查项,是动作项。动作做完不代表目的达到了:命令成功了,但进程可能因为配置错误一秒后退出;进程活着,但监听的不是你以为的那个 socket。
检查单的正确形态是每一条都能回答「怎么判断通过」。这要求每一条都自带一个可执行的判据。差的写法是「检查 PHP-FPM 状态」,好的写法是「systemctl is-active php-fpm 返回 active,且 ss -lntp 里能看到 FPM 的 socket 路径」。判据越具体,检查单越不容易被跳过——因为跳过它的人自己也知道,后面出问题一定会被问「你这条验了吗」。
我自己的检查单按四个层次组织,每一层都是上一层失效时的兜底。第一层是进程层:服务在不在跑、监听在不在、端口对不对。第二层是请求层:从本机 curl 本机能拿到 200 且响应里含一个固定字符串,这一层排除了「进程活着但应用报错」的情况。第三层是链接层:经过 Nginx、经过 CDN、从公网访问,这一层排除了反代配置错、证书错、CDN 缓存错。第四层是内容层:随机抽一篇正文页,检查标题、分类、广告位、评论区都在,这一层排除了「首页好着但内页全 500」这种最常见的静态页误导。
四层缺一不可。只验第一层,你会发出「服务正常但用户打不开」;只验第二层,你会错过 CDN 层面的问题;只验前三层,你会漏掉那些由某个插件、某段模板引起的单页异常。每一层的命令都不复杂,四层加起来一次二十秒,值得在每次改动后都跑一遍。
二、把「最近改了什么」当成第一优先级信息
故障排查中最贵的时间花在「回忆」上。三天前改过什么、上周谁调过一个参数、那个 cron 什么时候加进去的——靠脑子记,一定记不全。检查单里必须有一节专门处理这件事,而且顺序上要放在所有技术检查之前。
具体做法是把改动记录下来本身变成一条纪律。我的习惯是:任何会影响线上的动作,在 /root/changes.log 追加一行,格式固定为三部分:时间、改了什么、预期什么效果。看起来像多余的仪式,但它的用处不在平时,在出事那天。当你凌晨两点盯着一个莫名其妙的 502,能一眼翻到「昨天 15:20 把 fastcgi_read_timeout 从 60 改成 10,目的是让超时请求早点失败」,答案就已经在眼前了。
这一条还有一个副作用值得说:当你必须写「预期什么效果」时,很多没想清楚的改动会在写这一行的时候被自己否掉。写不出预期效果的改动,通常是不该做的改动。
三、用 diff 而不是靠眼睛做配置复核
发布前的配置复核,最容易走过场。打开 nginx.conf 扫一眼,「看着没问题」就过了。人的眼睛对格式化的配置文件几乎不具备比对能力,少一个分号、多一层 include、放错一个 location 都看不出来。
可靠的复核是把当前配置和上一次通过验收的版本做一次 diff。这意味着平时就要有版本基线:把 /etc/nginx、/etc/php、/etc/systemd/system 这些关键目录纳入 git 或者定期的 tar 快照。没有基线的话,至少也要在改动前 cp 一份带日期的备份,改完 diff 一次。这一条能抓住绝大多数手误。
# 改配置前先留底,改完只 diff 这一处
cp -a /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak_$(date +%Y%m%d_%H%M)
nginx -t
diff -u /etc/nginx/nginx.conf.bak_20260101_1200 /etc/nginx/nginx.conf | head -40注意脚本里用了 $(date ...) 生成文件名而不是写死,这样每一批备份都在,不会互相覆盖。文件名里带下划线而不是冒号,是为了避免某些老工具不认冒号。这类细节不影响功能,但影响你三个月后还敢不敢在这些目录里操作。
四、健康检查要区分「活着」和「能干活的」
用 curl -I 打首页看 200,是最常见的健康检查,也是最容易骗人的。首页可能是静态 HTML 或者被整页缓存,应用已经连不上数据库了,首页照样 200。这种「活着但发不了力」的状态,比彻底挂掉更难发现。
有效的健康检查至少要探一个真正会碰数据库的地址。如果应用没有现成的健康接口,最省事的做法是在代码里加一个极简的口子:查一次数据库能连上、取一行数据、返回一个固定字符串,不带缓存头。有了它,健康检查就从「进程在不在」升级成「链路通不通」。
# 健康检查的正确姿势:探动态页、看响应体、不缓存
curl -s -o /dev/null -w 'code=%{http_code} time=%{time_total} ' \
-H 'Cache-Control: no-cache' \
"https://www.example.com/health.php?_=$(date +%s)"命令里的 ?_=$(date +%s) 是必须的:前面有 CDN 的时候,不带随机参数的请求大概率命中缓存,你看到的是十分钟前的旧响应。这类「探到缓存」的假阴性,我在第一次给站点做外部监控时踩过,结果是数据库挂了四个小时没人发现。
五、故障预案:先定动作,不要先定原因
预案和检查单是两件不同的事。检查单是事前防止出错,预案是事后有序止血。预案最容易写坏的地方,是把它写成一份「原因排查指南」——遇到 502 先查 PHP-FPM、再查 Nginx 日志、再查磁盘。这类文档在真出事时几乎没用,因为人在紧张状态下读不动分支逻辑。
有用的预案是按「现象 → 先做什么 → 怎么退」三段式写的,而且第一段必须是能在三十秒内执行的动作。比如「站点全站 502」这一条,第一动作不是查日志,是判断范围:本机能通而公网不通,就把问题锁定在 Nginx 之上(CDN 或证书);本机也不通,就看 PHP-FPM 进程数。三十秒的分类动作,能让后面所有的排查都落在正确的那一半上。
第二段「先做什么」必须是可逆的。改配置回滚、切回上一个版本目录、把 CDN 切到维护页——这些都在几十秒内完成而且能随时改回来。反过来,「先把数据库迁移脚本跑了试试」这种动作绝不能出现在预案的前两段。预案的原则是:先让站点可用,再慢慢找原因,两者顺序不能反过来。
六、回滚不是一条命令,是一组状态的还原
这是最容易在预案里被简化掉的一节。写下「回滚:git checkout 上一个 tag」然后收工,看起来很清楚,但代码之外至少还有四样东西可能停在新的状态上:数据库结构、缓存、静态资源、外部配置。
数据库结构是唯一不可逆的一项。如果这次发布执行了加列、改字段类型、删索引,回滚代码只会得到一个「老代码 + 新表结构」的组合,报错概率极高。可行的做法是把每一次带结构变更的发布都明确划分为「可回滚」和「不可回滚」两类,不可回滚的那一类在预案里只允许「向前修复」,并把修复脚本提前写好、在测试机上跑过。这个分类要在发布前就写在检查单上,而不是出了事再判断。
缓存是最容易被忘记的一项,也是最容易造成「回滚了但还是错的」的一项。OPcache 缓存了旧的 PHP 字节码,Nginx 的 fastcgi_cache 缓存了旧响应,CDN 缓存了旧页面,浏览器可能还缓存了旧的 JS。回滚完成后必须有一个明确的清缓存序列,顺序是先清应用缓存,再 reload PHP,再清 Nginx 缓存目录,最后刷 CDN。少一步都会出现「部分用户好、部分用户坏」的诡异现象。
七、把检查单固化成一条命令
写在文档里的检查单,实际执行率不会超过一半。真正能坚持的是把它写成脚本,每次发布后跑一次,输出一份短报告。脚本不需要花哨,一段 for 循环加几个判据就够。
#!/bin/sh
# 发布后自检:四层各一条,任何一条不过就打印 FAIL
fail=0
chk() { if eval "$2" >/dev/null 2>&1; then echo "PASS $1"; else echo "FAIL $1"; fail=1; fi; }
chk "fpm-active" "systemctl is-active --quiet php-fpm"
chk "fpm-socket" "ss -lnt | grep -q ':9000'"
chk "local-200" "test \$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/) = 200"
chk "public-200" "test \$(curl -s -o /dev/null -w '%{http_code}' https://www.example.com/) = 200"
chk "ssl-days" "test \$(echo | openssl s_client -connect www.example.com:443 2>/dev/null | openssl x509 -noout -checkend 604800 | grep -c 'will not expire') = 1"
echo "---"
[ $fail -eq 0 ] && echo "ALL PASS" || echo "SOME FAILED"这段脚本的价值不在技术含量,而在于它把「验一下」从一件需要动脑的事变成一件不需要动脑的事。人在忙、在累、在下班前赶最后一步的时候,会因为「反正有脚本」而真的去跑一次。这就是检查单的全部意义。
八、小结
发布检查单和故障预案,本质上是在把一个人脑子里的经验外部化。检查单要求每一条都有可执行判据,按进程、请求、链接、内容四层兜底,并且必须先确认「最近改了什么」。预案要求按现象分类、第一动作三十秒内完成、前两步必须可逆。回滚要当成一组状态的还原,数据库结构这一项在发布前就要分成可回滚与不可回滚两类。最后,所有这些都要落成一条能直接跑的命令,否则它只是文档里的漂亮话。