Nginx 可以充当静态文件服务器跑起来,但真实工作里 Nginx 90% 的用处是当反向代理——前面承接客户端流量,后面把请求转给一组 Node / Java / Python 后端。
这一篇按真实部署流程走:最小反代 → upstream 多后端 → 负载均衡策略 → 健康检查 → 长连接 → 真实 IP 透传。结尾给一份完整可粘贴的「Nginx 前 + 两个 Node 后端」demo,照着粘贴就能跑。
一、正向 vs 反向代理一句话区别
正向代理:代表客户端 反向代理:代表服务端
┌────┐ ┌──────┐ ┌──────┐ ┌────┐
│Cli │ → │Proxy │ → Internet → │Nginx │ → │APP │
└────┘ └──────┘ └──────┘ └────┘
"翻墙、公司出口代理" "Nginx、CDN、API 网关"
- 正向代理:客户端知道 proxy 的存在、配置走代理;服务端不知道真实客户端是谁
- 反向代理:客户端不知道 proxy 的存在、以为直接连服务端;服务端不知道真实客户端是谁(要靠 X-Forwarded-For 头透传)
Nginx、CDN、SLB、API 网关本质都是反向代理。本篇所有内容都围绕反向代理展开。
二、最小反代示例:proxy_pass 一行就够
假设后端是一个 Node 服务,监听在 127.0.0.1:3000。
# /etc/nginx/conf.d/my-api.conf
server {
listen 80;
server_name api.example.local;
location / {
proxy_pass http://127.0.0.1:3000;
}
}
nginx -t && nginx -s reload,访问 http://api.example.local/users 就被转发到后端。就这么简单一行。
但生产可用配置不止这一行——还需要传 header、配超时、连接池、健康检查、负载均衡,下面一步步加。
三、proxy_pass 的两种写法(踩坑王)
proxy_pass 有两种写法,转发后的 URL 完全不同,这是新人最容易踩的坑:
写法 1:proxy_pass 不带 URI(保留原 URI)
location /api/ {
proxy_pass http://backend; # 不带 /api 或别的 URI
}
请求 /api/users/123 → 后端收到 /api/users/123(原样转)
写法 2:proxy_pass 带 URI(路径替换)
location /api/ {
proxy_pass http://backend/; # 带了一个 /
}
请求 /api/users/123 → 后端收到 /users/123(location 前缀被替换成 proxy_pass 的 URI)
用一张表对比
| | |
|---|
/api/users/1 | proxy_pass http://backend; | /api/users/1 |
/api/users/1 | proxy_pass http://backend/; | /users/1 |
/api/users/1 | proxy_pass http://backend/v2; | /v2users/1 |
/api/users/1 | proxy_pass http://backend/v2/; | /v2/users/1 |
铁律:
proxy_pass 后面带不带斜杠 /,转发后的 URL 完全不同
实战记忆法:
- 想原样转:
proxy_pass http://backend;(不带任何路径) - 想剥掉 location 前缀:
proxy_pass http://backend/;(带一个 /)
还有一个高频翻车点:变量 + URI
# ❌ 当 proxy_pass 里出现变量时,nginx 不会自动替换路径
location /api/ {
set $backend "http://upstream-srv";
proxy_pass $backend;
# 此时无论加不加 / 都按"不带 URI"处理(原样转)
}
# ✅ 想剥前缀要自己 rewrite
location /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://backend;
}
四、必传 header 三件套:后端拿到真实 IP / Host
默认情况下,请求被反代后后端只能看到 Nginx 的 IP,看不到真实客户端 IP;Host 头也会变成 upstream 的地址。要让后端拿到真实信息,得显式传 header。
location / {
proxy_pass http://backend;
# 三件套
proxy_set_header Host $host; # 原始 Host
proxy_set_header X-Real-IP $remote_addr; # 客户端真实 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 链式追加
proxy_set_header X-Forwarded-Proto $scheme; # http / https
}
每一行的作用:
| | |
|---|
Host | $host | |
X-Real-IP | $remote_addr | |
X-Forwarded-For | $proxy_add_x_forwarded_for | |
X-Forwarded-Proto | $scheme | 客户端访问的协议(用于后端生成正确的 redirect) |
新人最容易忽略 Host 头:不传的话后端的 Host 头变成 backend,多租户应用直接懵——不知道用户访问的是 a.example.com 还是 b.example.com。
X-Forwarded-For 的链式逻辑
客户端 1.2.3.4 → Nginx-A → Nginx-B → 应用
应用收到的 X-Forwarded-For = "1.2.3.4, 10.0.0.1"
↑ ↑
客户端 Nginx-A
(X-Real-IP 通常只是当前 hop 的客户端,X-Forwarded-For 是完整链)
安全提醒:不要盲信 X-Forwarded-For——客户端可以伪造任意值。只在自己信任的 reverse proxy 后面才能解析它,并且只取最右侧的可信 IP(你信任的代理设置的部分)。
抽成 proxy_params 复用
每个 location 都写一堆 proxy_set_header 太啰嗦,可以抽到一个文件:
sudo tee /etc/nginx/proxy_params <<'EOF'
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_send_timeout 60s;
proxy_read_timeout 60s;
EOF
后续 location 一行 include 即可:
location / {
include proxy_params;
proxy_pass http://backend;
}
Debian / Ubuntu 包自带一份 proxy_params,可以直接 include 用。
五、upstream 模块:定义后端组
把后端从「一台」变「一组」就靠 upstream 块:
# 在 http 上下文定义
upstream backend {
server 10.0.0.10:3000;
server 10.0.0.11:3000;
server 10.0.0.12:3000;
}
server {
listen 80;
server_name api.example.com;
location / {
include proxy_params;
proxy_pass http://backend; # 注意 upstream 名要和这里对应
}
}
upstream 内 server 指令的几个常用参数:
upstream backend {
server 10.0.0.10:3000 weight=3; # 权重(默认 1)
server 10.0.0.11:3000 weight=1; # 收到的流量是上面的 1/3
server 10.0.0.12:3000 backup; # 备用(其他都挂时才用)
server 10.0.0.13:3000 down; # 标记下线(不参与)
server 10.0.0.14:3000 max_fails=3 fail_timeout=30s; # 失败 3 次拉黑 30s
server 10.0.0.15:3000 max_conns=100; # 该 server 最多并发 100
}
六、负载均衡策略对比
Nginx OSS 自带的策略:
| | | |
|---|
| 轮询(默认) | | | |
| 加权轮询 | weight=N | | |
| least_conn | least_conn; | | |
| ip_hash | ip_hash; | | NAT 场景会塌缩 |
| hash $key | hash $cookie_uid; | | |
| random two | random two least_conn; | | |
# 例 1:默认轮询
upstream backend {
server 10.0.0.10:3000;
server 10.0.0.11:3000;
}
# 例 2:least_conn
upstream backend {
least_conn;
server 10.0.0.10:3000;
server 10.0.0.11:3000;
}
# 例 3:按 cookie 中的 uid 哈希
upstream backend {
hash $cookie_uid consistent; # consistent 是一致性 hash,扩缩容不会全部重洗
server 10.0.0.10:3000;
server 10.0.0.11:3000;
}
ip_hash 的塌缩问题
ip_hash 是新人最爱写、坑最多的策略:
upstream backend {
ip_hash; # 同 IP 总打到同一后端,看似能保会话
server 10.0.0.10:3000;
server 10.0.0.11:3000;
}
实际坑:
- 客户端经过 NAT(公司出口、家庭路由)时,几百个用户共享一个出口 IP,全部塌缩到同一后端,负载严重不均
- 客户端走移动 4G/5G 网络,IP 频繁变动,"黏住"形同虚设
生产推荐:
- 完全无状态(推荐):用默认轮询或 least_conn
- 需要会话保持:把会话信息放 Redis / Memcached 等集中存储,应用本身无状态
- 实在要黏:用
hash $cookie_sessionid consistent 而不是 ip_hash
七、健康检查:被动 vs 主动
Nginx OSS 自带的是「被动健康检查」:等转发失败几次后摘掉。Nginx Plus 才有「主动健康检查」(定时去探)。
被动健康检查(OSS 自带)
upstream backend {
server 10.0.0.10:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.11:3000 max_fails=3 fail_timeout=30s;
}
含义:
- fail_timeout 内(30 秒),如果失败 ≥ max_fails 次(3 次),就把这个 server 拉黑
- 拉黑期就是 fail_timeout(30 秒),到期后会再尝试
- 「失败」的定义看
proxy_next_upstream 配置:默认包括 error timeout
两个高频问题:
- 流量小的应用拉黑可能很慢:QPS 只有 1,3 次失败要 3 秒;如果间隔超过 fail_timeout,永远拉不黑
- 拉黑期到了试探时,那一个请求注定失败
主动健康检查的替代方案
OSS 没有原生主动健康检查,常见三种替代:
upstream_check_module(Tengine / 淘宝版第三方模块):定时探测- OpenResty + lua-resty-upstream-healthcheck
- 第三方框架:Consul / Nacos + nginx-template 动态更新 upstream
中型项目建议直接上 APISIX / Kong,主动健康检查、动态路由、监控一揽子。
八、长连接:upstream keepalive 必须配
默认情况下,Nginx 每次转发请求都会新建一条到后端的 TCP 连接,转发完关闭——3 次握手 + 4 次挥手,开销巨大。生产必须配 keepalive 复用:
upstream backend {
server 10.0.0.10:3000;
server 10.0.0.11:3000;
keepalive 32; # 每个 worker 维护 32 个空闲连接
keepalive_timeout 60s; # 空闲超过 60s 关
keepalive_requests 1000; # 单连接处理 1000 个请求后关
}
server {
location / {
include proxy_params;
proxy_pass http://backend;
# **这两行是 keepalive 必须的,少一行就废**
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
为什么要 proxy_http_version 1.1 和 Connection "":
- 默认
Connection: close 会强制关连接,要置空(让 nginx 自己加 keep-alive)
漏配的代价:高 QPS 场景下 nginx 与后端之间 TIME_WAIT 暴涨,源端口耗尽,新连接全部失败。
九、完整 demo:Nginx 前 + 两个 Node 后端
来一份完整可粘贴、上手即跑的最小 demo。
后端:起两个 Node 服务
// app.js
const http = require('http');
const port = process.env.PORT || 3000;
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
from: `node-${port}`,
path: req.url,
host: req.headers.host,
realIp: req.headers['x-real-ip'],
forwardedFor: req.headers['x-forwarded-for'],
}));
}).listen(port);
console.log(`Backend listening on ${port}`);
# 一个终端
PORT=3001 node app.js
# 另一个终端
PORT=3002 node app.js
Nginx 配置
# /etc/nginx/conf.d/demo.conf
upstream backend {
least_conn;
server 127.0.0.1:3001 max_fails=3 fail_timeout=10s;
server 127.0.0.1:3002 max_fails=3 fail_timeout=10s;
keepalive 16;
keepalive_timeout 60s;
keepalive_requests 1000;
}
server {
listen 80;
server_name demo.local;
access_log /var/log/nginx/demo.access.log;
error_log /var/log/nginx/demo.error.log warn;
# 健康检查端点(让自己监控用)
location = /healthz {
return 200 "ok\n";
access_log off;
}
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 $scheme;
# 长连接
proxy_http_version 1.1;
proxy_set_header Connection "";
# 超时
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 失败重试(默认 nginx 会自动 next_upstream,这里显式声明)
# ⚠️ 默认只对 GET / HEAD 等幂等请求 retry;如果允许 POST 重试,
# 需要 proxy_next_upstream_methods,但生产慎用——会重复写
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 10s;
}
}
验证
# reload
sudo nginx -t && sudo nginx -s reload
# 加 hosts
echo "127.0.0.1 demo.local" | sudo tee -a /etc/hosts
# 连续访问,看是不是轮到不同后端
for i in {1..6}; do
curl -s http://demo.local/api/users | jq '.from'
done
# "node-3001"
# "node-3002"
# "node-3001"
# "node-3002"
# ...
# 看 Host 头透传是不是正确
curl -s http://demo.local/test | jq
# {
# "from": "node-3001",
# "path": "/test",
# "host": "demo.local", ← Host 头正确
# "realIp": "127.0.0.1", ← X-Real-IP 正确
# "forwardedFor": "127.0.0.1"
# }
# 模拟一个后端挂掉
# Ctrl+C 杀掉 3001
# 再连续访问,看 nginx 是否自动避开
for i in {1..6}; do curl -s http://demo.local/ | jq '.from'; done
# "node-3002"
# "node-3002"
# ...
# 测完清理 hosts,避免污染本机解析
sudo sed -i '/demo.local/d' /etc/hosts
十、常见踩坑清单
| | |
|---|
| 没配 X-Real-IP / X-Forwarded-For | |
| | proxy_set_header Host $host; |
| | upstream 加 keepalive + location 加 http_version 1.1 |
502 Bad Gateway | | 看 error.log 的 connect() failed |
504 Gateway Timeout | | |
503 No upstreams | | |
| | 用 resolver 8.8.8.8 valid=30s; + 变量绕过 |
| | 换 hash $cookie_xxx 或彻底无状态 |
| 用了 return / rewrite ... last; | 用 proxy_pass 直接转发,不要中间 rewrite 改 method |
DNS 解析的隐藏坑
# ❌ OSS 中 server 域名只在启动 / reload 时解析一次
upstream backend {
server api.internal.example.com:3000;
}
# 后端 IP 变了 nginx 完全无感,一直打到旧 IP(除非 reload)
# ✅ 用 resolver + 变量绕过
http {
resolver 8.8.8.8 valid=30s ipv6=off;
}
server {
location / {
set $upstream "api.internal.example.com";
proxy_pass http://$upstream:3000;
}
}
# 用变量后,每次请求都按 resolver 的 valid 时间重新解析
K8s 场景下后端 Pod IP 频繁变化,这个坑特别致命。生产建议用 Service ClusterIP(稳定)或上 APISIX / Envoy(原生支持服务发现)。
写在最后
- proxy_pass 带 / 与不带 / 转发路径完全不同
- header 三件套必传:Host / X-Real-IP / X-Forwarded-For,否则后端拿不到真实信息
- upstream 必配 keepalive + location 必配 http_version 1.1 + Connection “”
- ip_hash 在 NAT 场景会塌缩
- OSS 的健康检查是被动的,需要主动健康检查的上 OpenResty 模块或换 APISIX / Kong
- upstream server 写域名要小心 DNS 缓存问题,要么用 IP,要么 resolver + 变量绕过
- 排查反代问题先看 error.log:connect refused = 后端没起 / timeout = 网络或后端慢 / 502 = 后端异常退出
阅读原文:点击这里
该文章在 2026/9/3 9:41:09 编辑过