Nginx upstream模块详解:反向代理、负载均衡与故障转移实战
发布时间:2026/10/17 8:23:06 锦皓数字建站

做服务端开发和运维的人几乎没有人能绕开 Nginx。我最早在线上环境接触 Nginx 反向代理时对 upstream 模块的理解只停留在“能轮询转发请求”这个层面配置基本靠复制粘贴部署完也确实能跑。但真正开始认真研究 upstream 模块是在某次后端节点发生故障之后请求大面积报错我却不知道 Nginx 在什么条件下会摘除故障节点更不知道怎么配合权重做灰度发布。这篇文章会把 Nginx 反向代理中的 upstream 模块从指令层面到完整配置示例逐步拆开顺便把我反复踩过的坑和排查思路一并写出来。无论你是刚接触 Nginx 的新人还是想梳理代理层稳定性细节的工程师都可以照着这份内容去对照自己的配置。1. upstream在反向代理架构里到底承担什么角色很多人一开始会把“反向代理”和“upstream”混在一起说甚至觉得 upstream 就是反向代理本身。其实 upstream 是反向代理里负责“定义后端服务器组”的那部分它和proxy_pass配合才构成了完整的反向代理能力。1.1 反向代理帮我们解决了哪几类问题先想一个生活场景你打电话给一家公司不需要知道每一个员工的分机号码只需要拨总机说“转张三”总机就会帮你把电话接到张三那去。反向代理扮演的就是这个“总机”的角色客户端永远只面对一个入口地址并不知道背后到底有几台服务器也不知道请求最终被转给了谁。在真实业务中这个总机至少帮我解决了四类问题。第一是统一入口。后端服务可以散落在不同网段、不同端口客户端只需要跟 Nginx 暴露的域名或端口通信后端扩容、网络调整都不会直接暴露给调用方。第二是集中处理公共能力。比如 TLS 证书统一在 Nginx 层挂载后端不需要每台都放证书再比如限流、访问控制、请求日志都可以在这一层集中做后端服务可以保持更纯粹的 HTTP 接口逻辑。第三是负载均衡。请求均匀地分发给多个后端节点避免某一台压力过大这是 upstream 模块最核心的职责。第四是故障转移。某台后端不可用时Nginx 会把请求转给其他可用节点客户端无感知。这个能力在节点较多、发布频繁的系统里尤其重要。1.2 直接proxy_pass与upstream配置的本质区别先看最简单的反向代理写法很多小型项目都是这样起步的location /api/ { proxy_pass http://127.0.0.1:8080; }这种写法的问题是后端地址是写死的单点只要 8080 这台服务挂了整个 API 入口就瘫痪了。要想加第二个节点你只能再加一个 location 或者手动改配置完全谈不上水平扩展和故障转移。upstream 模块做的事情就是把“一组后端服务器”抽象成一个可命名的服务器池然后在代理转发时引用这个池子。例如upstream api_pool { server 127.0.0.1:8080 weight3; server 127.0.0.1:8081 weight1; } location /api/ { proxy_pass http://api_pool; }从proxy_pass http://127.0.0.1:8080变成proxy_pass http://api_pool看起来只是换了个目标地址但能力完全不同Nginx 会按照 upstream 里定义的算法在多个节点间分发请求节点不可用时自动切换还可以通过参数控制权重、失败次数、备份节点、会话保持等行为。这才是生产环境真正需要的反向代理形态。2. upstream模块核心指令拆解不会用这些就谈不上配反向代理upstream 块本身指令并不多但每个参数背后都有对应的线上行为。我按使用频率把它们拆成三组讲清楚。2.1 server指令权重、失败次数与冷却时间一个典型的 upstream 块长这样upstream backend_pool { server 127.0.0.1:8080 weight3 max_fails2 fail_timeout10s; server 127.0.0.1:8081 weight2 max_fails2 fail_timeout10s; server 127.0.0.1:8082 backup; }server指令用来声明一个后端节点可写的参数包括weight、max_fails、fail_timeout、backup、down等它们决定了这个节点在负载均衡中的“分量”和“容错策略”。weight是权重默认 1。比如weight3表示每 4 个新请求里有 3 个转发给它。权重的本质是“相对比例”不需要让三台机器的权重加起来等于 100只要比例符合预期即可。我习惯把权重写成后端机器的实际处理能力比例比如主节点性能好就 3低配节点给 1避免配置过于碎片化。max_fails和fail_timeout必须放在一起理解它们构成了一套最简单的被动健康检查机制。含义是在fail_timeout这段时间内如果对该节点的转发失败次数累计达到了max_failsNginx 就把这个节点标记为不可用并且持续fail_timeout时间不再向它转发请求这就是节点的“冷却期”。举个例子max_fails2 fail_timeout10s表示10 秒内失败 2 次节点会被摘除 10 秒。这里有一个新手容易忽略的点fail_timeout既是统计失败的窗口也是节点被摘除后的冷却时长两个语义共用一个值。如果你想实现“失败 3 次后冷却 30 秒”就要把fail_timeout写成 30s但这同时意味着统计窗口也变成了 30 秒窗口内失败 3 次才摘除。理解这个耦合关系后配置就不会凭感觉写了。backup表示备份节点平时不接收流量只有当所有非 backup 节点都不可用时才启用。down则表示直接将节点标记为不可用常用于手动下线某台机器而暂时不想改 upstream 的场合。2.2 负载均衡算法选型轮询、ip_hash、least_conn与hashupstream 默认使用加权轮询不配置算法时Nginx 按权重轮流分配请求。这是最通用的方案适用大多数后端处理时间相对平均的场景。ip_hash按客户端 IP 的哈希结果固定选择后端节点保证同一个客户端的请求尽可能落在同一台后端上适合需要使用 Session、且没有独立会话服务的老项目。但它的代价也很明显如果某台后端宕机触发流量转移映射到该节点的所有客户端会话都会失效如果中间经过多层代理拿到的是代理 IP哈希分布会非常不均匀。least_conn把请求转发给当前活跃连接数最少的后端适合请求处理时长差异很大的场景比如有些接口是秒回有些接口要跑好几秒。如果仍然用轮询慢请求可能堆积在某一台节点上least_conn 能在一定程度上缓解这种偏斜。hash指令支持自定义哈希键比如按$request_uri做哈希并配合consistent参数启用一致性哈希。这个方案非常适合缓存类服务同一个 URI 固定打到同一台后端后端的本地缓存命中率会很高节点增减时也只会影响少量请求的重新映射不会出现大面积缓存失效。我把常用算法和适用场景整理成一张表算法核心逻辑适合场景注意点加权轮询按权重轮流分配后端处理时间相近的常规服务请求时长差异大时可能偏斜ip_hash按客户端 IP 哈希固定节点需要会话粘滞的传统应用代理 IP 会导致分布不均故障转移会迁移会话least_conn分配给活跃连接最少节点接口时长差异明显的服务需要正确理解“活跃连接”的含义hash ... consistent按自定义键一致性哈希缓存类、本地状态类服务哈希键要选好避免集中热点2.3 keepalive长连接池容易被忽略的隐性参数upstream 块里的keepalive指令很容易被忽略但它对代理层性能的影响非常大。Nginx 与后端之间默认是短连接每个请求转发到后端时都要重新建立 TCP 连接请求结束再关闭。在高并发场景下这种频繁建连会导致大量 TIME_WAIT 连接同时也带来额外的握手开销。在 upstream 块里配置keepalive 32可以让每个 Nginx worker 进程都维护一批与后端的长连接请求转发时复用连接大幅降低建连开销。值得强调的是keepalive 32表示的是“每个 worker 进程与后端之间保持的空闲长连接上限”不是全局连接数。假设你的机器配置了 4 个 workerupstream 里 keepalive 为 32那么最多可能有 128 条与后端的空闲长连接。如果后端连接池上限设置得很低这个数字就可能反过来成为隐患。只写keepalive 32还不够还需要在 location 或 server 级别配合两条指令proxy_http_version 1.1; proxy_set_header Connection ;因为 Nginx 与后端复用连接需要基于 HTTP/1.1 协议同时必须清掉Connection请求头否则后端可能误认为要关闭连接。这三条配置是一个整体我只写keepalive不写后面两条连接池就形同虚设这个坑我踩过不止一次。3. 完整反向代理配置示例参数取值、发布与验证配置示例看得再多不如自己落一份完整的。我下面给出一份可以直接对照使用的完整配置然后逐个解释关键参数为什么要这么设。3.1 一份可以抄的完整配置假设有三台后端 Web 服务IP 分别是 10.0.1.10、10.0.1.11、10.0.1.12端口 8080。Nginx 作为入口对外提供服务。http { upstream web_pool { server 10.0.1.10:8080 weight3 max_fails2 fail_timeout10s; server 10.0.1.11:8080 weight2 max_fails2 fail_timeout10s; server 10.0.1.12:8080 backup; keepalive 32; } 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_addr upstream_status$upstream_status upstream_response_time$upstream_response_time request_time$request_time; access_log /var/log/nginx/access.log main; server { listen 80; server_name example.com; location / { proxy_pass http://web_pool; 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 30s; proxy_next_upstream error timeout http_502 http_503 http_504; } } }这份配置我在简化掉证书、压缩等模块后仍然保留了生产环境反向代理的核心骨架。它的逻辑是客户端请求example.comNginx 将请求转发到web_pool服务器池按 3:2 的权重分流到 10.0.1.10 和 10.0.1.11第三台作为备份节点兜底。这里有个细节第三台我刻意用了backup而不是给它一个权重。这样平时它不接收流量只有当前两台都不可用时才接管全部请求。对只承担兜底任务的节点来说这种配置比给它一个低权重更干净因为它不会在正常时期分摊业务流量也就不会引入额外的容量估算负担。3.2 proxy_set_header、超时与重试参数为什么这样配先看头部传递。proxy_set_header Host $host用来让后端拿到原始的域名而不是 Nginx 的 IP 和内网端口。很多后端服务会按 Host 做虚拟主机区分或生成完整链接Host 传错会导致重定向地址错误。X-Real-IP传的是直接跟 Nginx 建立连接的客户端 IPX-Forwarded-For使用$proxy_add_x_forwarded_for它会在已有 XFF 链上追加一层。X-Forwarded-Proto传的是客户端原本使用的协议比如 HTTPS 还是 HTTP。这三条头部是我认为反向代理环境下的“最小标准集”再往后的网关或后端服务依赖它们做日志追踪、风控判断和链接生成。再看超时参数。proxy_connect_timeout 5s控制的是 Nginx 与后端建立 TCP 连接的超时时间默认 60s但线上没必要等那么久设成 5s 可以让故障节点暴露得更快。proxy_read_timeout和proxy_send_timeout控制的是“两次读操作之间”和“两次写操作之间”的空闲超时不是整个请求的总耗时。所以对于确实需要长时间运算的接口比如导出报表、批量任务盲目把proxy_read_timeout调大不一定合理正确的做法是先确认后端处理时长再针对性设置。proxy_next_upstream定义了在什么情况下把请求转发给下一个节点。我配了error timeout http_502 http_503 http_504意思是连接错误、超时以及后端返回 502/503/504 时Nginx 会尝试其他节点。注意默认情况下 502 等状态码并不在重试列表里所以如果后端偶发 502客户端就会直接看到 502而不是自动切换节点。这个指令要谨慎使用如果后端接口不是幂等的比如订单创建、支付回调重试可能导致重复操作这类场景要把proxy_next_upstream改小或者不加 POST 相关异常。3.3 从nginx -t到curl实测的验证流程改完配置后我从来不会直接 reload而是先检查语法nginx -t看到syntax is ok再执行nginx -s reloadreload 是平滑重载配置不会中断正在处理的请求。之后我会做一轮最基本的链路验证在 Nginx 所在机器上执行curl -H Host: example.com http://127.0.0.1/如果返回正常继续追日志确认它确实走了 upstreamtail -f /var/log/nginx/access.log日志行里会看到类似upstream_addr10.0.1.10:8080的记录说明请求确实经过代理转到了后端。同时多看几条请求确认不同请求落在不同节点上权重配置生效。这里还有一个容易被忽略的动作验证完代理转发后要单独 curl 一次后端健康检查接口确认后端返回的不是 200 页面之外的错误页。否则可能 Nginx 转发正常但后端本身已经在报错了。4. 压测与流量分布验证用数据确认配置没白做配置完成后我会建议至少做一轮压测。不是为了发一个“秒杀百万并发”的漂亮数字而是为了验证两件事流量分布是否符合预期代理层是否引入了异常的耗时。4.1 压测前的观测准备压测前必须先让数据可观测。我给 Nginx 配置的日志格式里已经带了upstream_addr和upstream_response_time也就是每个请求实际转发到了哪台后端、后端处理耗时是多少。这一步不做后面压测数据只能看到 QPS根本定位不到单节点问题。后端也要配合一下。最简单的方法是让每个后端接口在响应体里返回自己的节点标识比如把实例 IP 放进一个响应头。我常用X-Backend-Server头来标记节点这样压测时只要看响应头就能判断请求真正被谁处理了。压测工具我用得最多的是wrk单机就能把代理层流量打起来。命令参考wrk -t8 -c200 -d30s http://127.0.0.1/这个命令表示 8 个线程、200 个并发连接、持续 30 秒。压测时我习惯同时开两个终端一个跑压测另一个用top观察 Nginx worker 进程和后端进程的 CPU 占用。如果 Nginx 所在机器 CPU 先打满说明瓶颈在代理层可能是 keepalive 没生效或者 SSL 加解密开销太大如果后端 CPU 打满则说明瓶颈在后端服务。4.2 权重与流量分布的验证方法假设三台后端权重配置是 3:2:0第三台 backup 不接流量压测完成后统计请求分布应该大约有 60% 落在 10.0.1.1040% 落在 10.0.1.1110.0.1.12 上一条流量都不该有。统计方法很简单直接从 access.log 里按 upstream_addr 计数awk {print $NF} access.log | sort | uniq -c | sort -rn这里的$NF因为日志格式不同可能要对位调整但思路就是把每条请求的upstream_addr提取出来做频次统计。我发现实际压测结果通常会有少量偏差比如 58% 和 42%。这不一定是配置错误更常见的原因是keepalive长连接复用导致某条连接上的请求连续落在同一节点短时间窗口内看起来不够均匀。如果偏差持续存在且达到 10 个百分点以上就要回头检查是不是有节点处于 fail_timeout 冷却期或者某个节点被防火墙/容器网络拖慢了 TCP 握手。4.3 日志里的upstream观测指标压测结束后日志是判断代理层健康状况最好的依据。我最关注的三个字段是upstream_addr实际转发到的后端地址配合计数可以看分布。upstream_status后端返回给 Nginx 的 HTTP 状态码如果大量非 200问题大概率在后端而不是 Nginx。upstream_response_time后端处理请求的耗时不包含 Nginx 本身的开销。把响应时间按节点拆出来对比能很快发现是不是有某台节点稳定性特别差。我曾经遇到过一个案例三台后端配置相同但其中一台的upstream_response_time明显高于另外两台。最后排查发现是这台机器上还跑着另一个资源密集型任务CPU 争抢导致接口变慢。这种单靠看 QPS 是发现不了的必须看分节点的耗时统计。5. 线上常见问题与排查技巧实录这一部分整理的是我线上环境里最常碰到的几类问题每类都给出排查思路而不是只贴概念。5.1 502 Bad Gateway排查实录502 出现时第一反应不是改 Nginx 配置而是先确定问题出在哪一层。先直接在 Nginx 机器上 curl 后端curl -v http://10.0.1.10:8080/health如果后端本身连不上那问题在网络或后端进程如果后端能通但经过 Nginx 就 502再看 Nginx error.log。error.log里最常见的几类线索connect() failed (111: Connection refused) while connecting to upstream后端端口没有进程监听或者监听在内网 IP 上而 Nginx 访问的地址不对。connect() failed (110: Connection timed out) while connecting to upstream网络层不通常见于防火墙拦截或跨网段路由异常。no live upstreams while connecting to upstreamupstream 里所有节点都被标记为不可用通常是因为 max_fails 触达阈值后节点进入冷却期而备份节点也没配置。遇到no live upstreams时我的处理顺序是先用curl确认后端是否真的恢复如果已恢复可以临时把max_fails调大或把fail_timeout调短reload 后观察是否还会被摘除。如果后端还在频繁失败问题应该回到后端排查而不是继续在 Nginx 层面兜底。5.2 504 Gateway Timeout排查实录504 的根因几乎都是“Nginx 等待后端响应超时”也就是proxy_read_timeout或proxy_connect_timeout被触发。出现 504 时不要急着把超时时间调大先去看 access.log 里对应请求的upstream_response_time。如果upstream_response_time接近或超过超时阈值说明后端处理确实很慢调大 Nginx 超时只是把问题往后延。此时应该去后端看线程池是否耗尽、数据库连接是否打满、接口里是否有慢查询。如果upstream_response_time很小但客户端还是收到 504那更可能是 Nginx 与后端之间的连接被异常断开比如后端主动关闭空闲连接。作为临时应急把proxy_read_timeout从 30s 调到 60s 是可以的但一定要同时设置一个监控观察调整后upstream_response_time的 P99 是否仍然逼近阈值。如果逼近长期方案只能是优化后端性能或引入异步任务而不是无限放大超时。5.3 upstream prematurely closed connection的排查这个报错在 error.log 里很典型upstream prematurely closed connection while reading response header from upstream。意思是 Nginx 还在等待响应头后端却把连接关了。常见原因有几个。第一后端进程在处理请求时崩溃比如 OOM 被系统杀掉或代码抛了未捕获异常导致进程退出。第二后端的空闲连接超时时间比 Nginx 的 keepalive 短连接被后端主动回收下一个请求复用连接时才发现连接不可用。第三请求体或响应体超过后端限制后端直接拒绝。排查时我一般先看后端自己的访问日志和错误日志确认进程有没有重启记录。如果是连接复用导致的最直接的验证方法是在 upstream 里临时把keepalive 32注释掉观察报错是否消失。如果消失说明后端对空闲连接的处理与 Nginx 的长连接策略不匹配需要协调后端调整空闲连接回收时间或者降低 Nginx 的 keepalive 值。这个报错还有一个隐藏点开源版 Nginx 的被动健康检查只统计转发失败不会主动去探测后端空闲连接是否可用。所以即使 keepalive 连接已经失效Nginx 也可能复用它直到请求失败才摘除节点。理解了这一点就不会对“明明配置了健康检查还是会偶发报错”感到奇怪。5.4 真实客户端IP丢失与链路混乱配置了反代后后端 Log 里所有客户端 IP 都变成 Nginx 的 IP这是新手常遇到的问题。解决办法就是前面配置里的proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;但要注意如果 Nginx 前面还有别的负载均衡设备或者云 LB$remote_addr拿到的是上一层的内网地址不是真实客户端 IP。这种情况下需要让上游设备把客户端 IP 写进 X-Forwarded-ForNginx 再追加自己的记录。后端服务取真实 IP 时应该取 X-Forwarded-For 列表里最左边的一个而不是简单地取最后一个否则会被伪造的头部欺骗。我见过一种比较隐蔽的问题多个应用共用一套 Nginx 配置模板其中某个应用需要拿真实 IP另一个应用不需要结果有人把proxy_set_header X-Real-IP $remote_addr写在 server 级别被 location 内覆盖后导致应用日志记录错误。排查方法是用nginx -T查看最终生效的完整配置确认每一层覆盖关系。5.5 负载不均与会话保持问题负载不均有两类常见表现。一类是配置了权重但流量分布严重偏离预期优先检查是否有节点处于冷却期、是否有防火墙干扰握手以及 keepalive 导致的长连接复用偏差。另一类是为实现会话保持而使用ip_hash但在客户端 IP 经过 NAT 或代理时大量用户会聚合成同一个 IPhash 到同一台后端造成热点。ip_hash的另一个问题是节点故障后的会话迁移。假设用户 A 的请求原本哈希到节点 1节点 1 宕机后Nginx 会暂时把用户 A 的请求转到其他节点等节点 1 从冷却期恢复后又会被重新哈希回节点 1。如果应用把会话状态放在本地内存而不是独立 Session 服务用户就会反复掉线。我的建议是新系统优先使用独立会话存储比如把 Session 放到集中缓存中老系统如果必须依赖节点本地会话可以配合hash $request_uri consistent使用一致性哈希至少节点增减时只影响部分 URI 映射比ip_hash在故障转移时的表现更平滑。6. 从配置到稳定灰度发布与实操心得很多人把 upstream 配置当成一次性工作配完就不动了。实际上它应该是发布流程和稳定性保障的一部分。6.1 用权重与backup做灰度发布upstream 的权重参数天然支持灰度发布。假设新版本后端已经部署到一台新节点上想先导 10% 流量验证稳定性可以把权重配成upstream web_pool { server 10.0.1.10:8080 weight9; # 旧版本节点1 server 10.0.1.11:8080 weight9; # 旧版本节点2 server 10.0.1.12:8080 weight2; # 新版本节点 }做一个计算总权重 20新节点权重 2占比正好 10%。验证没问题后把权重逐步调整到 3、5、8最后让新节点承载全部流量再摘除旧节点。整个过程只需要修改 upstream 块并 reload不需要动后端服务也不会断开现有连接。需要注意的是Nginx 的weight调整对长连接复用场景生效会有延迟因为已经建立的 keepalive 连接可能仍然在向旧节点转发请求。所以灰度发布时不要只看前几秒的流量比例至少观察几分钟等长连接逐步释放后再做判断。backup节点同样可以用于发布窗口的兜底。比如发布期间担心新版本异常可以把旧版本保留一台配置为backup正常情况下不接收流量一旦新节点全部异常旧节点自动接管给回滚争取时间。6.2 稳定性监控与几个容易忽略的细节upstream 配置上线后我建议至少盯下面几个指标Nginx error.log 中upstream相关报错的数量、access.log 中upstream_status非 200 的比例、upstream_response_time的 P99 分位数。任何一个出现异常趋势都要能追溯到具体节点。前面已经说过开源版 Nginx 的max_fails fail_timeout是“被动健康检查”它只在转发失败后才摘除节点不会主动探测。如果需要更精细的健康检查比如定时检查后端健康接口并即时摘除异常节点可以考虑支持 active health check 的 Nginx 商业版本或者引入专门的健康检查模块但引入新模块通常意味着需要重新编译 Nginx发布前一定要在预发环境做完整回归。最后分享一个小技巧每次准备改 upstream 前先执行一次nginx -T把当前完整的配置导出保存改完跑nginx -t验证reload 后立刻看 error.log 和 access.log 前几分钟的upstream_status。这个习惯帮我挡掉过好几次配置失误比如手误把某个节点 IP 写错、漏写了keepalive配套指令、或者把proxy_next_upstream改得太激进导致重复请求。配置 Nginx 反向代理没有太多玄学把每个指令背后的行为想清楚再用日志和数据验证配置就不会变成线上事故的源头。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。