接手一个需要支撑高并发的服务,后端挂了、流量分不匀,第一个想到的工具基本就是 Nginx。它的反向代理和负载均衡能力,是绝大多数 Web 架构里的基础配置,也是我从接触 Linux 运维到现在用得最频繁、最顺手的能力之一。今天把一套完整的配置示例拆开讲,假设有三台后端服务器,写清楚怎么配、为什么这么配、上线之后又该怎么验证和排错。这篇东西适合刚接触 Nginx 的同学对照着抄作业,也适合已经会配但对参数含义一知半解的人补一补“为什么”。
先说清楚这套配置解决的是什么问题。你有一个域名或者说一个入口地址,请求进来之后,Nginx 负责把请求转发给后端真正的业务服务。如果后端只有一台机器,坏掉就是全体不可用;如果有很多台,就需要按策略分流量。反向代理解决的是“入口统一、后端隐藏”的问题,负载均衡解决的是“流量怎么分、后端怎么撑”的问题。Nginx 一台机器同时干这两件事,配置文件写明白,后面运维省心一大截。
1. 先从原理说起:反向代理与负载均衡到底在解决什么问题
1.1 “反向”在哪里:方向不同,用途完全不同
很多初学者容易把正向代理和反向代理搞混。正向代理是客户端主动配置代理,代理替客户端去请求外部资源,典型场景是局域网内访问外网。反向代理则是客户端根本不知道后端服务器的存在,它只访问 Nginx,Nginx 再把请求转给内部后端。方向一反过来,安全性、扩展性、可维护性就完全不一样。
为什么必须要 Nginx 挡在前面?直接暴露后端服务器不是更简单吗?你想想几个场景:后端服务要升级重启,如果客户端直连,升级的瞬间连接全部断掉;后端有多台机器,客户端到底连哪一台,谁来决定;日志、限流、鉴权这些通用能力,如果每个后端各自实现一遍,重复劳动且容易漏。Nginx 把这些公共问题收敛到一个入口,后端只管业务逻辑,这是架构上比较经典的分层思路。
我用生活化的类比:后端服务器像厨房里的几个灶台,Nginx 像餐厅的服务员。客人不会自己冲进厨房找锅,他只跟服务员下单,服务员根据哪个灶台空闲、哪个灶台做得快,把单子分下去。客人体验到的只有一个入口,厨房内部怎么安排是后厨的事。这就是反向代理加负载均衡的直观画面。
1.2 负载均衡的基础算法:轮询、权重、IP_HASH怎么选
Nginx 默认使用轮询(round-robin)算法,请求按顺序轮流分发到每台后端。这是最省事、最公平的策略,适合后端配置差不多、接口响应时长也差不多的场景。但真实环境里后端资源往往不齐整,比如两台是 8 核 16G,一台是 4 核 8G,平均分显然浪费了高配机器。这时候要用 weight(权重)参数,让高配机器多扛一些流量。
另一种常见策略是 ip_hash,根据客户端 IP 的哈希值把请求固定分发到某一台后端。这个策略的核心价值是“会话保持”,比如用户登录状态存在后端内存里,如果第二次请求被转发到另一台机器,登录态就丢了。虽然现在有 Redis 这种集中式会话存储,但一些老系统、小项目还是会依赖 ip_hash 解决粘滞问题。
还有 least_conn(最少连接数),Nginx 会把请求分给当前并发连接数最少的那台后端。这个策略对长连接、耗时差异大的接口比较友好,但计算开销稍高,适合后端服务处理能力差异大、接口耗时不平均的业务。我见过不少团队默认用轮询,结果某台后端偶尔出现 CPU 飙高,排查半天发现是接口响应差异大导致的倾斜,换成 least_conn 之后明显改善。
1.3 为什么单独强调超时和重试机制
后端服务器不是永动机,总有假死、报错、响应迟缓的时候。Nginx 负载均衡不只是“分发”,还承担“故障转移”的职责。默认配置下,如果某台后端连接失败,Nginx 会把请求转给下一台;但如果是响应超时,转发逻辑会更复杂。这里涉及的参数是 proxy_connect_timeout、proxy_read_timeout,它们决定了 Nginx 愿意等后端多久。
很多人忽略的是,Nginx 默认只在“连接失败”时重试,不会在所有超时场景下自动重试,避免把重复请求都打给后端。这点要结合具体业务去调,读接口可以适当放宽重试,写接口要谨慎,否则可能出现重复下单或者重复扣款之类的严重问题。配置里我一般把 proxy_next_upstream 参数显式列出来,而不是依赖默认行为,这样出问题的时候一眼能看明白。
2. 完整配置示例:三台后端机的标准写法
2.1 upstream 块:定义后端服务器池
先把核心配置亮出来,这是一个典型的、可直接使用的反向代理加负载均衡配置。配置文件放在 /etc/nginx/conf.d/ 下,比如命名为 loadbalance.conf:
upstream backend_servers { # 默认轮询方式 server 192.168.1.11:8080 weight=1 max_fails=2 fail_timeout=30s; server 192.168.1.12:8080 weight=2 max_fails=2 fail_timeout=30s; server 192.168.1.13:8080 backup; }这个 upstream 块是负载均衡的核心。三行 server 指令分别定义了三台后端,权重分别为 1 和 2,也就是说 11 号机承担约三分之一流量,12 号机承担约三分之二流量。13 号机标注为 backup,平时不参与分发,只有前面两台都挂了或者不可用的时候,Nginx 才会把请求转给它。这种主备模式在生产环境非常实用,避免备用机白白空转到过热,也便于紧急切换。
max_fails 和 fail_timeout 配合使用,表示在 30 秒内如果这台服务器失败达到 2 次,Nginx 会把这台标记为不可用,暂时不转发请求给它是 timeout 窗口过后再重新尝试。这个参数就是故障转移的“闸门”,决定了后端失败后多久能被摘除、多久能被恢复探测。
2.2 server 块:入口监听与请求转发
继续看完整的 server 块配置:
server { listen 80; server_name www.example.com; access_log /var/log/nginx/example_access.log; error_log /var/log/nginx/example_error.log; location / { proxy_pass http://backend_servers; 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; } }listen 80 表示监听标准的 HTTP 端口,server_name 用来匹配请求头里的 Host,这是 Nginx 多站点共存的基石。请求进来之后进入 location /,所有路径都会匹配到这个块,然后 proxy_pass 把请求公开给 upstream 里定义的 backend_servers 这个后端池。
proxy_set_header 四行是很多新手容易漏的。默认情况下 Nginx 转发请求时,后端看到的 IP 其实是 Nginx 的 IP,客户端真实 IP 丢失了。X-Real-IP 和 X-Forwarded-For 手动把真实 IP 带过去,后端的日志、埋点、风控才能正常工作。我接手过不止一个项目,后端拿不到客户端 IP,排查了老半天,最后发现是 Nginx 模板里少了一行 proxy_set_header,这种基础问题尽早写进模板,一步到位。
2.3 location 细化:不同业务路径转不同后端
生产环境里很少只有一个后端池。常见的场景是 /api/ 开头的请求走业务接口后端,/static/ 开头直接本地文件或走静态资源服务,其他路径走页面渲染服务。这时候就需要在 location 层做分流:
upstream api_servers { server 192.168.1.21:8000; server 192.168.1.22:8000; } upstream web_servers { server 192.168.1.31:80; server 192.168.1.32:80; } server { listen 80; server_name www.example.com; location /api/ { proxy_pass http://api_servers; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias /var/www/static/; expires 7d; } location / { proxy_pass http://web_servers; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里有个容易踩坑的细节:location /api/ 和 location / 的匹配优先级,Nginx 默认是前缀匹配时最长路径优先,所以 /api/ 的请求会精确地进到 /api/ 块,不会掉到 / 块。理解了这点,你就知道为什么 location 顺序有时候不重要,有时候又影响很大,关键看路径的覆盖关系。
2.4 proxy_pass 结尾带不带斜杠的差异
这个坑我必须单独拎出来说。proxy_pass 后面的 URL 带不带路径 /,转发行为完全不同。如果写的是 proxy_pass http://backend_servers;,不带路径,Nginx 会把完整的原始 URI 原样转发给后端;如果写的是 proxy_pass http://backend_servers/;,带一个斜杠,Nginx 会把 location 匹配的那段前缀替换掉再转发。
我来举个例子。请求访问的是 /api/user/info,location 是 /api/。不带斜杠时,后端拿到的 URI 是 /api/user/info;带斜杠时,后端拿到的 URI 变成 /user/info。这两种行为决定了后端接口定义的路径风格。很多前后端分离项目里,前端调 /api/user/info,后端实际暴露 /user/info,靠的就是这层替换。如果配反了,要么 404,要么路由全部错乱,排查起来很让人头疼。
3. 实操过程:从零到能跑通全套流程
3.1 环境准备与 Nginx 安装
先说明我的实验环境:两台 Ubuntu 22.04 虚拟机加一台本机,Nginx 版本是 1.24.0,后端用最简单的 Python HTTP 服务模拟。你可以用任何后端替换,只要端口对得上就行。
安装 Nginx 我建议直接用发行版官方源,不求最新,但求稳定。在 Ubuntu/Debian 上就是两条命令:
sudo apt update sudo apt install -y nginxCentOS/RHEL 系则用:
sudo yum install -y nginx安装完先不急着配,先确认服务能起来。systemctl start nginx 之后,浏览器访问本机 IP 能看到 Nginx 欢迎页,说明基础环境没问题。这一步很重要,避免后面配置出问题时分不清是 Nginx 的问题还是配置文件的问题。
3.2 配置整段写入与语法检查
我不会去修改 /etc/nginx/nginx.conf 主文件,而是新建一个独立配置文件,这样后续维护、删除、对比都方便。在 /etc/nginx/conf.d/ 下创建 loadbalance.conf,把前面那套 upstream 和 server 配置写进去。这里有个前提:主配置里 http 块内通常已经有 include /etc/nginx/conf.d/*.conf; 这一行,Ubuntu 默认自带,其余系统需要确认一下。
写完之后,最关键的一步是语法检查。Nginx 提供了现成的工具:
sudo nginx -t这个命令会检查所有配置文件语法,同时检查引用的文件路径是否合法。输出类似:
nginx: configuration file /etc/nginx/nginx.conf test is successful看到 successful 才算通过。我见过太多同学跳过这步直接 restart,结果配置有误,服务直接挂掉。nginx -t 这个习惯一旦养成,能帮你省下无数个凌晨的排障时间。
3.3 重载配置与验证分发效果
语法没问题之后,接下来就是让配置生效。这里我推荐用 reload 而不是 restart。reload 会平滑加载新配置,正在进行的请求不会断掉;restart 是直接重启进程,会造成瞬间的连接中断。生产环境里能用 reload 就不用 restart,这是运维的基本礼仪。
sudo systemctl reload nginx重载之后怎么确认配置真的生效?首先是看进程状态:
sudo systemctl status nginx然后验证实际的转发行为。我习惯先在每台后端起一个能显示自身标识的 HTTP 服务,最简单的是写一个小脚本:
from http.server import HTTPServer, BaseHTTPRequestHandler class Handler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header('Content-Type', 'text/plain') self.end_headers() self.wfile.write(b'This is backend 11') HTTPServer(('0.0.0.0', 8080), Handler).serve_forever()三台后端分别改成 11、12、13,起在不同的端口上。然后在客户端侧持续请求 Nginx:
for i in $(seq 1 10); do curl -s http://127.0.0.1/ ; echo; done如果权重配置是 1:2:0,你会看到大约三分之一的请求返回 backend 11,三分之二返回 backend 12,13 号全程不出现。连续执行多轮,比例基本稳定在 1:2 附近,说明权重在真实生效。如果发现请求一直落在同一台,先去检查是否有缓存、是否有 keepalive 连接粘连、location 是否匹配到了别的代理规则。
3.4 顺手验证一下 backup 的故障切换
权重验证通过后,我还会顺手做一次故障模拟。把 11 号后端停掉,也就是把那个 Python 进程 kill 掉,再连续请求 Nginx。这时候由于 max_fails 和 fail_timeout 的生效,Nginx 会在最多几次失败后把 11 号摘除,请求全部打到 12 号。然后再把 11 号重启,等 fail_timeout 窗口过了,Nginx 又会探测到它恢复,流量自动重新分配。这个验证非常值钱,模拟的是真实的后端宕机场景,确认故障转移真能跑得通,心里才有底。
4. 进阶参数与生产环境调优
4.1 连接复用与 keepalive 配置
默认情况下,Nginx 和后端之间每次请求都会新建一条 TCP 连接,请求结束就断开。高并发场景下,这条连接的建立开销会非常可观。upstream 块里可以启用长连接:
upstream backend_servers { server 192.168.1.11:8080; server 192.168.1.12:8080; keepalive 32; }keepalive 32 表示 Nginx 为每个 worker 进程保留最多 32 条空闲的长连接,复用给后续请求。配套地,location 里还要加两行:
location / { proxy_http_version 1.1; proxy_set_header Connection ""; }为什么必须加上?因为后端服务判断是否长连接,依赖 HTTP 协议版本和 Connection 头。HTTP/1.0 默认是短连接,必须显式改成 1.1 并清空 Connection 头,否则 keepalive 根本不生效。我见过一些配置抄了 upstream 里的 keepalive 却忘了改 location,结果连接池一直是空的,性能没提上来。这两处必须成对出现,缺一不可。
4.2 权重与故障转移参数再深入
再展开说说 weight、max_fails、fail_timeout 这三个参数在实际项目里的调整经验。
weight 不只是简单的比例关系,它影响的是 Nginx 的调度权重。我通常在刚上线新机器时把权重调低,比如 0.5,观察一段时间确认新机器稳定,再一点点往上加。这比一次性把流量切过去稳得多。如果新机器代码有问题或配置不对,权重低只是影响少量用户,可以快速回退。
max_fails 和 fail_timeout 我一般搭配成 max_fails=2 fail_timeout=30s,这组参数意思是 30 秒内失败 2 次就摘除 30 秒。如果后端服务启动很慢,比如 Java 应用冷启动要四十多秒,那 fail_timeout 就得相应调大,否则健康检查频繁把正在重启的节点摘除,反而引发雪崩。这种“参数跟着业务特性和后端启动速度走”的思维,比死记硬背一组数值重要得多。
4.3 会话保持的两种主流方案
前面提到 ip_hash 可以实现会话保持,但它有一个坑:如果某个客户端 IP 后面是大量用户,比如公司出口 NAT,那这堆用户的请求会全部落到同一台后端,造成热点。这种场景下更好用的是 cookie 粘滞(sticky),Nginx Plus 商业版支持,开源版可以用第三方模块或者在后端会话层解决。
我的建议是:不要过分依赖 Nginx 层的会话保持,优先把会话状态抽到 Redis 或数据库里,让后端变成无状态服务。无状态的好处是,任何请求落在哪台后端都一样,扩容缩容随时做,后端重启不影响用户。如果历史包袱太重,不得不依赖内存 Session,再用 ip_hash 顶一阵,但记得留时间改造。
4.4 常见超时参数一表看懂
超时参数是生产环境排障的常客,我把最常用的几个整理成一个速查表:
| 参数名 | 默认值 | 作用说明 | 实际建议 |
|---|---|---|---|
| proxy_connect_timeout | 60s | Nginx 与后端建立 TCP 连接的超时 | 内网建议 5-10s,避免后端假死拖垮连接 |
| proxy_read_timeout | 60s | Nginx 等待后端响应的超时 | 按接口最慢耗时放宽,一般 30-120s |
| proxy_send_timeout | 60s | Nginx 把请求体发给后端的超时 | 涉及大文件上传时酌情调大 |
| proxy_next_upstream | error timeout | 触发重试的错误类型 | 写接口建议去掉 timeout,只保留 error |
最后一个参数很容易引起数据重复问题,我再强调一遍。如果后端口误删除了记录但返回 200,Nginx 不会重试;如果后端处理到一半超时,客户端可能已经收到错误或被重试,这会导致重复写入。所以写接口的代理重试策略要保守,读接口可以激进一些。我的配置里,写接口一般这样限制:
location /api/write/ { proxy_pass http://backend_servers; proxy_next_upstream error; }只保留 error 类重试,超时和 http_500 都不重试,避免一个请求被多次处理。
5. 验证结果与排查经验速查
5.1 通过日志验证真实转发去向
生产环境不方便随便停后端,那怎么确认负载均衡在按预期工作?最直接的方式是看 Nginx 的访问日志。默认日志格式里能看到请求的响应状态、耗时、来源 IP,但看不到转发给了哪台后端。要看到这个信息,需要自定义日志格式,在 http 块或 server 块里定义 log_format:
log_format upstream '$remote_addr - $upstream_addr [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_user_agent" "$upstream_response_time"';关键变量是 $upstream_addr,它记录了请求实际被转发到的后端地址,比如 192.168.1.12:8080。日志里如果长期只有某一台后端的地址,说明流量倾斜;如果三台交替出现,那负载均衡就是在正常干活。我把这个 log_format 放到 nginx.conf 的 http 块里,所有站点都能复用,有需要的时候直接把日志路径指到这个格式,很方便。
5.2 常见问题速查表
实践过程中,我把高频问题整理成了一张速查表,遇事可以直接对照:
| 问题现象 | 大概率原因 | 排查方向 |
|---|---|---|
| 502 Bad Gateway | 后端服务没起或端口不对 | 检查后端进程和监听端口,确认 Nginx 与后端网络通 |
| 504 Gateway Timeout | 后端响应时间超过 proxy_read_timeout | 调大 read 超时,或检查后端慢查询 |
| 请求全打到一台后端 | location 写错,走了单台代理 | 检查 location 匹配,确认 upstream 名字引用正确 |
| 后端拿不到客户端真实 IP | 缺少 proxy_set_header | 补 X-Real-IP 和 X-Forwarded-For |
| reload 后配置未生效 | 语法错误被忽略或缓存 | 先 nginx -t,再 reload,再确认版本 |
| 3台后端轮询,个别请求失败 | max_fails 判定失败摘除中 | 查看 error.log,确认是否在 fail_timeout 窗口内 |
5.3 一个典型排障实录
分享一次真实排障经历。有次线上反馈说某个接口偶发 502,我第一反应是看 Nginx error.log。日志里能看到类似 connect() failed (111: Connection refused) while connecting to upstream 的记录。这说明 Nginx 尝试连接后端时,连接被拒绝。接着我去看后端的连接数和进程状态,发现后端 Tomcat 的线程池被打满,新的连接直接排队,触发拒绝。
这个问题的根源不是负载均衡配置,而是后端容量不足。Nginx 只是尽到了“发现后端不可用”的职责,停止继续转发请求给故障节点。但我的 max_fails=2 在这个场景反而加重了问题:只要连续失败两次,Nginx 就把节点摘除 30 秒,期间流量全打到另一台,另一台也很快被打满。后来我调整了策略,把 fail_timeout 缩短,同时在后端增加了健康检查的动态摘除手段,整体稳定性才上来。
这个案例想说的一点是,Nginx 参数的取值直接影响故障行为。不是参数越多越高深越好,而是要匹配后端的真实容量和故障恢复速度。脱离业务场景谈配置,全是空谈。
5.4 监控与告警建议
负载均衡配置写完不算完事,还要有盯住它的手段。最简单实用的方式是盯 Nginx 的 stub_status 模块,它在 location 里暴露一个内置状态页:
location /nginx_status { stub_status on; access_log off; allow 127.0.0.1; deny all; }访问 /nginx_status 能看到当前活跃连接数、请求总数、读写等待连接数。配合 Zabbix、Prometheus 这类工具可以周期抓取这些数据,画出趋势图。后端每台机器的负载和日志也要一起看着,因为负载均衡只能分流量,不能消故障,真正兜底的是后端服务的健康度。
我个人在实际操作中的体会是,负载均衡配置最容易出的问题往往不在配置本身,而在“参数不匹配业务”。权重设得太激进、健康检查阈值太灵敏、超时时间卡得太死,都会导致正常流量被误伤。上线前最好模拟一遍故障场景,停一台机器、延迟几秒响应,看看 Nginx 会做出什么反应,这个过程非常值得做。
最后再分享一个小技巧:每次改完 Nginx 配置,先备份一份带日期的副本再 reload。比如 cp loadbalance.conf loadbalance.conf.bak-20260101,万一改乱了回滚也方便。别嫌麻烦,这个习惯在关键时刻能救你一命。Nginx 配置这件事,重要的不只是会写,更要会安全地改、可追溯地改。