2024 年我们上线一个新版本后,用户陆续反馈页面间歇性打不开,刷一下有时好有时坏。我们看后端服务的监控,CPU、内存、接口耗时全都正常,可前端就是会随机蹦出 502 和 504。排查了好一阵才意识到,问题根本不在后端,而在最前面那一层我们平时几乎不去碰的 Nginx——它的 upstream 配置、超时参数、缓冲区设置藏着好几个坑,正是这些把好好的请求挡在了门外。Nginx 是几乎每个 Web 系统的第一道关口,可恰恰因为它"平时很稳",它的配置往往最少被人认真审视。投了几天把 Nginx 配置系统梳理了一遍,本文复盘这次实战。
问题背景
业务:Web 站点,Nginx 反向代理 -> 后端 Java 服务(8 个实例)
事故现象:
- 页面间歇性打不开,前端随机出现 502 / 504
- 后端服务自身监控全部正常(CPU/内存/接口 RT 都没问题)
- 大文件上传必失败,页面报错
现场排查:
# 1. 看 Nginx 错误日志
$ tail -f /var/log/nginx/error.log
[error] connect() failed (111: Connection refused) while
connecting to upstream, upstream: "http://10.0.0.5:8080"
[error] upstream timed out (110: Connection timed out)
[error] client intended to send too large body
# 2. 三类错误,对应三个问题:
# - Connection refused -> upstream 里配了一个已下线的后端实例
# - upstream timed out -> 某些慢接口超过了 proxy 超时时间
# - too large body -> 上传文件超过 client_max_body_size
# 3. 看 upstream 配置
upstream backend {
server 10.0.0.1:8080;
server 10.0.0.5:8080; # 这台机器上周已经下线了!
...
}
# Nginx 仍把请求轮询到这台死掉的机器 -> 必然 502
根因:
1. upstream 里残留已下线实例,且没配健康检查 -> 轮询到它就 502
2. proxy 超时参数用默认值,慢接口直接被 Nginx 判 504
3. client_max_body_size 没调,大文件上传被 413 拦截
4. Nginx 配置长期"能跑就不动",积累了一堆隐患没人审视
修复 1:502 Bad Gateway —— 后端连不上
# === 502:Nginx 连不上后端,或后端异常断开连接 ===
# 502 的本质:Nginx 想把请求转给 upstream,但没成功。
# === 常见原因与排查 ===
# 1. 后端实例挂了 / 端口不通 -> error.log 里 "Connection refused"
# 2. upstream 配了已下线的机器 -> 轮询到它就 502
# 3. 后端处理中崩溃、连接被重置 -> "upstream prematurely closed"
# === 修复 1:给 upstream 配置故障转移 ===
upstream backend {
server 10.0.0.1:8080 max_fails=2 fail_timeout=10s;
server 10.0.0.2:8080 max_fails=2 fail_timeout=10s;
# max_fails=2:10s 内失败 2 次,就把这台标记为不可用
# fail_timeout=10s:标记后,10s 内不再往它转发请求
keepalive 64; # 与后端保持长连接,减少握手开销
}
# === 修复 2:被动健康检查 + 失败重试 ===
location / {
proxy_pass http://backend;
# 一台后端失败时,自动重试下一台,对用户无感
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2; # 最多重试 2 台
proxy_next_upstream_timeout 10s;
}
# === 修复 3:keepalive 要配套 HTTP/1.1 ===
location / {
proxy_pass http://backend;
proxy_http_version 1.1; # 长连接必须用 1.1
proxy_set_header Connection ""; # 清掉 Connection 头,否则长连接失效
}
# 经验:upstream 配置必须随实例上下线及时维护,
# 并配 max_fails 让 Nginx 能自动摘除故障节点。
修复 2:504 Gateway Timeout —— 后端太慢
# === 504:Nginx 把请求转给后端了,但等后端响应等超时了 ===
# 502 是"连不上",504 是"连上了但等不到回复"。
# === 三个关键超时参数 ===
location / {
proxy_pass http://backend;
# 1. 与后端【建立连接】的超时,一般很短
proxy_connect_timeout 5s;
# 2. 向后端【发送请求】的超时
proxy_send_timeout 60s;
# 3. 等后端【返回响应】的超时 —— 504 几乎都和它有关
proxy_read_timeout 60s;
# 默认也是 60s,如果有接口处理就是要 90s,这里就得调大,
# 否则后端还在正常干活,Nginx 已经先判了 504
}
# === 但调大超时不是万能解 ===
# proxy_read_timeout 调到 300s,确实不报 504 了,
# 但用户对着浏览器转 5 分钟圈,体验同样是灾难。
# 正确做法分两种:
# - 普通接口:超时设一个合理值(如 30~60s),
# 真慢说明接口本身有问题,要去优化接口,而不是无限调大超时
# - 确实耗时的操作(导出大报表、批量处理):
# 改成【异步任务】—— 接口立刻返回一个任务 id,
# 前端轮询任务状态,而不是让一个 HTTP 请求干等几分钟
# === 区分超时是 Nginx 判的还是后端判的 ===
# error.log 有 "upstream timed out" -> 是 Nginx 等后端超时
# error.log 没有,后端日志里有慢请求 -> 后端自己处理慢
location /api/export {
proxy_pass http://backend;
proxy_read_timeout 120s; # 导出接口单独放宽
}
修复 3:location 匹配优先级
# === 坑:多个 location 同时能匹配,到底走哪个?===
# location 匹配【不是按书写顺序】,而是有严格的优先级规则。
# === 优先级从高到低 ===
# 1. location = /path 精确匹配,匹配上立即停止
# 2. location ^~ /path 前缀匹配,匹配上且不再查正则
# 3. location ~ /regex 正则匹配(区分大小写),按书写顺序
# location ~* /regex 正则匹配(不区分大小写)
# 4. location /path 普通前缀匹配,优先级最低(兜底)
# === 一个真实踩过的坑 ===
location /static/ {
root /data/www; # 想让静态资源走这里
}
location ~ \.(js|css|png)$ {
expires 7d; # 想给静态资源加缓存头
}
# 问题:访问 /static/app.js,两个 location 都能匹配,
# 但【正则优先级高于普通前缀】,实际走了第二个,
# root 没生效 -> 找不到文件 -> 404。
# === 修复:用 ^~ 提升前缀匹配优先级 ===
location ^~ /static/ {
root /data/www;
expires 7d; # 缓存头直接写在这里
# ^~ 表示"匹配这个前缀就别再去试正则了"
}
# === 调试技巧:用变量把命中的 location 打到日志 ===
location /api/ {
set $matched "api";
add_header X-Matched-Location $matched; # 响应头里看命中了谁
proxy_pass http://backend;
}
# 经验:location 越多越要小心,= 和 ^~ 能精确控制匹配,
# 别让正则 location 意外"截胡"了你的请求。
修复 4:文件上传 413 与缓冲区
# === 413 Request Entity Too Large:上传体超过限制 ===
http {
# 默认 client_max_body_size 只有 1MB,
# 上传稍大的文件 / 图片就被 413 拦下
client_max_body_size 50m; # 按业务需要调,比如允许 50MB
# 接收请求体的缓冲区,太小会频繁写临时文件
client_body_buffer_size 256k;
}
# === 响应缓冲区:与"下载/响应慢"相关的坑 ===
location / {
proxy_pass http://backend;
# Nginx 默认会先把后端响应缓冲下来,再发给客户端。
# 缓冲区太小,大响应会被写到磁盘临时文件,变慢。
proxy_buffering on;
proxy_buffer_size 16k; # 响应头缓冲区
proxy_buffers 8 32k; # 响应体缓冲区
proxy_busy_buffers_size 64k;
}
# === 特殊场景:流式响应要【关闭】缓冲 ===
location /api/stream {
proxy_pass http://backend;
proxy_buffering off; # SSE / 大文件下载 / 实时流
# 关掉缓冲,后端产出一点就立即转发给客户端,
# 否则 Nginx 会攒着,流式效果就没了
}
# === 大文件下载相关 ===
location /download/ {
proxy_pass http://backend;
proxy_max_temp_file_size 0; # 0 = 不写临时文件,边收边发
}
# 经验:client_max_body_size 是上传方向,
# proxy_buffer* 是下行响应方向,两者别搞混。
修复 5:性能与安全加固
# === 1. gzip 压缩,减少传输体积 ===
http {
gzip on;
gzip_min_length 1k; # 小于 1k 的不压缩(没必要)
gzip_comp_level 5; # 压缩级别,5 是性价比平衡点
gzip_types text/plain text/css application/json
application/javascript text/xml;
gzip_vary on;
}
# === 2. 静态资源缓存 ===
location ^~ /static/ {
root /data/www;
expires 30d; # 浏览器缓存 30 天
add_header Cache-Control "public, immutable";
}
# === 3. 连接数 / 限流,防突发与恶意请求 ===
http {
# 按 IP 限流:每个 IP 每秒 10 个请求
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
# 按 IP 限并发连接数
limit_conn_zone $binary_remote_addr zone=connperip:10m;
}
location /api/ {
limit_req zone=perip burst=20 nodelay; # 允许突发 20 个
limit_conn connperip 50; # 单 IP 最多 50 并发连接
proxy_pass http://backend;
}
# === 4. 安全相关 ===
server {
server_tokens off; # 不在响应头暴露 Nginx 版本号
# 传递真实客户端 IP 给后端(否则后端只看到 Nginx 的 IP)
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://backend;
}
}
# === 5. 改配置后,务必先检查语法再 reload ===
# nginx -t 测试配置语法是否正确
# nginx -s reload 平滑重载,不中断现有连接
# 千万别 -t 都没过就 reload,语法错误会让 reload 失败
修复 6:Nginx 监控告警
# 用 nginx-prometheus-exporter 采集指标
groups:
- name: nginx
rules:
# 1. 5xx 错误率突增(502/504 等)
- alert: Nginx5xxHigh
expr: |
rate(nginx_http_requests_total{status=~"5.."}[5m])
/ rate(nginx_http_requests_total[5m]) > 0.01
for: 3m
annotations:
summary: "Nginx 5xx 错误率 > 1%,排查 upstream 与超时配置"
# 2. upstream 后端不可用
- alert: UpstreamDown
expr: nginx_upstream_server_up == 0
for: 1m
annotations:
summary: "upstream {{ $labels.upstream }} 有后端节点不可用"
# 3. 活跃连接数过高
- alert: NginxConnectionsHigh
expr: nginx_connections_active > 10000
for: 5m
annotations:
summary: "Nginx 活跃连接数过高,排查流量突增或后端变慢"
# 4. 请求处理时间上升(upstream 响应变慢)
- alert: UpstreamResponseSlow
expr: nginx_upstream_response_time_seconds{quantile="0.99"} > 2
for: 5m
annotations:
summary: "upstream 响应 P99 > 2s,后端处理变慢"
优化效果
指标 治理前 治理后
=============================================================
间歇性 502 upstream 含死节点 max_fails 自动摘除
慢接口 504 默认超时被误判 超时合理化 + 慢操作异步
大文件上传 413 拦截 client_max_body_size 50m
location 匹配 正则意外截胡 = 与 ^~ 精确控制
静态资源 无缓存头 expires 30d + 强缓存
传输体积 未压缩 gzip,文本类减小 60-70%
后端拿到的客户端 IP 全是 Nginx 的 IP X-Real-IP 透传真实 IP
恶意/突发请求 无防护 limit_req + limit_conn
Nginx 可观测 无 5xx/upstream/连接数监控
治理过程:
- 看 error.log 定位三类根因:0.5 天
- upstream 健康检查 + 故障转移:1 天
- 超时参数梳理 + 慢操作改异步:1.5 天
- location 优先级排查 + 缓冲区调整:1 天
- 性能安全加固 + 监控接入:1 天
避坑清单
- 页面间歇性 502/504,后端却一切正常,优先怀疑最前面的 Nginx 配置
- 502 是连不上后端,504 是连上了等响应超时,两者排查方向不同
- upstream 必须随实例上下线及时维护,配 max_fails/fail_timeout 自动摘故障节点
- 配 proxy_next_upstream 让单台后端失败时自动重试下一台,对用户无感
- proxy_read_timeout 决定 504,但无限调大不是解,慢操作应改异步任务
- location 按优先级匹配(= > ^~ > 正则 > 普通前缀),不是按书写顺序
- 正则 location 会截胡普通前缀 location,用 ^~ 提升前缀匹配优先级
- client_max_body_size 默认仅 1MB,大文件上传 413 要按业务调大
- 流式响应(SSE/大文件下载)要关闭 proxy_buffering,否则被攒着失去实时性
- 改完配置先 nginx -t 验证语法,再 nginx -s reload 平滑重载
总结
这次 Nginx 配置的排查,让我对系统里那些"平时很稳"的组件多了一份敬畏。我们一开始走了不少弯路,因为思维定式让我们死死盯着后端服务——页面打不开,那一定是后端的问题吧?可后端的 CPU、内存、接口耗时所有指标都好得很。直到我们想起去看 Nginx 自己的 error.log,真相才浮出水面:问题压根不在后端,而在那个把请求转发给后端的中间层。Nginx 作为绝大多数 Web 系统的第一道关口,它有一个很迷惑人的特性——它太稳定了,稳定到大家会渐渐忘记它的存在,它的配置文件往往是项目初期写好之后就再没人动过,后端实例上线下线了一轮又一轮,而 upstream 里那个早已下线的机器地址,却像幽灵一样一直留在配置里,Nginx 兢兢业业地按轮询规则把请求转发给它,然后理所当然地收获一个又一个 502。这件事让我明白,运维一个系统,不能只盯着业务代码,那些处在流量必经之路上的基础设施——Nginx、负载均衡、网关——同样是系统的一部分,它们的配置同样会过时、会腐化,同样需要被定期审视。具体到 Nginx 的配置上,这次复盘我理清了几条最容易踩的线索。首先要分清 502 和 504,它们看着都是 5xx,根子却完全不同,502 是 Nginx 根本没能把请求送到后端——后端挂了、端口不通、upstream 配了死节点;而 504 是请求送到了,但后端在规定时间内没把响应吐回来,要么是超时参数设得不合理把正常的慢接口误判了,要么是接口真的慢、那就该去优化接口或者把耗时操作改成异步任务,而不是一味地把超时往大调,让用户对着浏览器干等五分钟。其次是 location 的匹配,这是个特别反直觉的坑——它不是按你在配置文件里书写的先后顺序来匹配的,而是有一套精确匹配、前缀匹配、正则匹配、普通前缀的优先级规则,一个正则 location 会毫不客气地把本该走普通前缀 location 的请求"截胡"走,所以但凡 location 一多,就要用 `=` 和 `^~` 这样的修饰符把匹配范围精确地框死。还有上传下载方向上的缓冲区配置,`client_max_body_size` 默认只有区区 1MB,这个值不调,稍大一点的文件上传必然 413;而下行的 `proxy_buffering`,在流式响应的场景下又必须关掉,否则 Nginx 会把数据攒起来再发,实时性就荡然无存了。最后,我想把这次最朴素的一条经验记下来:Nginx 的所有问题,答案几乎都明明白白地写在它的 error.log 里,排查它的第一反应永远应该是去 `tail` 那个日志文件;而每一次改完配置,都要养成先 `nginx -t` 验证语法、再 `nginx -s reload` 平滑重载的肌肉记忆——因为一个配置文件里的小小笔误,足以让整个站点的入口瞬间关闭。
—— 别看了 · 2026