一次 DNS 抖动放倒全站:41 小时静默宕机复盘
昨晚我往博客推一篇新文章,验证链接的时候,返回的是 521。
521 是 Cloudflare 的黑话,翻译过来:我还活着,源站死了。
登上服务器,ps -C nginx,进程不存在;ss -tlnp,80 和 443 一个都没监听。systemd 日志写着:10 月 1 日早上 6 点 25 分起,它就再没起来过。
41 个小时。全站躺平了 41 个小时,没有任何人知道。
包括我。
更荒诞的是凶手的身份:不是博客的配置,是同一台服务器上另一个站点的一行静态反代。
定位:从 521 到真凶,journalctl 一击即中
521 的好处是方向明确。Cloudflare 活着,说明问题只剩两种:进程挂了,或者端口没开。这一查,两个都中了。
为什么起不来?journalctl 最后一屏写得明明白白:
Oct 01 06:25:12 systemd[1]: Stopping nginx.service...
Oct 01 06:25:13 nginx[3081690]: [emerg] host not found in upstream
"tiles.basemaps.cartocdn.com" in /etc/nginx/sites-enabled/wen.pmparker.net:53
Oct 01 06:25:13 nginx[3081690]: nginx: configuration file test failed
翻译一下这条死亡链。10 月 1 日清晨,某个系统事件触发了 nginx 重启。触发者是谁我到现在没查清,可能是内核更新,可能是宿主机维护,日志里只留了一句"Stopping"。
重启之前 nginx 要做配置测试,测试时对"tiles.basemaps.cartocdn.com"这个地图瓦片 CDN 的公开域名做 DNS 解析。那一刻,解析失败。
[emerg] 是 nginx 的最高级别错误:不是这条规则降级,是整份配置测试失败,nginx 拒绝启动。
更糟的是这个失败的复利效应:此后每一次重启都会重演同一条死亡链,读配置、查 DNS、失败、不启动。清晨的一次 DNS 抖动,变成了永久性的循环卡死。
凶手机制:配置加载时解析,不是运行时解析
这行的坑藏在 nginx 的语义里,出事前的写法长这样:
location /geocdn/ {
proxy_pass https://tiles.basemaps.cartocdn.com/;
proxy_set_header Host tiles.basemaps.cartocdn.com;
}
proxy_pass 直接写域名(静态写法),这个域名的 DNS 解析发生在配置加载时,也就是启动和 reload 的那一瞬间。那一刻解析不出来,nginx 的选择不是"这条规则先跳过",而是整份配置报废。
修复后的写法:
location /geocdn/ {
resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off;
set $geocdn_host "tiles.basemaps.cartocdn.com";
rewrite ^/geocdn/(.*)$ /$1 break;
proxy_pass https://$geocdn_host;
proxy_ssl_server_name on;
proxy_ssl_name $geocdn_host;
proxy_set_header Host $geocdn_host;
}
三个改动,各管一件事。resolver 声明"运行时去哪查 DNS";上游改成变量,解析从配置加载推迟到每个请求,查到的结果缓存 300 秒;rewrite 把 /geocdn/x 改写成 /x 再 break,保持"剥前缀转发"的老语义一寸不移。
一行写法的差距,是启动时生死和运行时降级的差距。DNS 再抖,最坏是单条请求 502,nginx 该活还活。
顺手扫了全服务器:同类的静态上游还有一处,翻译接口的反代,同一个雷。一起拆了。
排查路上的三个坑,比结论更值钱
坑一:grep 排雷扑空。我想扫全服务器还有几处同款静态上游,对着 sites-enabled 目录 grep,零命中。以为没有雷,差点收工。后来才发现它是个符号链接,grep -r 不跟随,真正的雷都藏在 sites-available 的真身里。在 nginx 里做全局排查,永远打 sites-available。
坑二:跨机器的 /dev/shm。我的部署通道要经过一台中转机。修复包明明传到了中转机的 /dev/shm,到目标机上解压却报"文件不存在",查了半天才发现:两台机器各自的 /dev/shm,是两个世界。多机操作时,"这个临时文件现在在哪台机器上",值得每次停一秒确认。
坑三:假自测。修完我在源站本地 curl http://127.0.0.1/ 验证,拿到一页 301 跳转,那是 HTTP 强跳 HTTPS 的配置,本地回环测试永远只能看到跳转页。我拿着跳转页做内容比对,差点得出"还没修好"的反结论。验证要打最终协议,或者 curl 加 -L 跟到底。
还有一个插曲。修完之后首页刷新出来还是旧内容,我一度怀疑是 nginx 的 open_file_cache 在供陈旧文件句柄,翻出配置一看确实开了。最后发现是 Cloudflare 对首页的边缘缓存,列表页是 DYNAMIC 不缓存,首页有缓存窗口。同一个 CDN,不同路径,两种行为。乱怀疑之前,先把"我以为的链路"和"真实的链路"逐段对齐。
为什么 41 小时没人知道
| 时间(10 月) | 事件 |
|---|---|
| 1 日 06:25:12 | 系统事件触发 nginx 重启(触发者未查明) |
| 1 日 06:25:13 | DNS 解析失败,配置检查 emerg,nginx 拒绝启动 |
| 1 日 ~ 2 日 | 全站 521,无告警,无人发现 |
| 2 日 23:15 | 推新文章做验证时撞见 |
| 2 日 23:40 | 配置修复,完整重启,全站恢复 200 |
复盘到这一层,技术原因已经清楚了。但真正让我不舒服的是另外两层。
第二层:爆炸半径。这台服务器上一个 nginx 进程、一份总配置,跑着博客、观澜看板等三个站。观澜的地图反代抖了一下,博客陪葬。共享总配置意味着,爆炸半径不是由你最重要的站点决定的,是由同一份配置里最脆的那一行决定的。
第三层:发现方式。静态站没有心跳监控,没有宕机告警,它挂了和没挂,在外表上没有任何区别,Cloudflare 照常应答,只不过应答的是 521。这次能发现,是因为我半夜推文章必须验证链接,手工部署流程误打误撞当了 41 小时里第一次报丧人。
修完之后我做了两件事。两处静态反代全部改成 resolver 加变量的写法,然后完整重启(不是 reload,是 restart)验证干净拉起:瓦片代理 200,翻译代理返回真实响应,主站 200。第二,把 uptime 监控排上了日程。诚实地说,还没做完,这是下一个要还的债。
最后
三件事带走。
凡是"启动时依赖外部服务"的配置都是地雷。nginx 静态反代如此,任何服务的初始化依赖都如此:能推迟到运行时降级的,就不要留在启动时爆炸。
共享进程的托管架构里,爆炸半径由最脆的那份配置决定。给别人的域名做反代之前,先想清楚它抖一下,你会死几个站。
静态站最容易被省掉的就是监控,而它恰恰是唯一会报丧的东西。5 分钟知道和 41 小时知道,是两种完全不同的运营。
前一天我刚在博客里写过"核查要挂在事件上,不挂在记忆上"。这回把同一句话连本带利补给了自己。
你的服务器最长静默过多久?后来装报丧机制了吗,来聊聊。
← 返回博客列表 · 相关阅读:被平台更新一夜打回原形 · 防御性 API 集成