案例 nginx 因 1 秒 DNS 空窗拒绝启动:整机站点宕机 3 小时复盘

2026-09-25 21:01:41

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 规则:

  1. proxy_pass 不含变量且带 URI → 前缀替换,把 /data/shi/ 换成 /china-dynasty-timeline/。
  2. 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,变量化后写死更稳)。

几个坑

  1. resolver 里写多个地址是轮询不是主备。反代公网域名两个 DNS 互为冗余;反代本机 named 内网 zone 绝不能这样写,轮询到 1.1.1.1 直接失败,那种 location 得单独配 resolver 127.0.0.1;。
  2. ipv6=off 刻意加,变量化后每次请求重新解析,家宽 IPv6 到 GitHub 不一定稳。
  3. 别指望 After= 防 restart 场景。
  4. 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 描述的是编排意图,不是运行时真实状态。靠得住的只有两种——失败了能自己重试(自愈),和压根不依赖那个东西(免疫)。另外这次是早上顺手看日志才发现的,监控该上还是得上。

复制全文 生成海报 nginx systemd 故障排查 DNS 运维

推荐文章

程序员茄子在线接单