Skip to content

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 haship_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=3fail_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_timeoutTCP 握手阶段——后端端口是否可达后端没起或端口不通
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_timeNginx 从收到请求到完成响应的总耗时(秒)
$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、网络还是后端的问题。