Appearance
Nginx 反向代理
第 1 篇里 Nginx 直接返回静态文件。但后端应用(Java、Go、Node.js)的请求不能靠 Nginx 处理——Nginx 不会执行业务代码。本篇讲 Nginx 怎么把请求转发给后端,以及转发时那些容易踩的坑。
一、最小代理
先跑一个后端服务。用 Python 起一个最简单的 HTTP 服务监听 8080:
bash
mkdir -p /data/app && cd /data/app
echo 'hello from backend' > index.html
python -m http.server 8080直接访问 8080,能拿到内容:
bash
curl http://127.0.0.1:8080/返回 hello from backend。但这意味着外部得通过 8080 端口访问,没有统一入口。让 Nginx 在 80 端口接请求、转发到 8080:
nginx
server {
listen 80;
server_name app.example.local;
location / {
proxy_pass http://127.0.0.1:8080;
}
}bash
nginx -t && systemctl reload nginx
curl -H 'Host: app.example.local' http://127.0.0.1/返回 hello from backend,代理通了。
二、后端拿到的 IP 不对
代理能跑,但很快会发现一个意外。后端服务如果想记录"是谁访问了我",看一眼日志——拿到的客户端 IP 是 127.0.0.1,也就是 Nginx 的 IP,不是真实客户端的 IP。
原因是请求是 Nginx 转发给后端的,后端看到的"客户端"就是直接连它的那个进程——Nginx。真实客户端的 IP,Nginx 知道,但没告诉后端。
解决办法是转发时把原始信息塞进请求头里带过去:
nginx
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host; # 保留用户访问的域名
proxy_set_header X-Real-IP $remote_addr; # 直接连接 Nginx 的客户端 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # 标记原始请求是 http 还是 https
}各请求头的作用,以及不传的后果:
| 请求头 | 做什么 | 不传的后果 |
|---|---|---|
Host | 用户浏览器里输入的域名 | 后端生成的跳转 URL 域名不对 |
X-Real-IP | 直接连到 Nginx 的客户端 IP | 后端审计日志里全是 Nginx 的 IP |
X-Forwarded-For | 完整的代理链路 IP | 多层代理时丢失真实客户端来源 |
X-Forwarded-Proto | 原始请求协议 | 后端生成 http:// 而非 https:// 的回调地址 |
多层代理时 X-Forwarded-For 会是一串 IP。后端取"真实客户端 IP"时,要配置可信代理 IP 范围——不能直接信任 X-Forwarded-For 最左边那个值,因为客户端可以伪造这个头。
三、proxy_pass 末尾的斜杠
代理配置里最容易踩的坑,是 proxy_pass 末尾有没有 /。后端路由前缀是 /api/users,Nginx 这么配:
nginx
# 情况一:proxy_pass 末尾无斜杠
location /api/ {
proxy_pass http://127.0.0.1:8080;
}
# /api/users → 转发到 http://127.0.0.1:8080/api/users(location 前缀保留)
# 情况二:proxy_pass 末尾有斜杠
location /api/ {
proxy_pass http://127.0.0.1:8080/;
}
# /api/users → 转发到 http://127.0.0.1:8080/users(location 前缀被替换)差一个斜杠,转发路径完全不同。后端路由如果是 /users,用了情况一,Nginx 转发到 /api/users,后端返回 404。本地调试时路径碰巧对得上,上线后后端路由前缀不一样就 404。
排查代理后 404,把 Nginx 配置里的 proxy_pass 和后端实际路由前缀放在一起对着看,先确认这个斜杠。
四、负载均衡
后端从一个变多个,Nginx 把请求分发出去。多个后端写成一个 upstream 组:
nginx
upstream app_backend {
server 192.168.10.21:8080;
server 192.168.10.22:8080;
}
server {
listen 80;
server_name app.example.local;
location / {
proxy_pass http://app_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;
}
}默认是轮询(round-robin),其他分发方式:
| 方式 | 配置 | 适用场景 |
|---|---|---|
| 轮询 | 默认,不写就是 | 后端规格一致 |
| 权重 | server ... weight=2 | 后端机器配置不同 |
| 最少连接 | least_conn; | 请求耗时差异大(长短连接混合) |
| IP hash | ip_hash; | 简单的会话保持 |
权重示例:
nginx
upstream app_backend {
server 192.168.10.21:8080 weight=2; # 这台配得高,分到更多请求
server 192.168.10.22:8080 weight=1;
}五、后端挂了会怎样
把一个后端的进程杀掉,再访问,请求会落到挂掉的那个后端上吗?开源版 Nginx 的健康检查是被动的——不会主动探测后端活不活,要等请求失败了才知道。
nginx
upstream app_backend {
server 192.168.10.21:8080 max_fails=3 fail_timeout=30s;
server 192.168.10.22:8080 max_fails=3 fail_timeout=30s;
}| 参数 | 含义 | 不写的默认行为 |
|---|---|---|
max_fails=3 | 在 fail_timeout 时间内失败 3 次后,该后端被临时摘除 | max_fails=1,失败一次就摘除 |
fail_timeout=30s | 失败计数的统计窗口,也是摘除持续的时间 | fail_timeout=10s |
代理层重试:
nginx
location / {
proxy_pass http://app_backend;
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2; # 最多尝试 2 个后端,避免无限重试放大流量
}proxy_next_upstream_tries 不写会怎样:默认可能把所有后端都试一遍,一个慢请求被重试多次,流量被放大。设上限是必要的。
重试只对幂等请求(GET)安全。下单、支付、创建任务这类请求如果在入口层被重试,后端可能收到两次写入——所以应用自身要有幂等保护,不能依赖 Nginx 层面不重试。
六、接口慢,卡在哪
一个接口响应慢,可能是连不上后端、可能是后端处理慢、也可能是发送数据慢。三个超时分别管不同阶段:
nginx
location / {
proxy_pass http://app_backend;
proxy_connect_timeout 3s; # 连接后端的超时——连不上时快速失败
proxy_send_timeout 30s; # 向后端发送请求体的超时
proxy_read_timeout 30s; # 等待后端返回响应的超时——接口慢时先看这个
}| 参数 | 超时发生在哪个阶段 | 超时的现象 |
|---|---|---|
proxy_connect_timeout | TCP 握手阶段——后端端口是否可达 | 后端没起或端口不通 |
proxy_send_timeout | 发送请求体阶段——Nginx 把请求数据发给后端 | 客户端上传慢、网络抖动 |
proxy_read_timeout | 等待响应阶段——后端多久没返回数据 | 最常见,后端处理慢 |
缓冲配置:
nginx
location / {
proxy_pass http://app_backend;
proxy_buffering on; # 开启缓冲:Nginx 收完后端响应再发给客户端
proxy_buffers 16 16k;
proxy_busy_buffers_size 64k;
}缓冲对普通 HTTP 接口是好事——后端快慢都先由 Nginx 扛着,客户端不会直接感受到后端的抖动。但流式响应(SSE、文件下载、实时推送)需要关闭缓冲,否则客户端迟迟看不到数据:
nginx
location /events/ {
proxy_pass http://app_backend;
proxy_buffering off; # 流式响应:后端吐一点 Nginx 就传一点
proxy_read_timeout 1h; # 长连接放宽超时
}proxy_buffering on 用在流式接口上会怎样:Nginx 攒够一批才发给客户端,SSE 推送会卡住、文件下载进度条不动。
七、WebSocket
WebSocket 需要协议升级,普通代理配置接不上。浏览器发起 WebSocket 时会带 Upgrade: websocket 头,Nginx 要把这个头透传过去并升级 HTTP 版本:
nginx
map $http_upgrade $connection_upgrade {
default upgrade;
'' close; # 没有 Upgrade 头时,Connection 设为 close
}
server {
listen 80;
server_name ws.example.local;
location /ws/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1; # WebSocket 必须 HTTP/1.1
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 1h; # 长连接超时放宽
}
}WebSocket 断连时,除了看 Nginx 配置,还要排查链路上每一层:中间负载均衡的空闲超时、浏览器控制台的错误信息、后端应用的连接日志。很多断连不是单点问题,而是某一层空闲超时设得太短。
八、代理日志
排查代理问题光看默认日志不够,加一个自定义格式把 upstream 状态和耗时打出来:
nginx
log_format proxy_main '$remote_addr - $host "$request" '
'status=$status body=$body_bytes_sent '
'request_time=$request_time '
'upstream=$upstream_addr '
'upstream_status=$upstream_status '
'upstream_time=$upstream_response_time';
access_log /var/log/nginx/app.access.log proxy_main;关键字段:
| 字段 | 含义 |
|---|---|
$request_time | Nginx 从收到请求到完成响应的总耗时(秒) |
$upstream_addr | 实际转发到的后端地址 |
$upstream_status | 后端返回的 HTTP 状态码 |
$upstream_response_time | 后端处理耗时(秒) |
接口慢时,这几个字段能区分是谁的问题:$request_time 高、$upstream_response_time 也高 → 后端慢;$request_time 高但 $upstream_response_time 正常 → 客户端传输慢或 Nginx 侧排队;$upstream_status 是 502/504 → 后端挂了或超时。
九、故障排查
| 现象 | 常见原因 | 排查方向 |
|---|---|---|
| 502 Bad Gateway | 后端端口不可达、进程挂了、协议不对 | ss -lntp 看端口、后端日志、Nginx error log |
| 504 Gateway Timeout | 后端响应超过 proxy_read_timeout | 后端耗时、数据库查询、外部接口调用 |
| 404(代理后) | proxy_pass 斜杠导致路由错位 | 对比 Nginx 转发路径和后端路由 |
| 后端拿不到真实 IP | 请求头没传或后端没信任代理 | X-Forwarded-For、应用框架代理配置 |
| 只有一个后端有流量 | upstream 配置写了多台但只有一台被用到 | access 日志里的 $upstream_addr |
| 大文件上传失败 | client_max_body_size 默认 1MB | 加大限制:client_max_body_size 100m; |
排查入口:先看 access 日志里的状态码、$upstream_addr 和耗时,再看 error 日志。比如 access 日志里是 502,$upstream_addr 指向 10.0.1.12:8080,error 日志同时出现 connect() failed (111: Connection refused) while connecting to upstream,基本能判断是后端端口没监听或进程挂了;如果是 504,$upstream_response_time 接近 30.000,更像后端处理太慢或外部依赖卡住。只看浏览器里一个 502,无法判断是 Nginx、网络还是后端的问题。