nginx 因 1 秒 DNS 空窗拒绝启动:整机站点宕机 3 小时复盘
早上起来看 home lab 日志,WebSocket 服务凌晨被重启过:
[2026-07-28 06:48:02] Server: Starting server at wss://0.0.0.0:8765
顺手打开博客——打不开,这台机器上其他站也全打不开。整台机器的 nginx 全躺了,已经三个多小时。
现场
uptime 显示机器没重启,是 nginx 单独出事。systemctl restart nginx 一下全恢复了。能 restart 成功说明磁盘配置是好的,那三小时前为什么起不来?翻 unit 日志:
Jul 28 06:48:02 nginx[2766192]: [emerg] host not found in upstream "bones7456.github.io" in /etc/nginx/sites-enabled/luy:75
Jul 28 10:00:10 systemd[1]: Started nginx.service.
host not found in upstream。博客有几个路径反代到 GitHub Pages,nginx 起来时要解析 bones7456.github.io,那一刻没解析出来,直接 emerg 拒绝启动。
时间线:06:45–06:50
把全量日志捞出来:
06:47:50 Starting apt-daily-upgrade.service <== unattended-upgrades 开跑
06:47:57 systemd[1]: Reexecuting ... systemd 255.4-1ubuntu8.16 running in system mode <== systemd 自己被升级了
06:48:02 满机器的服务被批量重启(needrestart 干的)
Stopping named... <== named 被停掉,不再监听 127.0.0.1#53
Stopping nginx...
Starting nginx... (PID 2766192)
└─ [emerg] host not found in upstream "bones7456.github.io"
06:48:03 Starting named... named: listening on IPv4 interface lo, 127.0.0.1#53
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:
[Unit]
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target
nss-lookup.target 是 systemd 表示“域名解析已就绪”的标准锚点,DNS 服务用 Before=nss-lookup.target 挂上去。nginx 早就声明等 DNS 就绪,还是死在 DNS 上。
原因:After= 只在同一个 systemd job transaction 内部编排启动先后,不检查目标服务实时健康状态。needrestart 逐个 systemctl restart,nginx 和 named 属于两个独立 transaction,彼此无排序约束。更糟的是 nss-lookup.target 是 passive target,named 停止时它不会跟着 deactivate,全程 active——nginx 一查依赖满足,启动,一头撞死。
结论反直觉:restart 场景下 After= 基本等于安慰剂,加 After=named.service 防不住。
防线一:让它自己重试
Ubuntu nginx.service 默认没有 Restart=,启动失败一次就永久 failed,没有任何重试。named 只用了 1 秒恢复,只要能重试一次就没事:
sudo mkdir -p /etc/systemd/system/nginx.service.d
sudo tee /etc/systemd/system/nginx.service.d/override.conf <<EOF
[Unit]
StartLimitIntervalSec=600
StartLimitBurst=20
[Service]
Restart=on-failure
RestartSec=10
EOF
sudo systemctl daemon-reload
StartLimit 两行不是可选:systemd 全局默认 10 秒内最多 5 次,配 RestartSec=10 会撞上限流彻底放弃。改成 10 分钟 20 次才扛得住。
drop-in 合并规则:
- 标量指令(
Restart=、RestartSec=、Type=、PIDFile=)覆盖; - 列表指令(
After=、Wants=、Environment=、ExecStartPre=)追加不是覆盖; ExecStart=在Type=forking下只允许一条,直接写会报错,必须先清空再写。
看合并后生效值:
systemctl cat nginx
systemctl show nginx -p After -p Restart -p RestartUSec
这次失败的其实是 ExecStartPre 里的 nginx -t -q,Restart=on-failure 覆盖这种情况。
防线二:别让 nginx 在启动期解析域名
根上毛病是:一个 location 的 upstream 解析不了,整个 nginx 拒绝启动,所有站点陪葬。配置校验 all-or-nothing,一个小站点临时故障放大成全局故障。
解法:给 proxy_pass 用变量——只要含变量,nginx 不再启动期一次性解析,改运行时按需查 resolver。DNS 挂了 nginx 照样能起来,最坏一个 location 返回 502。
变量化 proxy_pass 的坑
原配置 proxy_pass 后面带了路径:
location ^~ /data/shi/ {
proxy_pass https://bones7456.github.io/china-dynasty-timeline/;
proxy_ssl_server_name on;
proxy_set_header Host bones7456.github.io;
}
天真地把域名换成变量 proxy_pass https://$gh_pages/china-dynasty-timeline/; 就掉坑。nginx 规则:
proxy_pass不含变量且带 URI → 前缀替换,把/data/shi/换成/china-dynasty-timeline/。proxy_pass含变量 → 前缀替换彻底失效,URI 被固定成你写的那个。
后果:请求 /data/shi/assets/app.js 原配置 → /china-dynasty-timeline/assets/app.js ✓;变量版 → /china-dynasty-timeline/ ✗。首页看着正常,所有子资源错位,CSS/JS/图片全挂。
正解用 rewrite ... break 手动接管路径映射,配不带 URI 的 proxy_pass。先在 server 块放解析器和变量:
resolver 127.0.0.1 1.1.1.1 8.8.8.8 valid=300s ipv6=off;
resolver_timeout 5s;
set $gh_pages "bones7456.github.io";
location 改成:
location ^~ /data/shi/ {
rewrite ^/data/shi/(.*)$ /china-dynasty-timeline/$1 break;
proxy_pass https://$gh_pages;
proxy_ssl_server_name on;
proxy_ssl_name bones7456.github.io;
proxy_set_header Host bones7456.github.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect https://bones7456.github.io/china-dynasty-timeline/ /data/shi/;
proxy_redirect https://bones7456.github.io/ /data/shi/;
}
实际只动三处:加 rewrite ... break、proxy_pass 去掉路径换成变量、显式加 proxy_ssl_name(默认取 $proxy_host,变量化后写死更稳)。
几个坑
- resolver 里写多个地址是轮询不是主备。反代公网域名两个 DNS 互为冗余;反代本机 named 内网 zone 绝不能这样写,轮询到 1.1.1.1 直接失败,那种 location 得单独配
resolver 127.0.0.1;。 ipv6=off刻意加,变量化后每次请求重新解析,家宽 IPv6 到 GitHub 不一定稳。- 别指望
After=防 restart 场景。 nginx -t通过不代表改对了,只校验语法。
验证
必须测子路径,别只测首页:
curl -sI https://luy.li/data/shi/ | head -3
curl -s https://luy.li/data/shi/ | grep -oE '(src|href)="[^"]+\.(js|css)"' | head -3
三个独立问题任意修掉一个都不会出事:nginx 没有可靠的 DNS 就绪依赖、配置启动期硬依赖 DNS、失败后没有任何重试。三个凑齐,1 秒 DNS 抖动放大成 3 小时全站宕机。
别把系统健壮性寄托在“顺序”上。After=、Before=、network-online.target 描述的是编排意图,不是运行时真实状态。靠得住的只有两种——失败了能自己重试(自愈),和压根不依赖那个东西(免疫)。另外这次是早上顺手看日志才发现的,监控该上还是得上。