原创

Nginx 502 Bad Gateway 怎么排查:从错误日志定位上游故障

Nginx 返回 502 时,浏览器只告诉你“网关没有拿到有效的上游响应”。它没有告诉你 Java 进程是否还活着、代理端口是否写错,也没有证明请求已经进入业务代码。把 proxy_read_timeout 改到十分钟,通常解决不了连接被拒绝的问题。

排查应当沿着一次请求走:确认命中了哪台 Nginx、找出实际上游地址,再从 Nginx 所在环境直接访问那个地址。只有定位到失败的阶段,修改配置才有意义。

先把故障固定到一条请求

下面的命令按 Linux 上常见的 Nginx 安装布局书写,日志路径以实际配置为准。先记录失败 URL、发生时间、状态码,然后在服务器执行:

curl -i --connect-timeout 3 --max-time 8 http://127.0.0.1/api/health \
  -H 'Host: app.example.test'
sudo nginx -t
sudo nginx -T 2>&1 | less
sudo tail -n 100 /var/log/nginx/error.log

Host 不能省略。有多份 server 配置时,直接访问 IP 可能进入默认站点,得到的 200 与用户访问的那个域名并无关系。nginx -T 能看到包含进来的配置;只打开 /etc/nginx/nginx.conf,容易漏掉真正生效的站点文件。配置输出可能包含内部地址,排障记录应当先做脱敏。

如果站点经过负载均衡,还要确认请求是否命中正在检查的实例。同一域名后面的两台机器配置不一致,会表现为偶发成功、偶发失败。可以从日志里的请求时间、路径和实例信息核对,不能拿另一台实例的成功结果结束调查。

接下来找到该 location 的 proxy_pass。假设它是 http://127.0.0.1:18381/,就在 Nginx 所在主机执行:

curl -i --connect-timeout 2 --max-time 5 http://127.0.0.1:18381/health
sudo ss -lntp 'sport = :18381'

Nginx 跑在容器里,就进入那个容器或它的网络环境检查。容器中的 127.0.0.1 指向容器自身;宿主机上能访问 Java 的 8080 端口,不能证明 Nginx 容器也能访问。两个 Compose 服务通常通过服务名与容器端口通信,例如 http://app:8080,而不是照抄宿主机映射出来的端口。

error.log 里最有用的是失败阶段

看到 connect() failed ... while connecting to upstream,重点检查监听端口、地址和进程状态。Linux 中常见的 Connection refused 表示连接被拒绝;应用没启动、只监听了另一个地址、端口填错,都比“接口太慢”更值得先查。此时增加读取响应的超时不会让端口开始监听。

upstream timed out ... while connecting to upstream 则发生在建立连接时。检查两端路由、防火墙、安全组和网络策略,还要确认监听地址是否对来访环境可达。不要仅凭一次 ping 成功判断 TCP 端口正常,ICMP 与目标端口的访问规则可能不同。

如果失败阶段变成 while reading response header from upstream,连接通常已经建立,上游没有及时给出完整响应头。此时再看业务日志、数据库慢查询、线程池和外部调用。应用可能在等连接池,也可能卡在远程服务,Nginx 本身未必缺少资源。这类超时常见的客户端结果是 504,不能把所有代理故障都叫成 502。

还有两种容易被“重启一下”掩盖的情况:upstream prematurely closed connection 说明上游提前断开,需要查看进程退出、异常终止和协议使用;upstream sent too big header 指向响应头超过代理可用缓冲,常见诱因包括过大的 Cookie 或响应头。后者应先找到具体头字段,删除无用内容,再评估缓冲配置,不能拿它当作所有 502 的通用修复。

如果上游地址写的是域名,还要区分启动时解析失败与运行时解析策略。域名无法解析可能直接导致配置检查或重载失败,并不必然表现为某次请求的 502。先确认当前工作进程究竟使用了哪份配置。

用一套小服务把 200、404、502、504 分开

这个复现只依赖 Python 3.10 以上和 Nginx,便于在测试机确认日志的含义。开始前确认18381、18390端口未被占用,若有已有服务,就同时更换代码和配置中的对应端口。新建 backend.py:

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import time


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/slow":
            time.sleep(3)
        status = 200 if self.path in ("/health", "/slow") else 404
        payload = json.dumps({"path": self.path, "status": status}).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(payload)))
        self.end_headers()
        try:
            self.wfile.write(payload)
        except (BrokenPipeError, ConnectionResetError):
            pass


if __name__ == "__main__":
    ThreadingHTTPServer(("127.0.0.1", 18381), Handler).serve_forever()

在第一个终端运行 python3 backend.py。另开终端,在一个独立目录中建立日志目录和 nginx.conf,不会占用现有站点的 80 端口:

mkdir -p logs

配置文件完整内容如下:

pid logs/nginx.pid;
error_log logs/error.log info;
events {}
http {
    log_format route '$request status=$status upstream=$upstream_addr '
                     'up_status=$upstream_status connect=$upstream_connect_time '
                     'response=$upstream_response_time total=$request_time';
    access_log logs/access.log route;
    server {
        listen 127.0.0.1:18390;
        location /api/ {
            proxy_pass http://127.0.0.1:18381/;
            proxy_http_version 1.1;
            proxy_connect_timeout 1s;
            proxy_read_timeout 1s;
        }
    }
}

从配置所在目录启动这份实例:

nginx -p "$PWD/" -c nginx.conf -t
nginx -p "$PWD/" -c nginx.conf
curl -i http://127.0.0.1:18390/api/health
curl -i http://127.0.0.1:18390/api/missing
curl -i http://127.0.0.1:18390/api/slow

三个请求分别用于观察正常响应、业务路径不存在和等待响应头超时,预期状态为 200、404、504。/api/ 在这份配置中会被 proxy_pass 的 / 替换,因此后端收到 /health,而不是 /api/health。路径错误得到了上游的正常 404 响应,增加超时也不会把它变成 200。

再回到第一个终端,按 Ctrl+C 停掉 Python 服务,重新请求 /api/health。现在预期得到 502,并在 logs/error.log 中看到连接被拒绝。此时响应头超时仍是同一个数值,故障原因却已经变成“没有服务接收连接”。

实验结束后只停止刚才这份实例:

nginx -p "$PWD/" -c nginx.conf -s quit

实际应用不必把超时设成一秒,这个值只是为了缩短复现等待。proxy_read_timeout 约束的是两次读取操作之间的等待时间,并非一条请求从开始到结束的总预算。接口允许执行多久,还需要应用和下游各自有明确的时间限制。

修复以后,用同一条访问链验收

端口写错就修正上游地址;应用没启动就检查服务启动日志与失败原因;网络不通就核对对应环境到目标端口的连通性;上游响应慢就追到具体等待点。修改之后先运行 nginx -t,通过后再重载,并重放最初失败的请求。

验收不能停在“首页能打开”。至少应确认原 URL 返回了正确业务内容、日志里的实际上游与预期一致、错误没有转移成新的超时或重试风暴。Nginx 代理成功返回一个登录页,也可能只是请求被错误地转发到了另一套服务。

如果为了排查暂时延长超时,要把它作为临时措施记录下来。让工作线程、数据库连接和客户端一起多等几分钟,可能只是把一个清晰的失败拖成更难恢复的积压。

正文到此结束
评论插件初始化中...
Loading...