做 Web 服务的朋友早晚都要跟 Nginx 反向代理打交道。upstream 模块负责定义一组后端服务器配合 proxy_pass 指令就能把请求按预定的策略分发到一台或多台机器上。我见过太多人只写了最简单的proxy_pass http://backend;结果后端一挂就直接 502或者流量全部倾斜到一台机器上还不自知——问题往往不在 Nginx 本身而是 upstream 参数和路径转发规则没吃透。这篇文章以实际维护过的项目为主线把 upstream 的核心语法、负载均衡策略选型、max_fails熔断参数、proxy_pass的斜杠语义、HTTPS 和 WebSocket 场景的完整配置示例以及部署后常见的 502/504/499 排查链路一次性讲清楚。适合已经会写基本 Nginx 配置但是经常在细节上踩坑的人刚入门的朋友也可以照着示例直接抄抄完再回头看原理。1. 反向代理在真实业务里的定位1.1 为什么入口一定要放一层 Nginx拿一个典型业务来说你有两个 Java 微服务、一个 Node 写的前端接口服务未来可能还要加一个 Python 的推荐服务。如果让客户端直接访问这多个地址前端要配一堆 URL域名解析要维护多条 A 记录TLS 证书得每台机器都装一遍更不用说某台机器挂了用户根本感知不到切换。在入口放一层 Nginx 做反向代理最直接的收益是把复杂留给入口统一入口和域名客户端只认一个地址后端架构怎么变都不影响外部访问。负载均衡upstream 里挂多台后端Nginx 按轮询、权重等方式分发流量。故障转移某个后端不可用时Nginx 自动把请求转发给剩余可用节点。TLS 终结证书只部署在 Nginx 一层后端内部走 HTTP省掉重复解密的开销。静态资源缓存图片、JS、CSS 命中的直接由 Nginx 返回不压到应用层。很多人会问那我要负载均衡直接用云上的 SLB 不就行了确实可以但 Nginx 是应用层7 层代理能根据 Host、路径、Header 做更细粒度的分发SLB 更偏向四层或者简单的七层分发。实际项目里我经常看到 Nginx 放在 SLB 之后做 7 层路由两层各司其职。1.2 反向代理和正向代理别搞混正向代理是代理客户端出网比如办公网里几十台电脑统一走一台代理去访问外部资源反向代理是代理服务器入口客户端感知不到背后有多台真实服务器只看到 Nginx 这一个入口。配置上二者都用到 proxy 相关指令但方向完全不同Nginx 默认场景下的proxy_pass基本都是反向代理。对开发和运维来说反向代理还有几个容易被忽略的价值集中访问日志方便做审计和问题回溯统一的限流和访问控制可以在入口层就把恶意爬虫挡掉本地联调时前端把/api代理到远程测试环境不用本地起全套后端。理解了这些再往下看 upstream 就不会觉得它只是一个负载均衡配置了。2. upstream基本盘server参数、负载均衡策略与失败熔断2.1 upstream块的最小形态和server参数upstream 只能定义在 http 块内不能写在 server 里。它的作用就是声明一个后端组upstream backend { server 192.168.1.10:8080; server 192.168.1.11:8080; server 192.168.1.12:8080; }写完 upstream 之后在 server 块里用proxy_pass http://backend;就能引用。注意proxy_pass后面填的是 upstream 名字不是域名。不过生产环境几乎不会只写 IP 端口每个 server 后面可以带很多参数这才是 upstream 真正有意思的地方参数默认值含义典型用法weight1权重越大分到的请求越多weight3max_conns0不限最多同时转发给该节点的连接数max_conns1000max_fails1fail_timeout 内失败几次视为不可用max_fails3fail_timeout10s失败统计时间窗口熔断后探测恢复的间隔fail_timeout30sbackup无备份节点主节点全部不可用才接管backupdown无标记节点永久下线不发请求down举个例子三台机器性能不一样可以这样写upstream backend { server 192.168.1.10:8080 weight5 max_fails3 fail_timeout30s; server 192.168.1.11:8080 weight3 max_fails3 fail_timeout30s; server 192.168.1.12:8080 backup; }weight 是给机器性能差异用的老机器 weight1新机器 weight3流量按比例 1:3 分。backup 节点平时不接流量只有前两台都熔断或者手动 down 了才顶上适合做兜底。2.2 负载均衡策略怎么选upstream 默认是轮询round-robin但实际场景往往需要换策略。让我印象比较深的是这几个策略配置写法适用场景注意点轮询默认后端配置接近接口无状态若请求处理时间差异大短时间可能不均匀加权轮询server weightn机器配置不同权重按业务流量峰值估算别光看 CPU 核数最少连接least_conn;长连接、耗时差异大的接口对 WebSocket 这类长连接场景帮助很大IP 哈希ip_hash;需要按客户端 IP 做会话保持后端节点增减会导致大量会话失效通用哈希hash $request_uri consistent;缓存命中、按用户 ID 路由加consistent参数降低节点变动影响ip_hash 有个天然问题一旦后端节点变化扩容或宕机哈希表会重排原本登录的会话可能跳到另一台机器。现在大部分系统都做无状态了我反而很少用 ip_hash更多用hash $request_uri consistent;做一致性哈希配合 Redis 存 session避免单点依赖。least_conn 非常适合 WebSocket 和长轮询如果某台后端已经挂了 2000 个长连接新请求就不该再往它那塞最少连接策略会自动把它排到队尾避免新连接都堆到热点机器上。2.3 max_fails、fail_timeout 的熔断逻辑网上很多教程只写max_fails 是最大失败次数fail_timeout 是超时时间这个说法太粗糙了。实际上这两个参数共同构成一个熔断机制在 fail_timeout 这个时间窗口内如果转发到某台后端的失败次数累计达到 max_failsNginx 就认为这台机器不可用。接下来在同样长的 fail_timeout 时间内Nginx 不再把新请求转发给这台机器直到时间窗口结束才用少量请求重新探测。举个例子max_fails3 fail_timeout30s意思是 30 秒内被记录 3 次失败就熔断 30 秒30 秒后灰恢复正常调度如果连续失败会再次进入熔断。这里有个非常关键、也容易搞错的细节默认情况下后端返回 500 并不会直接触发 max_fails 计数因为 Nginx 判定失败主要看连接建立失败、发送失败、读取响应头超时或响应头无效。想让 5xx 状态码也参与故障转移必须在proxy_next_upstream里显式加上http_500 http_502 http_503 http_504等location / { proxy_pass http://backend; proxy_next_upstream error timeout http_502 http_503 http_504; }我见过不少同学把proxy_next_upstream和 max_fails 的语义搞混。简单说proxy_next_upstream控制本次请求在遇到上游错误时要不要换下一台后端重试而 max_fails 控制这台后端在时间窗口内累计多少次失败后被拉出调度池。3. proxy_pass的URI透传规则斜杠、变量与请求头3.1 不带URI与带URIproxy_pass 后面的写法分两种情况很多人在这里栽跟头不带 URI没有路径部分比如proxy_pass http://backend;Nginx 会保留客户端原始请求的完整 URI原样转发给后端。带 URI有路径部分比如proxy_pass http://backend/;或proxy_pass http://backend/api/;Nginx 会把 location 前缀匹配到的那段吃掉再用 proxy_pass 里的 URI 拼上剩余部分。最经典的例子location /api/ { proxy_pass http://backend; # 请求 /api/users - 后端收到 /api/users } location /api/ { proxy_pass http://backend/; # 请求 /api/users - 后端收到 /users } location /api/ { proxy_pass http://backend/new/; # 请求 /api/users - 后端收到 /new/users }注意proxy_pass http://backend;和proxy_pass http://backend/;看起来只差一个斜杠后端收到的路径完全不同。我曾经排查过一个诡异问题前端请求/api/list后端收到/list原因就是有人在 proxy_pass 后面多加了个/。3.2 正则 location 下 proxy_pass 的隐藏限制当 location 用正则表达式时proxy_pass 如果带 URI只能通过变量或者捕获组拼接location ~ ^/api/(.*)$ { proxy_pass http://backend/$1?$args; }正则 location 里proxy_pass http://backend/;这种静态 URI 写法是启动不了的Nginx 会直接报错proxy_pass cannot contain URI part in location given by regular expression。原因很好理解正则匹配到的 URI 动态性太强Nginx 没法确定静态 URI 和原始 URI 的替换关系必须由你通过变量明确告诉它。另外如果 proxy_pass 后面的目标用了变量比如set $backend http://api.example.com:8080; proxy_pass $backend;必须额外配置 resolver否则 Nginx 启动时能通过运行时解析域名失败会直接 502location /api/ { set $backend http://api.example.com:8080; proxy_pass $backend; resolver 114.114.114.114 valid30s; }这种动态 proxy_pass 灵活性高但性能比静态 upstream 差也没法享受 upstream 的连接池和负载均衡一般只在特殊场景下用。3.3 Host 头与 X-Forwarded 系列头不配等于白配反向代理默认情况下Nginx 会把发给后端的 Host 头设置为$proxy_host也就是 upstream 名字或 IP。如果后端是基于域名做虚拟主机路由的就收不到真实域名。所以生产配置里这几行几乎是标配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;字段含义Host客户端的原始域名后端做虚拟主机路由要用。X-Real-IP客户端真实 IP。X-Forwarded-For链路 IP 记录逐层追加。$proxy_add_x_forwarded_for会在传入的 X-Forwarded-For 基础上再追加$remote_addr。这里有个风险如果客户端伪造 X-Forwarded-For 头最后一串 IP 里会有脏数据。多层代理时建议在最靠近客户端的入口就清洗这个头或者干脆只信任内网来源。X-Forwarded-Proto客户端用的是 http 还是 https。后端生成重定向、判断安全协议时要用这个字段。另外如果 Nginx 后面还有一层负载均衡比如云 SLB - Nginx - 后端$remote_addr拿到的是内网 SLB 的 IP不是真实客户端。这时一般用 real_ip 模块配置set_real_ip_from加上real_ip_header X-Forwarded-For让 Nginx 从指定的头里取真实客户端 IP。4. 可直接抄的完整配置示例单后端、多后端、HTTPS、WebSocket这一章给出几个能直接落地的示例以一台 Nginx比如 192.168.0.10代理两台后端10.0.0.1:8080、10.0.0.2:8080做演示。4.1 单后端最小反向代理server { listen 80; server_name www.example.com; location / { proxy_pass http://192.168.0.11:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这种配置适合临时联调和单节点后端转发路径保持不变。上线前至少把 Header 三件套加上否则后端的访问日志里全是你 Nginx 的内网 IP排查问题的时候两眼一抹黑。4.2 多后端 加权轮询 熔断upstream backend { server 10.0.0.1:8080 weight5 max_fails3 fail_timeout30s; server 10.0.0.2:8080 weight3 max_fails3 fail_timeout30s; server 10.0.0.3:8080 backup; keepalive 16; } server { listen 80; server_name www.example.com; location / { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ; 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_connect_timeout 5s; proxy_read_timeout 30s; proxy_send_timeout 10s; proxy_next_upstream error timeout http_502 http_503 http_504; } }这个配置已经是生产可用级别了。proxy_http_version 1.1和proxy_set_header Connection ;是为了配合 keepalive 连接池后面第 6 章详细讲。proxy_next_upstream让请求在后端返回 5xx 时尝试下一台配合 max_fails 的熔断能显著降低单点故障的影响面。4.3 HTTPS 终结场景server { listen 443 ssl; server_name www.example.com; ssl_certificate /etc/nginx/certs/www.example.com.crt; ssl_certificate_key /etc/nginx/certs/www.example.com.key; ssl_protocols TLSv1.2 TLSv1.3; location / { proxy_pass http://backend; 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 https; } }这里有个小知识点有些旧版 Nginx 的listen写法是listen 443 ssl;而新版本1.25.1支持http2 on;。升级版本后如果突然发现 HTTP/2 没生效先检查是不是用了旧语法。X-Forwarded-Proto在 HTTPS 终结场景下建议直接写死https。如果写成$scheme在 80 端口还是会被转发为http后端判断协议时就会出错比如生成的跳转链接变成 http 而不是 https。4.4 WebSocket 代理WebSocket 是 HTTP Upgrade 协议Nginx 默认不会转发 Upgrade 头需要配合 map 处理map $http_upgrade $connection_upgrade { default upgrade; close; } upstream ws_backend { server 10.0.0.4:9501; server 10.0.0.5:9501; keepalive 16; } server { listen 80; server_name ws.example.com; location /ws { proxy_pass http://ws_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } }关键点Connection头不能写死upgrade因为普通 HTTP 请求没有 Upgrade 头时Connection 应该保持默认map 会在没有 Upgrade 头时返回close避免影响普通请求。WebSocket 代理的超时时间也要拉长一般建议 3600s 以上否则代理层空闲超时会把长连接断开。4.5 完整 nginx.conf 骨架综合起来给一个相对完整的配置注意 http 块内的基础项user www-data; worker_processes auto; pid /run/nginx.pid; events { worker_connections 4096; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for $upstream_addr $upstream_status $upstream_response_time $request_time; access_log /var/log/nginx/access.log main; error_log /var/log/nginx/error.log warn; sendfile on; keepalive_timeout 65; gzip on; include /etc/nginx/conf.d/*.conf; }这个日志格式里我特意把$upstream_addr、$upstream_status、$upstream_response_time放在了最后几个字段。这样排查问题时一条 awk 命令就能把响应超过 2 秒的请求挑出来awk {if ($(NF-1) 2) print} /var/log/nginx/access.log其中$(NF-1)是upstream_response_time$NF是request_time。日志字段固定顺序后这类快速过滤在命令行里非常管用。5. 上线前的配置校验与502/504/499排查链路5.1 nginx -t 和值得看的调试输出每次改完配置先跑这两条命令nginx -t nginx -Tnginx -t只做语法检查nginx -T会把 include 展开后的完整配置打印出来找幽灵配置比如两处 server_name 冲突、被覆盖的 upstream 定义非常好用。修改配置后平滑重载nginx -s reloadreload 不会中断现有请求Nginx 会让旧的 worker 处理完手头连接后平滑退出新配置的 worker 接管。这个操作在日常发布里是安全的可以放心用。另外别小看 4.5 的 log_format。没有$upstream_addr和$upstream_response_time的时候你只能看到 Nginx 返回了 502但是哪台后端挂了、响应花了多久全要靠猜。加上之后问题定位从猜变成看。5.2 查看 upstream 后端状态开源版 Nginx 没有直接命令查看当前 upstream 里谁被熔断了因为每台后端的状态是各 worker 进程独立维护的。实用的替代方案有三个看 error.log 里有没有no live upstreams while connecting to upstream有就说明所有节点都被熔断或标记下线。看 access.log 里的$upstream_status是否持续是 502/504以及$upstream_addr是否一直集中在某一台机器。如果编译了nginx-module-vts或nginx-module-sts可以暴露 JSON 页面看到每个 upstream 的活跃连接数和成功率。这个在压测和灰度观察时特别好用。5.3 502 / 504 / 499 逐个拆解最常遇到的三个问题直接说排查链路。502 Bad Gatewayerror.log 出现connect() failed (111: Connection refused)后端端口没监听或者后端进程监听在 127.0.0.1而你代理到了内网 IP。出现upstream sent invalid header后端返回的不是合法 HTTP 响应多半是后端进程崩溃、返回了空响应或者被防火墙直接 reset。出现upstream sent too big header while reading response header from upstream后端响应头太大超过了proxy_buffer_size把这个值调大即可。出现no live upstreams所有后端都被熔断或者全部配了 backup 且主节点都 down 了。排查顺序建议先确认后端进程活着 - 确认后端端口监听 - 在后端本机 curl 一次 - 从 Nginx 机器 telnet 后端端口 - 最后再看 Nginx error.log。很多人一上来就翻 Nginx 日志其实很多时候问题出在后端根本没起来。504 Gateway Timeout最常见的原因是proxy_read_timeout默认只有 60s后端接口本身要跑 90s直接超时。解决方案有几种接口降耗、异步化、调大proxy_read_timeout或者针对慢接口单独写 location 配置更大超时location /report/ { proxy_pass http://backend; proxy_read_timeout 300s; }还有一个隐蔽场景后端连接池耗尽Nginx 排队等待后端空闲连接最终触发读超时。这种要检查后端连接池配置和 Tomcat/Node 的并发上限不能只靠调 Nginx 超时解决。499 客户端主动断开499 不是后端错误是客户端在 Nginx 还没拿到响应前就取消了请求。常见原因前端设置了过短的超时时间、监控探针主动 abort、网络抖动导致客户端重连。处理方向前端设置合理超时并给用户反馈后端优化慢 SQL 和长接口如果确认业务允许这类请求不一定需要处理。但要注意如果某段时间 499 大量增加往往说明上游或网络出现了明显的性能劣化值得关注。6. 进阶优化keepalive连接池、缓存与动态upstream6.1 keepalive 连接池提升代理性能如果每个请求都被 Nginx 重新建立一条到后端的 TCP 连接高并发时握手开销非常可观。通过 keepalive 指令可以维持 worker 与后端之间的空闲连接池upstream backend { server 10.0.0.1:8080; server 10.0.0.2:8080; keepalive 16; } location / { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ; }这里有两个容易忽略的细节keepalive 16表示每个 worker 进程最多保留 16 条空闲连接到后端。如果你的 Nginx 有 4 个 worker那么最多有 64 条空闲连接。设太大占用后端连接数设太小效果不明显一般从 16 开始压测调整。proxy_set_header Connection ;是为了清空请求里的 Connection 头让连接尽量保持。配合proxy_http_version 1.1Nginx 和后端之间才能走连接复用。实测下来短连接接口在加上连接池之后平均响应时间能下降 30% 以上尤其是 HTTPS 握手开销被完全省掉之后。6.2 proxy_cache读多写少接口的加速利器proxy_cache_path /data/cache levels1:2 keys_zoneapi_cache:10m max_size10g inactive60m use_temp_pathoff; location / { proxy_cache api_cache; proxy_cache_key $host$request_uri; proxy_cache_valid 200 5m; proxy_cache_valid 404 1m; add_header X-Cache-Status $upstream_cache_status; }解释几个关键点keys_zoneapi_cache:10m缓存索引的内存区域10m 大概能存 8 万个 key。inactive60m60 分钟没被访问就淘汰。use_temp_pathoff直接写缓存目录减少临时文件拷贝。X-Cache-Status会返回 HIT / MISS / BYPASS / EXPIRED调试时用这个响应头能直观判断缓存是否生效。缓存适合读多写少的接口比如商品详情、配置类接口。注意别对动态接口乱开缓存尤其是有用户私有数据的接口很容易造成数据串号。开了之后一定要先看$upstream_cache_status的命中率。6.3 动态 upstream不靠 reload 切换节点发布新版本时最常做的是nginx -s reload这个操作安全且日常。但如果你的后端节点会频繁扩缩容每次改配置再 reload 其实有点繁琐而且 reload 瞬间还是会有一小段新老配置并存的窗口。工程化的做法有几种第三方dyups模块通过 HTTP API 动态修改 upstream 的 server 列表无需 reload。OpenResty 的balancer_by_lua直接从 Redis、Consul 读取节点列表按权重动态选择。可以配合注册中心做服务发现实现真正意义上的后端扩容Nginx 无感。定时生成配置 reload每 5 秒从配置中心拉取服务节点生成 upstream 配置文件再 reload。简单粗暴但节点数量较大的时候比人工改配置高效得多。如果你还没到需要动态调节点的阶段先别急着上 OpenResty。把文章里前面的静态配置、日志、超时、熔断做扎实已经能覆盖绝大多数业务需求。毕竟一个稳定可靠的静态集群比一个花哨但没压测过的动态方案要强太多。说实话Nginx 反向代理本身并不难难的是各种细节在高压场景下一起爆发。我维护这套代理之后最深的体会是宁可花十分钟把 upstream 参数、日志变量、超时配置一次写全也不要等 502 了再一台台翻日志。先把 keepalive、proxy_next_upstream、X-Forwarded 系列头打好底子后边接监控、接服务发现都会顺很多。如果你也正准备把业务迁到 Nginx 后面建议先从文中的最小配置开始跑通一层再加一层每次变更都留着nginx -t的输出出了问题回滚也快。