Buzeli
buzeliSoluções Digitais
事故

WAF健康但站点返回500:当nginx后端消失而反向代理变成盲人

发布于 2026年5月2日

症状:基础设施表面健康却返回500

大约08:09 UTC服务器重启。到13:35 UTC,将近五个半小时后,问题仍在持续:一个高流量内容门户的所有访客都收到HTTP 500。WAF和nginx容器都没有触发任何告警——两者都在运行。

服务器架构在同一台主机上运行两层不同的nginx:

复制
# 请求流程
CDN (Akamai) → WAF容器 (network_mode: host, 端口443)
             → proxy_pass https://172.28.5.61:443
             → nginx后端 (bridge网络, IP 172.28.5.61)
             → fastcgi → PHP-FPM

WAF运行在network_mode host模式下——直接使用虚拟机的网络接口,能看到Akamai边缘节点的真实IP,并将请求代理到位于内部bridge网络(172.28.5.61)上的nginx后端。PHP-FPM也运行在network_mode host模式下,以便与nginx后端共享Unix socket。

当外层健康而内层悄然失败时,传统监控无法检测到这一点。WAF的健康检查返回200。消失的内层对于从外部监控的人来说是不可见的。

诊断:0字节的vhost文件

第一步检查WAF的error.log,日志信息很直接:

复制
connect() failed (111: Connection refused) while connecting to upstream https://172.28.5.61:443/

WAF试图将请求代理到172.28.5.61:443的nginx后端,但收到了connection refused。nginx容器在运行——进程存在。但nginx没有为那个特定域名监听443端口。

检查vhost文件后,原因变得清晰:

复制
# 在主机上检查vhost文件大小
ls -la /home/developer/webserver/sites-enabled/blog.cliente-exemplo.com.br

# 输出:
-rw-r--r-- 1 root root 0 Apr 13 08:09 blog.cliente-exemplo.com.br

零字节。vhost配置文件完全为空。当nginx以空的vhost配置文件重新加载时,它根本不会创建对应的server block——没有listen 443,没有proxy_pass,什么都没有。nginx对所有其他已配置的域名继续正常工作。但对于这个特定域名,443端口根本不存在。

WAF在收到该域名的Akamai请求时,尝试配置的proxy_pass操作,却发现端口已关闭。Akamai收到connection refused,并在访客浏览器中呈现为ERR_READ_ERROR错误。

vhost为何变空

服务器在08:09 UTC重启了。vhost备份文件存在于服务器的主目录中——是在之前的维护会话中创建的:

复制
# 主机上可用的备份
/home/developer/webserver/blog.cliente-exemplo.com.br.bak.20260319       # nginx vhost
/home/developer/webserver/blog.cliente-exemplo.com.br.waf-bak.20260319   # WAF vhost

最可能的假设是:重启期间或之前的某次维护操作用空内容覆盖了vhost文件——可能是意外截断,也可能是部署中途失败。对应的WAF文件(waf-enabled/blog.cliente-exemplo.com.br)完好无损——这解释了为何WAF本身健康且能在443端口接受连接,但在尝试将请求传递给后端时失败。

健康的WAF掩盖了损坏的后端。从CDN的角度来看,源站在响应——只是返回502或connection refused而非200。对于外部监控来说,源站是WAF,而不是nginx后端。

修复方案:从备份恢复

修复方案很直接——从备份恢复vhost并重新加载nginx后端:

复制
# 从备份恢复nginx vhost
cp /home/developer/webserver/blog.cliente-exemplo.com.br.bak.20260319    /home/developer/webserver/sites-enabled/blog.cliente-exemplo.com.br

# 确认文件不再为空
wc -c /home/developer/webserver/sites-enabled/blog.cliente-exemplo.com.br
# 预期输出:类似"4321 /home/developer/webserver/sites-enabled/blog.cliente-exemplo.com.br"

# 重新加载前测试nginx配置
docker exec nginx nginx -t

# 重新加载nginx(无停机)
docker exec nginx nginx -s reload

13:35 UTC重新加载后,网站恢复返回200。WAF开始从后端收到有效响应。访客不再看到错误。

架构经验:从上一层的视角监控每一层

这次事故暴露了多层架构中的一个经典盲点:健康检查监控最外层,但不验证内层是否正常工作。

在下面的示意图中,每个箭头代表一个可能悄然失败的依赖:

复制
# 依赖链——每一层都可能在上一层不知情的情况下失败
[外部监控] → 检查WAF是否在443端口响应
[WAF]      → 检查nginx后端是否在172.28.5.61:443响应
[nginx后端] → 检查PHP-FPM是否处理fastcgi
[PHP-FPM]  → 检查数据库是否响应

# 实际被监控的内容:
[外部监控] → WAF 443端口:正常(WAF响应,但对该域名返回500)

# 应该被监控的内容:
[WAF]      → nginx后端:失败(172.28.5.61:443连接被拒绝)

关键点在于:监控需要从WAF的角度验证后端健康状况——而不仅仅是从外部角度验证WAF的健康状况。如果WAF返回自己的错误页面,对WAF IP执行curl检查可能返回200,而所有用户都在收到500。

如何在事故发生前检测到这种模式

两个简单检查,可以在访客报告之前检测到问题:

复制
# 1. 检查所有vhost文件大小是否非零
find /home/developer/webserver/sites-enabled/ -maxdepth 1 -type f -empty
# 如果有文件返回:告警——空vhost

# 2. 检查nginx后端是否在预期端口监听
docker exec nginx ss -tlnp | grep :443
# 如果本应激活的域名返回空:问题存在

# 3. 直接测试内部代理(绕过WAF)
curl -sk -H "Host: blog.cliente-exemplo.com.br" https://172.28.5.61/ -o /dev/null -w "%{http_code}
"
# 如果返回000或connection refused:nginx后端未在监听

直接对nginx后端内部IP执行curl检查特别有用,因为它完全模拟了WAF接收请求时的操作——并以同样的方式失败,让问题立即可见。

事故后的改进措施

备份文件存放在sites-enabled之外: .bak文件绝不能放在sites-enabled或waf-enabled目录内——nginx会加载这些目录下的所有文件,并产生conflicting server_name警告。

内部后端监控: 添加了直接对172.28.5.61进行curl检查并带Host头的健康检查——在WAF开始向访客返回connection refused之前,就能检测到缺失的listen 443。

部署时检查空vhost: 任何写入sites-enabled文件的流水线在重新加载nginx之前必须验证文件大小非零。

分层基础设施提高了韧性——但也增加了故障点与检测点之间的距离。每一层都需要对它所依赖的层的健康状况保持可见性,而不仅仅是自身的健康状况。