Nginx 与网关排障 — 502/504/499/413、流式与重写
nginx 站在客户端与应用之间,它报的错很少是它自己的错:502 是上游没接住,504 是某个超时到点了,499 是客户端先走了。把责任判给谁,决定了你下一步该查哪一层。
本手册按症状 → 判据 → 根因 → 最小修改 → 验证组织,覆盖九类高频故障。全部结论都以「先看日志字段、再改配置」为前提。
目录
- 何时使用
- 参数与占位符
- 现场采集脚本
- 症状索引
- 心智模型:三层超时与两条链路
- 模式 1 — 502:上游没接住
- 模式 2 — 504:到底是谁的超时
- 模式 3 — 499:客户端先走了
- 模式 4 — 413 与大文件上传
- 模式 5 — SSE/流式被缓冲
- 模式 6 — upstream keepalive 没生效
- 模式 7 — 尾斜杠与路径重写
- 延伸阅读(日志字段 / sub_filter / SNI)
- 错误处理约定
- 失败模式与恢复
- 完整走查(端到端样例)
- 不适用场景
- 反模式与常见问题
何时使用
自动触发:用户描述与下表场景相关时直接启用,不需要点名本技能。
| 用户会这么说 | 实际场景 |
|---|---|
| 「nginx 一直 502,后端明明活着」 | 上游连接层失败(模式 1) |
| 「偶尔 504,后端说没超时」 | 超时归属判断(模式 2) |
| 「日志里一堆 499,用户说没响应」 | 客户端/LB 先断开(模式 3) |
| 「传大文件 413,改配置没用」 | 请求体大小与缓冲(模式 4) |
| 「SSE 一次性到达 / 流式卡住」 | 缓冲与超时(模式 5) |
| 「改了 upstream keepalive 没提速」 | keepalive 配置不完整(模式 6) |
| 「访问 /api 被 301 到怪地址」「路径丢了前缀」 | proxy_pass 路径语义(模式 7) |
| 「sub_filter 以前好使,现在不替换了」 | 上游改版或 gzip(模式 8) |
| 「反代到 HTTPS 上游报证书错」 | SNI 与校验(模式 9) |
手动调用:对话里说「用 Nginx 与网关排障处理」,或直接用 @ 引用本技能(平台的完整 slug 可能带作者后缀,不必写全名)。
不适合:应用自身的异常(把 500 甩给网关)、Ingress 控制器(ingress-nginx 有额外注解语义)、CDN/WAF 侧规则,见文末。
参数与占位符
动手前先替换下表占位符;未替换就跑等于用错环境。
| 占位符 | 含义 | 从哪里取 |
|---|---|---|
<host> |
server_name | nginx -T | grep -n server_name |
<upstream> |
上游组名 | nginx -T | grep -n 'upstream ' |
<port> |
监听或上游端口 | nginx -T | grep -n listen |
现场采集脚本
python scripts/nginx_triage.py --json 一条命令采完版本、nginx -T 生效配置、监听端口、error.log 的 upstream/超时行、access log 状态码分布与上游证书到期时间;proxy_pass / upstream 里的 IPv6 地址([::1]:8080)能正确解析,不给 --upstream-host 时自动从配置的 https/443 上游挑证书检查目标;输出 findings(证据 + 最小动作)与 raw(原文)。--self-test 不碰系统,用内置样本验证判据。
| findings 的 code | 对应本节 |
|---|---|
E-ENV-01 |
模式 1(502:connect() failed / prematurely closed) |
E-ENV-03 |
模式 2、3(504 upstream timed out、499 占比) |
E-ENV-04 |
模式 4(413 client intended to send too large body) |
E-ENV-06 |
模式 9(上游证书 30 天内到期,见 references/subfilter-and-tls.md) |
症状索引
| 症状 | 先看 | 关键判据 |
|---|---|---|
| 502 Bad Gateway | 模式 1 | $upstream_status 为空 = 没连上;有值 = 上游回了非预期响应 |
| 504 Gateway Time-out | 模式 2 | $upstream_response_time 是否 ≈ 某个 timeout |
| 499 | 模式 3 | 只有 nginx 有 499;$request_time ≈ 客户端放弃时刻 |
| 413 Request Entity Too Large | 模式 4 | 报错来自 nginx 还是上游 |
| 流式/SSE 变成一次性 | 模式 5 | 响应头是否带 X-Accel-Buffering: no、proxy_buffering 是否 off |
| 上游连接数暴涨、TIME_WAIT 多 | 模式 6 | upstream 块是否配了 keepalive 且请求头 Connection 被清空 |
| 301/302 到错误主机或端口 | 模式 7 | proxy_pass 带不带 URI、server_name_in_redirect |
| 页面元素不替换 | 模式 8 | 上游是否 gzip、sub_filter_types、pattern 是否过期 |
| 证书不匹配 / SNI 报错 | 模式 9 | proxy_ssl_server_name、proxy_ssl_name |
心智模型:三层超时与两条链路
三层超时(从外到内,任何一层先到点,用户看到的就是那一层的错):
- 客户端 / 负载均衡 / CDN 的超时 → 表现为 499(客户端断开)或 LB 自己的 5xx
- nginx 的
proxy_connect_timeout/proxy_send_timeout/proxy_read_timeout→ 表现为 504 - 上游应用自己的超时 → 通常表现为上游返回 504/500,
$upstream_status有值
判据一句话:看 $upstream_status 和 $upstream_response_time。为空且时间接近 connect_timeout,是连不上;有值且时间接近 read_timeout,是上游太慢;两者都正常但请求仍失败,往更外层找。
两条链路:
- 连接链路:client → nginx → upstream。每一跳都有建立、传输、关闭三种状态,502 多发生在建立或关闭阶段。
- 缓冲链路:
client_body_buffer_size(请求体)与proxy_buffering(响应体)。缓冲区一介入,流式语义就变了,大小与超时也随之中断。
模式 1 — 502:上游没接住
判据:error.log 里出现以下之一:
connect() failed (111: Connection refused) while connecting to upstream
upstream prematurely closed connection while reading response header
no live upstreams while connecting to upstream
根因分类:
| 日志特征 | 根因 | 修法 |
|---|---|---|
Connection refused |
上游没监听该地址/端口,或只监听 127.0.0.1 而 nginx 走的是容器网络 |
核对 proxy_pass 地址族;容器内用服务名或 host-gateway |
prematurely closed |
上游进程崩溃、或读请求时被重启 | 查上游日志与重启记录(RestartCount/systemctl status) |
no live upstreams |
健康检查把全部节点摘了 | 看 max_fails/fail_timeout,确认健康检查判据是否过严 |
| 响应头格式非法 | 上游返回了非 HTTP 内容(如纯文本错误) | 直连上游复现,确认它真的在说 HTTP |
最小修改:先别改 nginx。用 curl 直连上游地址验证连通性;不通就是上游或网络的问题。
验证:
curl -sv http://<upstream>:<port>/health -o /dev/null
# 期望:Connected to ... / HTTP/1.1 200
模式 2 — 504:到底是谁的超时
判据:$upstream_response_time 接近某个配置值。
upstream timed out (110: Connection timed out) while reading response header from upstream
三个超时各自的语义:
| 指令 | 默认 | 管什么 | 典型误配 |
|---|---|---|---|
proxy_connect_timeout |
60s(默认值以 nginx -T 与所装版本文档为准) |
与上游建立连接 | 内网通常可降到 5s,快速失败 |
proxy_send_timeout |
60s | 向上游写请求(两次写之间) | 大请求体时被误判 |
proxy_read_timeout |
60s(同上,以 nginx -T 为准) |
等上游响应(两次读之间) | LLM/报表类接口必须调大 |
注意:proxy_read_timeout 是两次读之间的间隔,不是总时长。长任务只要持续有字节回来就不会超时;而"憋很久再一次性返回"的接口必然触发。
最小修改:只对需要的 location 调大 proxy_read_timeout,别全局改。
location /api/long-task/ {
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_pass http://app;
}
验证:构造一个稳定的慢响应(例如上游 sleep 90 再返回),确认 90s 内不再 504,且 $upstream_response_time 记录到 90 左右。
模式 3 — 499:客户端先走了
判据:$status=499、$request_time 明显小于上游耗时、$upstream_status 常为空。
根因:客户端(或它前面的 LB/CDN)在 nginx 还没拿到响应时就断了。常见三种:
- 前端
fetch设了更短的超时(如 10s),比 nginx 的 60s 先到 - 负载均衡空闲超时小于 nginx 的处理时间
- 浏览器标签被关/页面跳转
修法:把最外层的超时对齐——建议 客户端超时 > LB 空闲超时 > nginx proxy_read_timeout > 上游超时。但这是顺序要求,不是固定倍数:长任务/流式场景应按该接口的实际耗时设置,而不是机械地逐层放大。任何一层反了,都会出现"用户看到失败、服务端日志却显示成功或 499"。
验证:在客户端侧记录请求耗时与取消原因;同时看 nginx 的 $request_time 是否与客户端放弃时刻吻合。
模式 4 — 413 与大文件上传
判据:报错来自 nginx(响应体是 nginx 的 HTML),而不是上游。
根因:client_max_body_size(默认 1m)。另外两个容易被忽略的点:
- 前端框架/网关可能也有上限(如 Spring 的
max-request-size),只改 nginx 不够 - 开了
proxy_request_buffering on(默认)时,nginx 会先把整个请求体落盘再转发;大文件会放大磁盘占用与延迟
修法:
location /upload/ {
client_max_body_size 200m;
proxy_request_buffering off; # 流式转发,nginx 不再整包落盘
proxy_pass http://app;
}
关于 Expect: 100-continue:很多客户端在上传超过某个阈值时会先发这个头,等 100 Continue 再传。是否出现、阈值多少取决于客户端与实现,不要写死数字——用 curl -v 观察实际请求头,再决定要不要 proxy_http_version 1.1 与相关超时配套。
验证:
curl -v -F file=@big.bin http://<host>/upload/ -o /dev/null
# 期望:HTTP/1.1 200 或 201;不再是 413
模式 5 — SSE/流式被缓冲
判据:Chrome DevTools 里响应是一次性到达;curl -N 直连上游是流式的,经 nginx 变成批量。
根因:
proxy_buffering on(默认)→ nginx 攒够缓冲区才下发gzip on对text/event-stream也会缓冲- 上游发的
X-Accel-Buffering被忽略,或上游根本没发 proxy_read_timeout太短,长连接被掐断
最小修改:
location /events/ {
proxy_buffering off;
proxy_cache off;
gzip off;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 3600s;
proxy_pass http://app;
}
上游侧最佳实践是同时发响应头 X-Accel-Buffering: no——这样即使 nginx 配置被回滚,流式也不至于坏。
验证:
curl -N http://<host>/events/ -H 'Accept: text/event-stream'
# 期望:事件逐条出现(每行之间有明显间隔),而不是最后一次性刷出
模式 6 — upstream keepalive 没生效
判据:ss -tan state time-wait | wc -l 很高;上游看到的连接数远大于并发请求数。
根因:keepalive 只写一半。完整三件套缺一不可:
upstream app {
server 10.0.0.11:8080;
keepalive 32; # 1) 连接池大小
}
location / {
proxy_http_version 1.1; # 2) 必须 1.1
proxy_set_header Connection ""; # 3) 清空(默认会发 close)
proxy_pass http://app;
}
少第 2 条或第 3 条,连接仍会每次新建——配置看起来"加了 keepalive",实际没生效。
验证:上游的 netstat/连接数在请求结束后不再持续增长;或在上游打印连接对象 ID,观察复用。
模式 7 — 尾斜杠与路径重写
判据:访问 /api/users 被 301 到 /api/users/;或路径前缀在转发后丢失/重复。
根因:proxy_pass 的 URI 语义。
| 写法 | 行为 |
|---|---|
proxy_pass http://app;(无 URI) |
原样透传完整路径 |
proxy_pass http://app/;(有 /) |
替换掉 location 匹配部分 |
proxy_pass http://app/v1/; |
用 /v1/ 替换匹配部分 |
连带项:重定向时 nginx 默认用 server_name 与监听端口生成 Location,反代场景常需要:
absolute_redirect off; # 发相对 Location
server_name_in_redirect off;
port_in_redirect off;
验证:curl -sv http://<host>/api/users 看 Location 是否是期望的相对路径或正确主机。
延伸阅读
references/log-fields-and-commands.md·references/subfilter-and-tls.mdreferences/error-handling.md·references/faq-antipatterns.md
错误处理约定
本技能把出错分成四类(输入错误 / 环境错误 / 证据不足 / 结论冲突),处置与重试策略不同,不要混着来;四类判据表与统一输出格式见 references/error-handling.md 第一、二节。
本技能常见错误码:
| 代码 | 现象 | 处置 |
|---|---|---|
E-ENV-01 |
改完配置 502 变多 | 回滚到上一版配置,nginx -s reload |
E-ENV-02 |
reload 后旧进程不退 | 等连接自然结束或优雅升级(-s quit 后再起) |
E-ENV-03 |
504 调大超时仍复现 | 对齐外层 LB/CDN 超时 |
E-ENV-04 |
413 改完仍失败 | 同步调上游的请求体限制 |
E-ENV-05 |
流式偶发中断 | 调大该 location 的读超时 |
E-ENV-06 |
上游证书 30 天内到期或已过期 | 换证书后 nginx -t && nginx -s reload;自签上游先在 proxy_ssl_trusted_certificate 里导入 CA |
输入校验清单、重试与退出语义见 references/error-handling.md。
失败模式与恢复
| 失败现象 | 常见原因 | 检查方法 | 恢复动作 | 可否重试 |
|---|---|---|---|---|
| 改完配置 502 变多 | include 顺序或 upstream 名不匹配 | nginx -T 看最终生效块 |
回滚到上一版配置,nginx -s reload |
可 |
| reload 后旧进程不退 | 长连接未结束 | pgrep -c nginx 看 worker 数 |
等连接自然结束或优雅升级(-s quit 后再起) |
可 |
| 504 调大超时仍复现 | 调的是错 location / 被更外层超时截断 | 看 $upstream_response_time 是否真的到点了 |
对齐外层 LB/CDN 超时 | 可 |
| 413 改完仍失败 | 上游框架也有上限 | 直连上游上传复测 | 同步调上游的请求体限制 | 可 |
| 流式偶发中断 | proxy_read_timeout 小于事件间隔 |
看中断时刻与配置值 | 调大该 location 的读超时 | 可 |
| 上游证书校验失败 | 自签或 SNI 未发 | openssl s_client 复测 |
导入 CA 或显式配置 proxy_ssl_* |
不可重试(需改配置) |
失败时优先回滚到 nginx -t 通过的上一个版本:网关配置是全站共享的,宁可回退也不要在线上试错。
完整走查(端到端样例)
场景:用户反馈"上传 60MB 视频总是失败,其它页面正常"。
输入:
curl -sv -F file=@demo-60mb.mp4 https://app.example.com/upload/ -o /dev/null
tail -n 50 /var/log/nginx/error.log
中间输出(示例):
> POST /upload/ HTTP/1.1
< HTTP/1.1 413 Request Entity Too Large
< Server: nginx/1.24.0
< Content-Type: text/html
2026-10-31 10:12:03 [error] 412#412: *991 client intended to send too large body: 62914560 bytes,
client: 10.2.0.5, server: app.example.com, request: "POST /upload/ HTTP/1.1"
定位与修复:413 由 nginx 自己返回(响应头是 Server: nginx),且错误日志明确写了 client intended to send too large body → 是 client_max_body_size(默认 1m)拦的,不是应用层。改该 location:
location /upload/ {
client_max_body_size 200m;
proxy_request_buffering off;
proxy_read_timeout 300s;
proxy_pass http://app;
}
修复后复测:
nginx -t && nginx -s reload
curl -sv -F file=@demo-60mb.mp4 https://app.example.com/upload/ -o /dev/null
# < HTTP/1.1 201 Created
同时确认应用侧也接受了(上游日志出现对应文件记录),避免"nginx 放行、应用拒绝"的假成功。
结论:这轮走查验证了判据的先后顺序——先看是谁返回的 413(响应头 + error.log 归属),再决定改 nginx 还是改上游;proxy_request_buffering off 与超时是配套项,不是可选项。
不适用场景
- 纯应用层异常:应用返回 500 并带堆栈,nginx 只忠实转发——按应用日志排查。
- Kubernetes Ingress:ingress-nginx 的注解(如
nginx.ingress.kubernetes.io/proxy-body-size)与原生指令语义不同,且受控制器版本影响。 - CDN/WAF 侧:回源超时、请求体上限、拦截规则在 CDN 上另有一套,先确认响应头是谁加的(
Server、Via、X-Cache)。 - HTTP/3 与 QUIC 特有行为:需要单独的协议层排查。
反模式与常见问题
反模式与高频问答见 references/faq-antipatterns.md。结论先给:这些做法都会让问题更难排查,改法在对应小节。