I am LAZY bones?
AN ancient AND boring SITE

2026年 07月 28日 的归档

记一例 nginx 故障分析

早上起来习惯性瞄一眼 home lab 的日志,看到我那个 WebSocket 服务凌晨被重启过:

重启就重启吧,本来没当回事。结果顺手打开本博客——打不开。又试了下这台机器上跑的其他站,也全打不开。好家伙,整台机器的 nginx 全躺了,而且看时间已经躺了三个多小时。

先看现场

第一件事是确认机器有没有重启过。uptime 一看,没有,机器好好的。那就是 nginx 单独出事了。systemctl restart nginx 下去,唰一下全恢复了。

服务是回来了,但这更让人不安:能 restart 成功,说明磁盘上的配置本来就是好的,那它三个小时前到底为什么起不来?翻 unit 日志:

host not found in upstream。我博客有几个路径(比如这个)是反代到 GitHub Pages 的,nginx 起来的时候要解析 bones7456.github.io,那一刻没解析出来,直接 emerg 拒绝启动。

再一看,06:48 这个时间点太像 Debian 系的 cron.daily 窗口了(/etc/crontab 里是 6:25,加上 anacron 的随机延迟,另外 cron.weekly 是 6:47,更像),我当时第一反应是 certbot 续期把 nginx 重启崩了。结果把那五分钟的全量日志拉出来一看,跟 certbot 半毛钱关系没有。所以还是那句话:别猜,看日志。

1 秒的竞速

journalctl --since "06:45" --until "06:50" 全量捞出来,真凶一目了然:

看明白了:systemd 被 apt 自动升级,触发 needrestart 把机器上几乎所有服务全重启一遍。我这台机器上跑着 BIND 做内网 DNS,于是 named 和 nginx 一起被拖下水。

nginx 在 06:48:02 查 DNS,named 在 06:48:03 才恢复监听。就差 1 秒,nginx 输了这场竞速,代价是三个多小时的全站宕机。

最扎心的是同一批重启里的对比:php-fpm、redis、mariadb,还有我自己那几个 Flask 服务,全都 Started 成功了。唯独 nginx 死了。为什么?因为只有 nginx 在启动阶段需要解析外部域名,别的服务都不需要。

为什么 After=nss-lookup.target 没救下它

这才是这次最值得说的地方。翻开 Ubuntu 自带的 nginx unit:

nss-lookup.target 是 systemd 专门用来表示”域名解析已就绪”的标准锚点,DNS 服务一般会用 Before=nss-lookup.target 把自己挂上去。也就是说,nginx 早就声明过”我要等 DNS 就绪再启动”——然而它还是死在了 DNS 上。

原因在于After= 的语义被普遍误解了:

它只在同一个 systemd job transaction 内部编排启动先后,并不检查目标服务的实时健康状态。

needrestart 是逐个执行 systemctl restart <unit> 的,nginx 和 named 属于两个互相独立的 transaction,彼此之间没有任何排序约束。更要命的是,nss-lookup.target 是个 passive target,named 停止时它并不会跟着 deactivate,全程保持 active——于是 nginx 一查”依赖满足了吗”,满足,启动,然后一头撞死。

所以结论挺反直觉的:在 restart 场景下,After= 基本等于安慰剂。指望靠加一行 After=named.service 来防这类问题,是防不住的。

两道防线

既然顺序编排靠不住,那就换思路:一道事后自愈,一道事前免疫。

防线一:让它自己重试

Ubuntu 的 nginx.service 默认没有 Restart=,意味着启动失败一次就永久 failed,没有任何重试。而这次 named 只用了 1 秒就恢复——只要能重试一次,整件事根本不会发生。

StartLimit 那两行不是可选的:systemd 全局默认是 10 秒内最多 5 次,配上 RestartSec=10 会立刻撞上限流然后彻底放弃。改成 10 分钟内 20 次,才扛得住像样的 DNS 故障。

这里补一个 drop-in 的合并规则,很多人会搞混:

1. 标量指令(Restart=RestartSec=Type=PIDFile=…)是覆盖
2. 列表指令(After=Wants=Environment=ExecStartPre=…)是追加,不是覆盖。想清空得先写一行空赋值 After= 再写新值。
3. ExecStart= 是重灾区:Type=forking 下只允许一条,直接在 drop-in 里写会报错,必须先 ExecStart= 清空再写。

想看合并后到底生效了什么,别看文件,看这个:

还有个细节值得确认:这次失败的其实是 ExecStartPre 里的 nginx -t -q(日志里那句 Control process exited, code=exited, status=1/FAILURE,result 是 exit-code)。Restart=on-failure 是覆盖这种情况的,不用担心它管不到。

防线二:别让 nginx 在启动期解析域名

自愈只是兜底,根上的毛病是:一个 location 的 upstream 解析不了,整个 nginx 拒绝启动,机器上所有站点陪葬。nginx 的配置校验是 all-or-nothing 的,一个小站点的临时故障被放大成了全局故障。

解法是给 proxy_pass 用变量——只要 proxy_pass 里含变量,nginx 就不再在启动期做一次性解析,改成运行时按需查 resolver。这样 DNS 挂了 nginx 照样能起来,最坏只是那一个 location 返回 502。

但这里有个大坑,下面单独说。

变量化 proxy_pass 的那个坑

我原来的配置是这样的,注意 proxy_pass 后面带了路径

如果你天真地把域名换成变量,写成 proxy_pass https://$gh_pages/china-dynasty-timeline/;,就掉坑里了。nginx 的规则是:

1. proxy_pass 不含变量且带 URI → nginx 做前缀替换,把 location 匹配掉的 /data/shi/ 换成 /china-dynasty-timeline/
2. proxy_pass 含变量 → 前缀替换机制彻底失效,URI 被固定成你写的那个。

后果是这样的:

首页看着还挺正常,所有子资源全部错位——CSS、JS、图片全挂。这种”打开一看好像没事”的故障最恶心。

正解是用 rewrite ... break 手动接管路径映射,配一个不带 URIproxy_pass。nginx 文档明确写了:proxy_pass 不带 URI 时,若 URI 已被 rewrite 改写,传递的就是改写后的 URI——正是我们要的。

先在 server 块里放解析器和变量:

然后 location 改成:

实际只动了三处:加 rewrite ... breakproxy_pass 去掉路径换成变量、显式加 proxy_ssl_name(它默认取 $proxy_host,变量化后写死更稳)。proxy_redirect 是字面量匹配,不受影响。查询串也会自动带过去,/data/shi/a?b=1 照常工作。

几个坑

1. resolver 里写多个地址是轮询,不是主备。我这里反代的是公网域名,两个 DNS 都能解析,正好互为冗余;但如果你要反代本机 named 里那些内网 zone,就绝不能这么写——轮询到 1.1.1.1 直接解析失败。那种 location 得单独配 resolver 127.0.0.1;
2. ipv6=off 是刻意加的。变量化之后每次请求都要重新解析,家宽 IPv6 到 GitHub 的连通性又不一定稳,少一个变数是一个。确认 IPv6 走得通再打开。
3. 别指望 After= 能防住 restart 场景,前面说透了。它在正常开机时有用,成本为零可以留着,但不能当防线。
4. nginx -t 通过不代表这次改对了。它只校验语法,rewrite 的路径映射对不对,它一个字都不会告诉你。

怎么验证真的修好了

改完一定要实测,光看配置”觉得对”没用。先验路径映射——必须测子路径,别只测首页

说点本质的

复盘下来,这次事故里有三个独立的问题,任意修掉一个都不会出事:nginx 没有真正可靠的 DNS 就绪依赖、配置在启动期硬依赖 DNS、失败之后没有任何重试。三个凑齐了,一次 1 秒的 DNS 抖动才放大成 3 小时的全站宕机。

而我最想留下的一条经验是:别把系统的健壮性寄托在”顺序”上After=Before=network-online.target 这些东西给人一种”我已经处理好依赖了”的错觉,但它们描述的是编排意图,不是运行时的真实状态。真正靠得住的只有两种东西——失败了能自己重试(自愈),和压根不依赖那个东西(免疫)。分布式系统里讲了很多年的道理,放在单机 systemd 上一样成立。

最后再补一句题外话:这次是我早上”顺手看了眼日志”才发现的,不然还得挂更久。所以监控该上还是得上,healthchecks.io、UptimeRobot 之类的免费额度足够个人站用了。再优雅的配置优化,也不如有人(或者有个机器人)在你睡觉的时候盯着。

全文完。