现象

一台机器上四个子域名,反代到本地四个端口。后端都没起来,按说应该整整齐齐返回 502。实际是:

admin.example.cn   HTTP 502   0.12s
login.example.cn   HTTP 502   0.13s
pay.example.cn     HTTP 502   0.12s
api.example.cn     超时       (curl 等到 timeout 才放弃)

前三个瞬间失败,第四个卡住不动。四份配置是同一个模板生成的,看起来一模一样。

差别

把四份配置里的 proxy_pass 拉出来对比:

proxy_pass http://127.0.0.1:31400;   # admin
proxy_pass http://127.0.0.1:31401;   # login
proxy_pass http://127.0.0.1:31402;   # pay
proxy_pass http://31403;             # api   ← 少了 127.0.0.1:

为什么不是 502,而是挂起

关键在于 nginx 怎么解析 proxy_pass 后面那个 URL。

http://127.0.0.1:31403 —— host 是 127.0.0.1,port 是 31403。nginx 直接连本地回环,端口没人监听,内核立刻回 ECONNREFUSED,nginx 把它翻译成 502。失败得很快,因为根本没出网卡。

http://31403 —— nginx 按 URL 语法解析,冒号前面什么都没有,于是 31403 整个被当成主机名。这是一个合法的主机名(域名规则允许全数字标签),所以 nginx 不会报配置错误,nginx -t 也照样通过。它会老老实实拿去做 DNS 解析。

DNS 查一个不存在的名字,走的是超时重试路径——通常是若干秒一轮、重试几次。请求就卡在那里。等 resolver 最终放弃,nginx 才会返回 502 或 504。

所以这个 bug 的表现是,不是。而慢比错难查得多:你会先去怀疑后端卡住了、怀疑网络、怀疑超时参数,最后才想到去数配置里的字符。

顺带还有一处

同一份配置里,紧挨着的下一行也被污染了:

proxy_set_header Host 31403;         # 其它三个站是 Host $http_host

后端收到的 Host 头是字符串 31403。如果应用要靠 Host 推导回调地址、拼绝对 URL、或者做 CORS 白名单匹配,这会引发一连串更莫名其妙的问题——而且都发生在「请求确实到达了后端」之后,看起来完全不像同一个原因。

根因

这两处是同一个来源。面板类工具(宝塔、1Panel 之类)建反代站点时,界面上有个「目标 URL」输入框,模板会把它同时套进 proxy_passproxy_set_header Host。当时那一栏只填了 31403,没填 127.0.0.1:31403

也就是说:改生成后的配置文件只是治标。只要有人再从面板保存一次那个站点,配置就会被重新生成,bug 原样回来。要根治得去面板把那一栏改对。

怎么避免

  • proxy_pass 后面永远写完整的 scheme://host:port。少写 host 不会报错,只会让你在几个月后花一下午查一个「后端偶尔很慢」的问题。

  • 多站点部署时,横向对比同类配置比逐份细读有效得多:

    grep -h "proxy_pass" /path/to/vhost/*.conf | sed 's/^\s*//' | sort -u
    

    一眼就能看出哪一行长得不一样。

  • 用脚本或模板统一生成 vhost,别让人手填能被拼进配置的自由文本。

  • 排障时,区分「快速失败」和「超时」。前者通常是连接被拒(端口没人听、防火墙 REJECT),后者通常是解析卡住、路由黑洞、或者防火墙 DROP。这两类的排查方向完全不同,先分清能省很多时间。