2022 年我给一个后端服务加了 Nginx 做负载均衡。原本服务只有一台机器,扛不住量,我加到三台,前面挂一个 Nginx 分流。第一版我做得很省事:写一个 upstream,把三台后端的地址填进去,location 里 proxy_pass 一指,完事。本地一测——真香:请求轮着打到三台机器,负载看着挺均匀。我心里很踏实:"负载均衡嘛,不就是 upstream 里写几个后端地址,Nginx 自动帮我分流。"可等它真正上线、跑在真实流量里,一串问题冒了出来。第一种:有一台后端挂了,可 Nginx 照样把请求轮给它,用户每三次请求就撞上一次 502。第二种:用户登录之后,过一会儿就被踢回登录页——他的请求被轮到了另一台后端,登录态丢了。第三种:后端打日志,发现所有请求的客户端 IP,全是 Nginx 那台机器的 IP——真实用户 IP 拿不到了。第四种:用户上传一个稍大的文件就失败,后端报 413。我盯着这一连串问题想了很久才彻底想明白,第一版错在一个根本的认知上:我以为"负载均衡就是 upstream 里写几个后端地址,Nginx 自动帮我分流"。这句话把 Nginx 反向代理,简化成了一个"流量分发器"。可它不是。Nginx 反向代理是夹在用户和后端之间的一整层代理,它要管的事远不止"把请求分出去":挂掉的后端要能自动摘除、用户的会话要能保持、真实的客户端信息要透传给后端、超时和缓冲要配对、失败要能重试。真正用好它,核心不是"upstream 里写几行",而是理解它作为一层代理,要替你处理健康检查、会话、请求头、超时这一整套事情。这篇文章就把 Nginx 反向代理与负载均衡梳理一遍:为什么"写几个后端"不等于负载均衡、负载均衡策略怎么选、挂掉的后端怎么自动摘除、真实客户端信息怎么透传、超时和缓冲怎么配,以及失败重试、长连接、平滑重载这些把反向代理真正做对要避开的坑。
问题背景
先把那串问题的现象和我的误判讲清楚,后面所有的设计都是冲着纠正这个误判去的。
现象:给后端加了 Nginx 负载均衡后,上线冒出一串问题:一台后端挂了 Nginx 照样往它转发,用户撞 502;用户登录态过一会儿就丢;后端日志里客户端 IP 全是 Nginx 的 IP;用户上传稍大的文件就报 413。
我当时的错误认知:"负载均衡就是 upstream 里写几个后端地址,Nginx 自动帮我分流,会用 proxy_pass 就够了。"
真相:Nginx 反向代理是用户和后端之间的一整层代理,它要替你处理一整套事情:用 max_fails 把挂掉的后端摘除;用合适的策略或外置 session 保持会话;用 proxy_set_header 把真实客户端信息透传给后端;用超时、缓冲、体积上限对付慢后端和大请求;用 proxy_next_upstream 做失败重试。upstream 写几行只是开头,把这层代理的职责配全才是关键。
要把 Nginx 反向代理用对,需要几块认知:
- 为什么"写几个后端"不等于负载均衡——它是一层代理,不是流量分发器;
- 负载均衡策略——轮询、加权、最少连接、IP 哈希,各自适合什么;
- 健康检查——max_fails / fail_timeout 怎么把挂掉的后端自动摘除;
- 请求头透传——真实 IP、协议、Host 怎么传给后端;
- 超时缓冲、失败重试、长连接、平滑重载这些工程坑怎么处理。
一、为什么"写几个后端"不等于负载均衡
先把这件最根本的事钉死:Nginx 反向代理,不是一个站在路口、机械地把车流分到三条路的"分流牌"。它是一整层代理,它代表后端去面对用户、又代表用户去面对后端。这意味着:它得知道后端是死是活,不能把请求往一具尸体上送;它得明白用户的请求之间有"状态"的牵连,不能把同一个用户的连续请求随意打散;它得把用户的真实身份信息(IP、协议、域名)如实转告后端,因为这些信息到了它这里就被它自己"挡掉"了;它还得处理用户和后端之间一快一慢的速度差。你只在 upstream 里写三个地址,等于只告诉了它"有哪三条路",却没告诉它这一整层代理该怎么当。
下面这段配置,就是我那个"上线就出问题"的第一版:
# 反面教材:以为 upstream 里写几个地址就万事大吉了
upstream backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
# 破绽一:没有健康检查 —— 某台后端挂了,Nginx 照样把请求轮给它,
# 用户每三次请求就撞上一次 502。
# 破绽二:没透传任何请求头 —— 后端拿到的"客户端 IP"全是 Nginx 的 IP,
# 拿到的协议和 Host 也都不对。
# 破绽三:没配超时和缓冲 —— 后端一慢,连接就大量堆积。
# 破绽四:没配会话保持 —— 同一用户的请求被轮到不同后端,登录态丢失。
这段配置能跑、流量也确实分出去了,它的问题不在配置本身,而在一个被忽略的前提:它默认"我的活就是把请求分匀,分匀了就完了"。可它漏掉了一层代理该尽的全部其他职责。于是那串问题就有了解释:撞 502,是因为它不做健康检查、不知道哪台后端死了;登录态丢失,是因为它把同一用户的请求随意打散到了不同后端;真实 IP 丢失,是因为它没把 X-Real-IP 之类的头透传下去;上传报 413,是因为它用了默认的请求体大小上限。问题的根子清楚了:Nginx 反向代理的每一项职责——健康检查、会话、请求头、超时——你不专门去配它,它就出问题。先从负载均衡本身说起。
二、负载均衡策略:轮询、加权、最少连接、IP 哈希
Nginx 默认的负载均衡策略,是轮询(round-robin)——请求按顺序、一台一台地、均匀地分给各个后端。这在所有后端配置相同、每个请求耗时也差不多时,工作得很好。但现实往往不是这样,于是 Nginx 提供了另外几种策略:
# 默认策略:轮询(round-robin),请求按顺序均匀分给每台后端
# 加权轮询:性能强的机器多分一些流量
upstream backend_weighted {
server 10.0.0.1:8080 weight=3; # 这台配置好,分 3 份
server 10.0.0.2:8080 weight=1;
server 10.0.0.3:8080 weight=1;
}
# 最少连接:把新请求发给当前活跃连接数最少的后端,
# 适合各请求耗时差别很大的场景,避免慢请求把某台机器压垮
upstream backend_leastconn {
least_conn;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
# IP 哈希:同一个客户端 IP 永远落到同一台后端
upstream backend_iphash {
ip_hash;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
这几种策略的选法是有讲究的:后端机器配置不一样,就用 weight 让强的机器多担一点;请求耗时忽长忽短(有的查询秒回、有的要跑十几秒),纯轮询会让某台机器积压一堆慢请求,这时 least_conn 把新请求发给最闲的那台更稳妥;而 ip_hash 是专门为"会话保持"设计的——它保证同一个客户端 IP 的请求,永远落到同一台后端,这样用户的登录态就不会因为换了机器而丢。不过这里要提前埋一个提醒:ip_hash 只是一种朴素的、不完美的会话保持,它的局限留到最后一节细说。策略选完了,下一个更要命的问题是:万一某台后端挂了,Nginx 怎么知道、怎么别再往它送请求?
三、健康检查:让挂掉的后端自动被摘除
开头那个"每三次请求撞一次 502",根子就是没有健康检查。Nginx 的开源版本,没有"主动定时去 ping 后端"这种健康检查,但它有一套被动的机制:它在转发真实请求的过程中,如果发现某台后端连续失败,就把它暂时摘掉。控制这套机制的,是 max_fails 和 fail_timeout:
upstream backend {
# max_fails:在 fail_timeout 这段时间内,这台后端累计失败 3 次,
# 就把它标记为"不可用",暂时不再往它转发请求
# fail_timeout:既是上面的统计窗口,也是被摘除后的"冷静期"时长
server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
# backup:平时不接任何流量,只有当上面的后端全部挂掉,它才顶上
server 10.0.0.3:8080 backup;
}
这套机制的逻辑是:某台后端在 fail_timeout(这里是 30 秒)的窗口内,失败次数达到 max_fails(3 次),Nginx 就把它标记为不可用,在接下来的 fail_timeout 时间里不再往它转发;冷静期过后,Nginx 会试探性地再放一个请求过去,通了就恢复。backup 则是留的一手后路:平时完全不接流量,只有主力后端全军覆没时才被启用。这里要建立一个清醒的认知:开源 Nginx 的这套是被动健康检查——它得先用几个真实用户的请求去"踩雷",才能发现后端挂了。也就是说,后端刚挂的那一小段时间里,还是会有少量请求撞上 502,这没法完全避免(要做到真正的零感知,得上 Nginx Plus 的主动健康检查,或配合上层的探活)。但配好 max_fails,至少能保证一台后端挂掉后,绝大部分请求会被迅速、自动地导向健康的机器,而不是一直往坑里掉。后端的死活管住了,下一个问题是:后端怎么知道请求到底是谁发来的?
四、请求头透传:把真实客户端信息告诉后端
开头那个"后端日志里 IP 全是 Nginx 的 IP",是反向代理必然会带来、也必须主动解决的问题。道理很简单:用户的请求,先到 Nginx;再由 Nginx 转发给后端。所以在后端看来,这个请求是 Nginx 发来的——它看到的源 IP,自然就是 Nginx 的 IP;它看到的 Host、协议,也都变成了 Nginx 这一跳的信息。用户原本的真实信息,在 Nginx 这一层"断"了。解决办法,是让 Nginx 在转发时,把这些信息用专门的请求头"带"给后端:
location / {
proxy_pass http://backend;
# Host:把用户原本访问的域名透传给后端,
# 否则后端按虚拟主机分发时会找不到对的站点
proxy_set_header Host $host;
# X-Real-IP:把真实的客户端 IP 单独告诉后端
proxy_set_header X-Real-IP $remote_addr;
# X-Forwarded-For:在已有的转发链后追加本次客户端 IP,
# 经过多层代理时,这里会形成一条完整的 IP 链
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# X-Forwarded-Proto:告诉后端用户用的到底是 http 还是 https
proxy_set_header X-Forwarded-Proto $scheme;
}
这四个 proxy_set_header,几乎是每个反向代理配置都该有的标配。它们各自补回了一块在 Nginx 这层被"挡掉"的信息:Host 让后端知道用户访问的是哪个域名;X-Real-IP 和 X-Forwarded-For 让后端拿回真实的客户端 IP(这对打日志、做风控、限流都至关重要);X-Forwarded-Proto 尤其容易被忽略——如果用户用 TLS,在 HTTP 之上加一层 TLS 加密,防止中间人窃听和篡改。">HTTPS 访问 Nginx、Nginx 用 HTTP 转发给后端,后端如果不看这个头,就会误以为用户在用 HTTP,于是生成出 http:// 开头的链接、或错误地发起跳转。这里的认知要点是:反向代理在"挡"在用户和后端之间的同时,也"挡"掉了一批用户的真实信息——把这些信息透传回去,是它分内的责任,不是可选项。信息透传清楚了,下一个要处理的,是用户和后端之间的速度差。
五、超时与缓冲:对付慢后端和大请求
反向代理夹在中间,要同时面对两端:一端是可能很慢的用户网络,另一端是可能很慢的后端服务。处理不好这个速度差,连接就会大量堆积。先看超时和缓冲:
location / {
proxy_pass http://backend;
# 三个超时:和后端建立连接、读取后端响应、向后端发送请求,
# 各自的时间上限。任何一个超了,这次转发就失败
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
# 响应缓冲打开:Nginx 先尽快把后端的完整响应收下来、缓存住,
# 再慢慢发给慢速的客户端。这样后端连接能尽早释放,
# 不会被一个网速很差的用户长时间占着
proxy_buffering on;
proxy_buffers 8 16k;
proxy_buffer_size 16k;
}
proxy_buffering 这个开关的意义,值得专门讲一下:打开它,Nginx 会充当一个"蓄水池"——它用自己较快的网络,迅速把后端的响应全部接过来,后端这边的连接立刻就能释放、去服务下一个请求;至于那个网速很慢的用户,就由 Nginx 慢慢地、耐心地把缓存的数据喂给他。后端宝贵的连接资源,因此不会被慢用户拖住。而开头那个"上传大文件报 413",是另一个独立的坑——Nginx 对请求体的大小有一个默认上限:
http {
# 默认请求体上限只有 1M,上传稍大的文件就会直接被 Nginx
# 挡下,返回 413 Request Entity Too Large —— 请求根本到不了后端
client_max_body_size 50m;
server {
location /upload {
proxy_pass http://backend;
# 上传大文件时关掉请求缓冲,让数据边收边转给后端,
# 而不是等 Nginx 把整个大文件缓存完再转
proxy_request_buffering off;
# 上传通常耗时较长,把读写超时放宽
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
}
这两段配置合起来说明一件事:反向代理对请求和响应,都有一套自己的"尺寸和时间"的默认限制,这些默认值是为通用场景定的,不一定适合你。client_max_body_size 默认才 1M,做文件上传必须调大,否则请求连后端的门都摸不到就被 413 挡回去了。下面这张图,把一次请求穿过 Nginx 反向代理的完整路径串起来:
六、工程坑:失败重试、长连接与平滑重载
五块设计之外,还有几个工程坑,不处理就会让反向代理用得别别扭扭。坑 1:配上失败重试,让一次倒霉的转发自动换台机器。哪怕配了 max_fails,某台后端刚开始出问题、还没被摘除的那几个请求,仍可能失败。proxy_next_upstream 能让这种请求自动换一台后端重试:
location / {
proxy_pass http://backend;
# 这次转发遇到下列情况时,自动换 upstream 里的下一台后端重试:
# error:连接后端出错;timeout:转发超时;
# http_502 / http_503:后端返回了 502 或 503
proxy_next_upstream error timeout http_502 http_503;
# 给重试套上限,防止一个请求无限地换后端、无限地重试
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 10s;
}
这里有个必须注意的细节:proxy_next_upstream 默认只对"安全"的请求(如 GET)重试。对 POST 这类会改数据的请求,自动重试是危险的——万一第一次其实已经写成功了、只是响应丢了,重试就会造成重复下单、重复扣款。所以非幂等请求的重试,要格外谨慎。坑 2:和后端之间配长连接,省掉反复握手的开销。默认情况下,Nginx 每转发一个请求,都和后端新建一次 TCP 连接、用完就关。高并发下,这种反复的握手和挥手是不小的浪费。配 keepalive 维持一个到后端的长连接池:
upstream backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
# 维持一个到后端的长连接池,空闲连接最多保留 32 个,
# 后续请求直接复用,省掉反复的 TCP 握手
keepalive 32;
}
location / {
proxy_pass http://backend;
# 用长连接,必须把转发协议升到 1.1、并清空 Connection 头
# (默认的 Connection: close 会让连接一用完就被关掉)
proxy_http_version 1.1;
proxy_set_header Connection "";
}
坑 3:改完配置用 reload,绝不要用 restart。这是运维层面最该形成肌肉记忆的一条。restart 是把 Nginx 整个停掉再启动,这中间所有连接都会被粗暴中断;而 reload 是平滑重载:
# 平滑重载:改完配置不要用 restart(会中断所有连接),要用 reload
nginx -t # 第一步:先测试配置语法,有错就停下,别往下走
nginx -s reload # 第二步:平滑重载
# reload 的过程:Nginx 主进程加载新配置、启动新的 worker 进程,
# 老的 worker 进程则继续把手头已有的请求处理完,然后才退出。
# 全程不中断任何已有连接,新连接由新配置接管 —— 用户完全无感知。
nginx -t 这一步千万不能省:它先帮你检查新配置有没有语法错误。要是跳过它直接 reload,而配置恰好写错了,Nginx 会拒绝加载——好在它会继续用旧配置跑,但你得到的是一个"改了却没生效"的假象。坑 4:分清 502 和 504,它们指向完全不同的病因。502 Bad Gateway 意味着 Nginx 联系后端时,后端拒绝连接、或者挂了、或者回了一个非法响应——病根在后端进程本身(崩了、没起来、端口不对)。504 Gateway Timeout 则是 Nginx 连上了后端,但在 proxy_read_timeout 之内没等到响应——病根是后端太慢(慢查询、死锁、外部依赖卡住)。502 去查后端死活,504 去查后端性能,方向不能搞反。坑 5:ip_hash 不是可靠的会话保持。它有两个硬伤:一是大量用户可能共用一个出口 IP(同一个公司、同一个小区的 NAT 出口),这些人会全部被哈希到同一台后端,负载根本不均;二是一旦增减后端机器,哈希结果会大面积改变,大批用户的会话当场失效。所以真正可靠的会话保持,不该依赖 ip_hash,而应该把 session 外置——存到 Redis 这类共享存储里,让后端变成无状态的,这样请求落到哪台机器都一样。
关键概念速查
| 概念 / 手段 | 说明 |
|---|---|
| 反向代理 | 夹在用户和后端之间的一整层代理,不只是流量分发 |
| 轮询 round-robin | 默认策略,请求按顺序均匀分给各后端 |
| weight 加权 | 性能强的后端配更大权重,多承担流量 |
| least_conn | 发给当前连接数最少的后端,适合请求耗时不均 |
| max_fails / fail_timeout | 被动健康检查,后端连续失败就暂时摘除 |
| backup | 备用后端,主力全挂时才顶上 |
| proxy_set_header | 透传真实 IP 协议 Host 给后端,弥补代理挡掉的信息 |
| proxy_buffering | Nginx 缓冲响应,让后端连接不被慢客户端拖住 |
| proxy_next_upstream | 转发失败时自动换后端重试,非幂等请求需谨慎 |
| reload vs restart | reload 平滑重载不断连接,restart 会粗暴中断 |
避坑清单
- upstream 写几个后端只是开头,健康检查会话请求头超时都得另配。
- 不配 max_fails,某台后端挂了 Nginx 照样往它转发,用户撞 502。
- 开源 Nginx 是被动健康检查,后端刚挂的瞬间仍会有少量请求失败。
- 必配 proxy_set_header 透传真实 IP 协议 Host,否则后端拿到的全错。
- X-Forwarded-Proto 别漏,否则 HTTPS 访问下后端会误判成 HTTP。
- client_max_body_size 默认才 1M,文件上传场景必须调大否则 413。
- 打开 proxy_buffering,别让慢速客户端长时间占着后端连接。
- proxy_next_upstream 默认只重试 GET,POST 等非幂等请求重试要谨慎。
- 改配置用 reload 不要用 restart,且 reload 前先 nginx -t 测语法。
- ip_hash 不是可靠会话保持,真正可靠的做法是把 session 外置。
总结
回头看那串"后端挂了照样转发、登录态丢失、真实 IP 拿不到、上传报 413"的问题,以及我后来在 Nginx 上接连踩的坑,最该记住的不是某一条配置指令,而是我动手前那个想当然的判断——"负载均衡就是 upstream 里写几个后端地址,Nginx 自动帮我分流"。这句话错在它把 Nginx 反向代理,矮化成了一个只会"分流"的机器。我以为它的全部职责,就是把请求像发牌一样匀给后端。可它根本不是一个分流器,而是一整层代理。"代理"这个词的分量在于:它代表后端去面对用户,又代表用户去面对后端。它站在了用户和后端中间——这个位置,意味着所有原本由用户和后端直接打交道才能传递的信息和状态,现在都得由它来居中处理:后端的死活,它得探;用户的真实身份,它得转告;两端的速度差,它得调和;转发的失败,它得兜底。
所以用好 Nginx 反向代理,真正的工程量不在"写一个 upstream、proxy_pass 一指"那两行配置上。那两行,任何入门教程的第一页就教完了。真正的工程量,在于你要理解它"夹在中间"这个位置带来的全部责任,并把这些责任一项项配上:它挡在后端前面,你就得给它配 max_fails,让它能发现并摘除挂掉的后端;它挡掉了用户的真实信息,你就得用 proxy_set_header 把真实 IP、协议、Host 透传回去;它两头连着快慢不同的网络,你就得用 proxy_buffering 和超时去调和这个速度差;转发可能失败,你就得用 proxy_next_upstream 兜底,还要分清这个失败是 502 还是 504。这篇文章的几节,其实就是顺着这条思路展开的:先想清楚"写几个后端"为什么不等于负载均衡,再讲透负载均衡的几种策略,用健康检查接住"后端挂掉",用请求头透传补回"被挡掉的真实信息",用超时和缓冲调和"两端的速度差",最后是失败重试、长连接、平滑重载这几个把反向代理用扎实的工程细节。
你会发现,Nginx 反向代理这个角色,和现实里一家公司的"前台"完全相通。一个不称职的前台,会怎么干?谁来了都往里领,根本不管要找的人在不在工位(这就是不做健康检查,把请求往挂掉的后端送);客人进门时,他不记下客人是谁、从哪来,里面的同事只看到"前台带进来一个人",根本不知道这是哪位(这就是不透传真实 IP);他也不管来访者是来递个文件还是来搬一整车货,一律照同一个流程处理(这就是不区分小请求和大文件上传)。而一个称职的前台怎么做?他先确认要找的同事在不在、忙不忙,不在就引荐给别的能办事的同事(健康检查与失败重试);他会清楚地把"这位是谁、来意如何"通报给里面(请求头透传);他懂得让快递员把整车货先卸在前台、由自己慢慢搬进去,而不是让快递员的车一直堵在公司门口(响应缓冲);他还知道太大件的东西门口进不来,得提前打招呼走货梯(client_max_body_size)。反向代理配得好不好,从来不在于它能不能"把人往里领",而在于它有没有真正担起"夹在中间"这个位置该担的全部责任。
最后想说,Nginx 反向代理配没配对,差距永远不会在"本地一台后端跑通"时暴露——本地只有一台后端、它不会挂、没有慢请求、没有真实的多用户,你会觉得"写个 upstream 转发一下"这几个字已经是全部。它只在真实的、有后端宕机、有慢查询、有海量真实用户的线上环境里才显形。那时候它会用最让人头疼的方式给你结账:配不好,你会像我一样,被一串看似无关的怪象折磨——后端明明还活着两台,用户却一直在撞 502;用户反复掉登录;后端的风控和限流全失灵,因为它眼里所有人都是同一个 IP;你查遍了后端代码,后端明明没毛病,可系统就是不稳;而配对了,你的服务会稳得让人安心:一台后端宕机,流量几秒内就自动绕开它,用户几乎毫无察觉;后端清楚地知道每个请求来自哪个真实用户;慢用户拖不垮后端;改配置平滑重载、不断一个连接。所以别等"莫名其妙的 502"找上门,在你写下那个 upstream 的那一刻就该想清楚:我配的不是一个流量分发器,而是一层代理——它要替我探后端的死活、透传用户的身份、调和两端的速度、为失败兜底,这些责任,我是不是每一项都配上了?这些问题有了答案,你的 Nginx 才不只是一个"看起来能分流"的转发配置,而是一层真正稳健、可靠、扛得住后端宕机和海量流量的反向代理。
—— 别看了 · 2026