proxy 反向代理

proxy_pass 带不带 URI 的本质区别、默认 Host 的坑、缓冲与超时、proxy_next_upstream 的重试语义。

作者 David更新于 第 9 篇(共 21 篇)

proxy_pass 的斜杠之争

这是 nginx 配置里最著名的一个坑。官方规则原文:

If the proxy_pass directive is specified with a URI, then when a request is passed to the server, the part of a normalized request URI matching the location is replaced by a URI specified in the directive.

一句话:带 URI 就替换,不带 URI 就透传。

带 URI(含末尾 /)→ 替换

location /name/ {
    proxy_pass http://127.0.0.1/remote/;
}
请求 /name/test  →  上游收到 /remote/test
     ^^^^^^ 被替换成 /remote/

不带 URI → 透传

location /some/path/ {
    proxy_pass http://127.0.0.1;
}
请求 /some/path/test  →  上游收到 /some/path/test
     完整 URI 原样传过去

判断标准是「proxy_pass 里主机后面有没有路径」,不是「有没有斜杠」这么简单:

写法是否带 URI效果
proxy_pass http://backend;否透传完整 URI
proxy_pass http://backend/;是(URI 是 /)/name/test → /test
proxy_pass http://backend/api/;是/name/test → /api/test

http://backend; 与 http://backend/; 行为完全不同——后者那个孤零零的 / 就是一个 URI,会把 location 前缀吃掉。这一个字符的差别造成过无数 404。

三种不能带 URI 的情况

官方明确列出:

  • 正则 location 与命名 location——必须用不带 URI 的形式。
  • rewrite ... break 之后——指令里的 URI 被忽略,传的是改写后的完整 URI。
  • proxy_pass 里含变量时——指定的 URI 原样传递,替换原 URI。
# 正则 location:不能带 URI
location ~ ^/api/(.*)$ {
    proxy_pass http://backend;      # ✅
    # proxy_pass http://backend/$1; ← 含变量,行为又不同
}

含变量时行为特殊,且变量形式不会做上游组解析(后面 upstream 章会讲),慎用。

Host 头默认值的坑

官方给的默认:

proxy_set_header Host       $proxy_host;
proxy_set_header Connection close;

默认传给上游的 Host 是 $proxy_host——即 proxy_pass 里写的那个主机名,不是客户端请求的 Host。

后果:上游若按 Host 做虚拟主机路由,会全部落到默认站点;应用生成的绝对 URL 也会带错域名。

所以几乎所有反代配置的第一行都是:

proxy_set_header Host $host;

HTTP/2 场景下,:authority 伪头默认同样用 $proxy_host,除非显式设置了 Host。见 HTTP/2 伪头。

标准转发头四件套

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_add_x_forwarded_for = 原有 X-Forwarded-For 值 + , + $remote_addr,实现逐跳追加。

边缘节点应该覆写而不是追加。 最外层 nginx 直接面对公网时,客户端可以自己伪造 X-Forwarded-For,追加会把伪造值留在最左边。边缘应写:

proxy_set_header X-Forwarded-For $remote_addr;   # 覆写,丢弃客户端自带的

只有在内层(前面确定有可信代理)才用 $proxy_add_x_forwarded_for。详见 HTTP 代理字段的安全模型。

别忘了数组型继承:任何一个内层 location 写了 proxy_set_header,这四条全失效。

proxy_http_version

语法:    proxy_http_version 1.0 | 1.1;
默认:    proxy_http_version 1.1;    ← 1.29.7 起
上下文:  http, server, location

默认值在 1.29.7 改了:原先是 1.0,现在是 1.1。

网上几乎所有教程都会告诉你「用 upstream keepalive 必须手动写 proxy_http_version 1.1;」——那是针对旧默认值的建议。新版本已经不必,但显式写上仍然无害,且对旧版本部署是必要的。

配合 upstream keepalive 时还需要清掉 Connection 头:

proxy_http_version 1.1;
proxy_set_header   Connection "";

因为默认会发 Connection: close,与长连接冲突。

proxy_buffering

语法:    proxy_buffering on | off;
默认:    proxy_buffering on;
上下文:  http, server, location
状态行为
on(默认)nginx 先把上游响应完整读进缓冲,再发给客户端
off收到多少同步转发多少

缓冲的价值:快速释放上游连接。慢客户端下载时,上游不必陪着等——nginx 已经把数据收下来了。这对后端是宝贵的保护。

缓冲区配置:

proxy_buffer_size   4k;      # 存响应头的那块(默认 4k|8k)
proxy_buffers       8 4k;    # 存响应体(默认 8 个 4k|8k)
proxy_busy_buffers_size 8k;

超出缓冲的部分写临时文件(proxy_max_temp_file_size、proxy_temp_file_write_size 控制)。

什么时候要关掉:SSE、流式响应、长轮询、大文件实时转发——这些场景缓冲会让数据卡在 nginx 里不下发。

location /events {
    proxy_pass       http://backend;
    proxy_buffering  off;
    proxy_cache      off;
}

上游也可以用响应头 X-Accel-Buffering: no 单次关闭缓冲,无需改 nginx 配置。

超时三兄弟

指令默认含义
proxy_connect_timeout60s与上游建立连接
proxy_send_timeout60s两次写操作之间
proxy_read_timeout60s两次读操作之间

proxy_read_timeout 不是「响应总时长上限」,而是「两次读之间的最大间隔」。上游只要持续有数据流出,跑一小时也不会超时;反之上游思考 61 秒再吐第一个字节就会 504。

官方另有一条限制:proxy_connect_timeout 通常不能超过 75 秒(受系统 TCP 连接超时约束)。

慢接口(大报表、AI 推理)需要调大 proxy_read_timeout:

location /slow-api/ {
    proxy_pass         http://backend;
    proxy_read_timeout 300s;
}

proxy_next_upstream

语法:    proxy_next_upstream error | timeout | invalid_header |
                             http_500 | http_502 | http_503 | http_504 |
                             http_403 | http_404 | http_429 |
                             non_idempotent | off ...;
默认:    proxy_next_upstream error timeout;
上下文:  http, server, location

默认只在 error 与 timeout 时换下一台。500/502/503 默认不重试——想要就显式加:

proxy_next_upstream error timeout http_502 http_503 http_504;

三条重要语义:

一、http_403 / http_404 永不算失败尝试。 即使列出来触发换机,也不会计入该上游的失败次数(不影响 max_fails)。

二、non_idempotent(1.9.13+)才会重试 POST。 默认不重试非幂等方法——这与 HTTP 的幂等性语义一致。加上它意味着你接受重复提交的风险,必须有幂等键兜底。

三、已经开始给客户端发数据就不能重试。 官方原话:request can only be retried if nothing sent to client yet。

限制重试规模:

proxy_next_upstream_tries   3;    # 默认 0 = 不限
proxy_next_upstream_timeout 10s;  # 默认 0 = 不限

默认不限次数意味着上游全挂时会把所有机器试一遍——生产建议设个上限。

上游相关变量

变量含义
$upstream_addr实际连的上游地址(重试过会是逗号/冒号分隔的列表)
$upstream_status上游返回的状态码
$upstream_response_time上游响应耗时
$upstream_connect_time建连耗时
$upstream_cache_status缓存命中状态,见缓存章
$upstream_http_名字上游响应头

把它们记进日志是排查性能问题的基础——$request_time 减去 $upstream_response_time 就是 nginx 自身与网络的开销。

一份完整反代配置

upstream backend {
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
    keepalive 32;
}

server {
    listen 443 ssl;
    http2  on;
    server_name api.example.com;

    location / {
        proxy_pass http://backend;

        proxy_http_version 1.1;
        proxy_set_header   Connection "";

        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $remote_addr;    # 边缘:覆写
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout 5s;
        proxy_read_timeout    60s;

        proxy_next_upstream       error timeout http_502 http_503;
        proxy_next_upstream_tries 2;
    }

    location /events {
        proxy_pass      http://backend;
        proxy_buffering off;
        proxy_read_timeout 1h;
    }
}

下一篇:upstream 负载均衡。

本页目录