Nginx 返回 504 时,错误页只说明网关没有在期限内拿到可用响应,不能直接证明网络不通,也不能证明应用已经崩溃。尤其当错误日志写着 while reading response header from upstream 时,TCP 连接通常已经建立,故障发生在等待上游响应头的阶段。此时盲目重启 Nginx 或把所有超时统一放大,可能只会延长用户等待并堆积更多在途请求。
适用范围与核心判断
本文适用于 Nginx 作为 HTTP 反向代理,客户端收到 504,且 Nginx 错误日志包含 upstream timed out ... while reading response header from upstream 的场景。V2CE 在 Docker 24.0.7、Alpine 3.20 与 Nginx 1.28.3 中构造了两个使用同一响应脚本、都会在接受连接后等待 3 秒才发送响应头的隔离同构上游实例。
短阈值路径在 proxy_read_timeout 1s 下返回 504,对照路径在 proxy_read_timeout 5s 下返回 200。这只能证明本次实验的阈值短于上游响应时间,不代表生产环境应该机械改成 5 秒或更长。生产修复必须先解释上游为什么变慢,以及延长等待是否会超过容量和业务时限。
第一步:从故障请求保存端到端证据
从用户实际经过的入口发起一次有边界的请求,记录状态码、DNS、连接时间、首字节时间和总耗时。请求应携带可关联的请求 ID;若业务已经支持 trace ID,优先使用现有字段,不要临时把敏感参数写入公开日志。
curl -sv --connect-timeout 3 --max-time 10 \
-H 'X-Request-ID: diagnose-20260812-001' \
https://example.com/api/health -o /dev/null
# 仅查看最近时间窗,不要无边界导出整份生产日志
sudo journalctl -u nginx --since '10 minutes ago' --no-pager
sudo tail -n 100 /var/log/nginx/error.log
保存 Nginx 日志中的 request、upstream、错误阶段和时间。不要只截图“504 Gateway Time-out”,因为错误页没有上游地址,也无法区分连接、发送请求、读取响应头或读取响应体阶段。
第二步:区分连接超时、拒绝连接与读取超时
proxy_connect_timeout 约束建立上游连接的阶段;proxy_read_timeout 约束读取上游响应时相邻两次读取之间允许的空闲时间,不是整个响应传输的统一总时限。日志阶段决定后续检查方向:
connect() failed (111: Connection refused):目标地址可达,但对应端口没有进程接受连接,优先检查监听地址、端口、进程和容器网络。upstream timed out ... while connecting to upstream:连接没有在期限内建立,检查路由、防火墙、网络策略、地址解析和连接队列。upstream timed out ... while reading response header from upstream:连接已经走到读取阶段,优先检查应用处理、线程池、事件循环、数据库锁等待和下游依赖。
在 Nginx 所在的同一网络命名空间直接请求日志里的上游,避免从宿主机成功就误判容器内路径正常:
# 宿主机或 systemd 服务
ss -lntp
curl -sv --connect-timeout 2 --max-time 8 http://真实上游地址/health
# 容器部署:先核对生效配置,再从代理容器重放
docker exec nginx-proxy nginx -T
docker exec nginx-proxy wget -S -O- http://应用服务名:端口/health
第三步:在应用侧解释首字节为什么变慢
找到同一请求 ID 在应用访问日志、APM 或 trace 中的记录,拆分排队时间、业务执行时间、数据库时间和外部调用时间。如果健康端点很快而特定业务请求超时,网络连通性通常不是主要矛盾;应继续检查慢查询、锁、连接池、线程池、CPU 饱和、事件循环阻塞或下游重试。
同时比较 Nginx 的 $upstream_connect_time、$upstream_header_time 和 $upstream_response_time。连接时间很短而响应头时间接近阈值,支持“连接成功、上游迟迟没有产生响应头”的判断。只有单次样本仍不够,应检查 P95/P99、并发量和超时发生率,避免把偶发冷启动当成持续容量问题。
第四步:评估是否允许调整超时
延长 proxy_read_timeout 会让慢请求占用连接更久。若上游吞吐已经饱和,单纯延长等待可能放大排队、内存占用和级联超时。调整前至少确认:业务的用户时限允许等待;上游能够在新上限内稳定完成;并发连接和工作线程有余量;客户端、负载均衡器与下游的超时顺序一致;请求重试不会对非幂等写操作造成重复副作用。
若慢请求本身异常,优先修复慢查询、锁等待、外部依赖、工作队列或输入上限。若耗时是经过确认的正常业务特征,可以只在对应 location 设置有边界的阈值,而不是修改全局默认值。
最小修复与安全发布
配置修改前保存当前生效配置,并在隔离或预发布环境复现同一请求。语法检查通过后才平滑重载;重载不会替代业务侧容量验证。
sudo nginx -T > /var/tmp/nginx-effective-before.conf
sudo cp /etc/nginx/conf.d/api.conf /etc/nginx/conf.d/api.conf.before
# 只修改确认需要的 location,然后验证并平滑重载
sudo nginx -t
sudo systemctl reload nginx
发布后重复原请求,确认状态码、首字节时间、Nginx 错误日志、应用完成记录和并发指标均符合预期。不要用“首页 200”代替对故障 API 的验证,也不要仅以 504 消失作为成功标准;如果所有请求只是更晚才失败,问题并没有解决。
复核与回滚
若新阈值导致连接堆积、上游队列增长、P99 恶化或客户端先超时,应恢复备份配置,执行 nginx -t,再平滑重载。回滚后重复同一请求和指标检查。应用修复则应使用上一版本镜像或既有发布系统回滚,并保留变更前后的日志、trace 和指标快照。
V2CE 复现实验记录
实验中的 Alpine TCP 上游先接受连接,等待 3 秒后发送 8 字节正文 slow ok。短阈值代理配置 proxy_connect_timeout 1s 与 proxy_read_timeout 1s;请求返回 504,错误日志明确记录 upstream timed out (110: Operation timed out) while reading response header from upstream。
对照代理只把读取阈值改为 5 秒,对应的隔离同构上游返回 HTTP 200 和 slow ok,对照代理日志没有 error 级记录。两条路径使用相同镜像、响应脚本、Docker 网络类型和 3 秒延迟,但使用独立上游实例避免前一次超时干扰后一次请求,因此差异能够归因于读取阈值与首字节延迟的关系。原始响应、Nginx 日志、Compose 状态和验证脚本保存在 labs/nginx-504-upstream-timeout/evidence。