一个人维护站点,最大的风险不是技术难题,而是「你记得住的东西太多」。半年后回来改一个配置,你得重新想:这个 cron 是干什么的、这个域名解析在哪家、这台机器的备份放哪、上次那个改动为什么改成这样。
这篇文章讲的不是工具,是一套我跑了三年、确实把我从几次事故里捞出来的习惯:变更管理的最小可行版本。没有工单系统,没有审批流,只有一个 Git 仓库和一张检查单。
一、先认清问题:单人运维的失效模式
团队里靠流程,一个人靠记性。而记性在三种情况下会失效,这三种情况恰好是事故高发期。
第一种是深夜和疲惫时。这时候的判断力下降,最容易做的事是「先改改看」,改完忘了改回去。第二种是半年后的自己。需求来了要恢复某个功能,你打开配置文件,发现里面有三行被注释掉的代码,没有任何说明,你不知道哪一行是当前生效的逻辑。第三种是紧急时刻。站点挂了,你肾上腺素上头,直接在生产上改,改完能跑就收工,没人记录改了什么 —— 然后一周后同样的故障再来一次。
这三种失效模式的共同点是:当时都不觉得有问题,问题都在以后爆发。变更管理要解决的正是这个时间差。
二、最小组件:一个 Git 仓库,就这一个
不需要 Ansible,不需要看板,不需要工单系统。你需要的只是一个 git 仓库,把所有「配置类文件」放进去:
- Nginx 站点配置(vhost 目录)
- systemd unit 文件、定时任务定义
- Docker Compose 文件、应用的 env 模板
- MySQL 的 my.cnf、PHP 的 ini 调整项
- 防火墙规则、fail2ban 配置
- 备份脚本、监控脚本、发布脚本
- 一份记录「这台机器上装了什么、为什么装」的 README
关键是只放配置文件,不放运行时生成的东西。比如别把整个 /etc/nginx 无脑 git add -A,那里面有 fastcgi_params 这类发行版自带的、跟你无关的文件,会让 diff 全是噪音。做法是列一个白名单,只 add 你真正改过的路径。
mkdir -p /root/ops && cd /root/ops
git init
# 逐条添加,不做 add -A
git add nginx/ php/ docker/ scripts/ README.md
git commit -m "initial: 现状快照,未做任何改动"第一次提交的意义很重要:它是「基线」。以后任何 diff 都能回答「相比基线,我改了什么」。注意这次提交要在什么都不改的状态下做,先把现状记录下来,别顺手优化。
三、每次变更一条提交,message 写清楚三件事
提交信息不是写给人看的仪式,是写给「半年后的自己」查线索用的。一条合格的信息要能回答三个问题:改了什么、为什么改、怎么回退。
nginx: 图片目录开放 expires,缓存 30 天
原因:首页图片每次请求都回源,CDN 命中率只有 40%,带宽吃紧。
改动:/etc/nginx/conf.d/static.conf 增加 location ^~ /uploads/
的 expires 30d,并加 immutable。
回退:git revert 本条提交后 nginx -t && systemctl reload nginx
验证:curl -I 看 Cache-Control 是否带 max-age=2592000看着啰嗦,但写一次五分钟,能省掉未来半小时的回忆。特别是「回退」那一行,它逼你在变更时就确认过「这个改动是可逆的」。如果一个改动写完发现回退路径很复杂,那说明这个改动本身需要更谨慎 —— 这是免费的风险提示。
另一个习惯:一次提交只做一件事。别把「升级 PHP」和「改站点配置」放在同一个提交里。出问题时无法只回退其中一半,而且 diff 混在一起根本看不清。
四、变更之前:五问检查单
动手之前默念这五个问题,超过一半的事故能在这里被挡住。
一问:现在这个状态能恢复吗?改配置前先 cp 配置 配置.bak_$(date +%Y%m%d_%H%M),改数据库前先确认有没有当天的备份,改 DNS 前先记下当前解析值。没有退路就不动手,这条没有例外。
二问:会影响到谁?给单个站点改配置,影响一个站;改 php.ini,影响这台机器上所有 PHP 站;改 my.cnf 的 max_connections,影响所有连这个库的应用;改防火墙,可能把自己锁在外面。影响面越大,越要挑时间、越要准备好回滚。
三问:验证方式是什么?改完怎么知道成功了?必须是可执行的检查,不是「打开看看」。比如改缓存头就用 curl -I 看响应头,改 301 规则就用 curl -o /dev/null -w '%{http_code} %{redirect_url}' 看状态码和跳转地址。事先想好验证命令,改完立刻跑,别靠眼睛。
四问:现在这个时间点合适吗?流量高峰不改,深夜不改(困了判断力差),周五下午不改(出问题要带着问题过周末)。最好的窗口是工作日上午、流量平稳、你自己精力充沛且接下来两小时有空。
五问:如果五分钟后站点打不开,我第一个命令是什么?这个问题能瞬间测出你准备是否充分。如果答案是「先看看日志」,那说明回滚方案还不明确。合格的答案应该是具体的一条命令,比如「cp /root/ops/nginx/site.conf.bak /etc/nginx/conf.d/ && nginx -t && systemctl reload nginx」。
五、变更之后:三条验证,一条记录
改完不能只看「没报错」。至少要跑三类验证:
语法与配置层:Nginx 是 nginx -t,PHP 是 php -l 加 php-fpm -t,MySQL 是重启时看 error log,systemd 是 systemctl daemon-reload 后 systemctl status。这一层过了只代表「能启动」,不代表「行为正确」。
功能层:真正请求一次。首页、文章页、后台登录、搜索、一个带参数的 URL。用 curl 就够,重点看状态码和关键响应头,不要只看页面「看起来正常」。
观测层:改完十五分钟、一小时各看一次错误日志。很多配置改动的问题是碎片化的 —— 95% 的请求正常,5% 的请求走进了错误的 location,只有日志里能看到。
tail -n 200 /var/log/nginx/error.log | grep -v 'client intended to send too large'
journalctl -u php-fpm --since "15 min ago" -p err
mysql -e "SHOW GLOBAL STATUS LIKE 'Aborted_connects'"记录那一步就是 commit。改完立刻提交,别拖到晚上「一起提交」。拖着的结果是忘了,或者把几个不相关的改动混成一条。
六、一个真实案例:一次 301 造成的收录下滑
讲个我自己的事故,说明这套东西救在哪。
某天为了统一 www 和裸域,在 Nginx 里加了一条 301 规则。加完 nginx -t 通过,访问首页正常跳转,就收工了。两周后看 GSC,发现收录量掉了三成。
查下来是规则写成了 rewrite ^ https://www.example.com$request_uri permanent;,但这条规则放在了所有站点的 server 块里,包括一个测试站点。测试站的所有页面全部 301 到主站的首页 —— 于是搜索引擎认为测试站的内容是主站的重复页,把测试站的收录清掉了,同时主站因为「大量页面指向首页」被判为软 404。
这个过程能复盘,全靠当时有 commit:diff 一看就知道改了哪个文件、加了哪一行、影响范围是哪些 server 块。如果当时是直接在服务器上 vim 改的,我大概只能从备份里翻,或者靠猜。
这件事之后我加了两条规则:一是改 Nginx 一律先 grep 确认这条规则会命中哪些 server_name;二是任何跳转规则上线后,必须 curl 三个域名确认各自的跳转目标正确,而不只测一个。
七、三个月一次的「清理日」
变更管理不只有加法,还要有减法。一个人维护的机器,半年后一定会积累一堆「不知道还起不起作用」的东西:注释掉的配置、临时加的 cron、试验用的容器、过期的证书文件。
我给自己定的是每季度一个下午,做四件事:
- 看一遍所有 cron 和 systemd timer,每条问「它还在干嘛」,答不上来的先 disable 观察一周,没影响就删
- 看一遍所有运行中的容器和服务,停掉三个月没被访问过的
- 把 /root/ops 里的配置和线上实际做一次 diff,找出「线上改过但没提交」的漂移
- 更新 README 里的「这台机器上有什么」清单
第二项有个具体做法:从访问日志里统计每个站点最近三十天的请求量,长期为 0 的站点,要么是废弃了该清理,要么是出问题了该修,两种情况都需要处理,不该放着。
# 按 host 统计最近三十天的请求数
awk '{print $NF}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20八、不要做的事
这套方法的价值有一半在于「不做某些事」,列几条:
别为了完整而备份整个 /etc。看起来保险,实际上会产生一个几百兆的仓库,里面 99% 是发行版自带文件,每次 diff 都是噪音。你永远不会去读它,于是备份等于没有。
别把密码提交进仓库。哪怕仓库是私有的。密码放在 .env 或者单独的文件里,仓库里只放 .env.example 的模板。曾经有站长把 .env 提交上去,后来仓库被误设为公开,两小时内数据库被扫到。
别在生产上「先试试」。没有本地环境的,至少拿一台最便宜的小机器做测试。一次试验事故的损失,远大于一台测试机的年费。
别把 commit message 写成「update」「fix」。那和不写没有区别。半年后你面对 300 条 update,一条线索都没有。
九、扩展到多台机器
等你有三四台机器,单个仓库会开始混乱。做法不用换成复杂工具,加一层目录就行:
ops/
common/ # 所有机器共用的脚本、模板
host-a/
nginx/
systemd/
README.md
host-b/
...每台机器 clone 整个仓库,但只 symlink 或用脚本部署自己那一份目录。好处是共用部分可以统一改,机器特定的部分互不干扰。再进一步可以用 git 的分支或者 git subtree,但对个人站来说目录分级已经够了。
同步方式我推荐「推」而不是「拉」:本机改完 push,然后在机器上 pull 并 reload。不要搞定时自动拉取 —— 一次错误的提交会在几台机器上同时生效,那就从「一台挂了」变成「全挂」。
十、小结
变更管理的本质是三句话:改之前知道能退回去,改之后知道怎么验证,改完记得为什么改。三件事各对应一个动作 —— 备份、验证、提交,加起来每次不到十分钟。
这十分钟换回来的是什么?是半年后你打开配置文件能看懂,是出事时能在十分钟内定位到是哪次改动,是不必靠记忆维持一个系统。对一个人维护的站来说,这是最便宜的一份保险。
如果现在就动手,建议顺序是:先做基线提交(记录现状,什么都不改),再给下一次变更写完整的 commit message,然后每季度清理一次。三步走完,你已经有了一套能用的流程,不需要任何新工具。