#DevOps

Nginx 反向代理实战

Nginx 是部署最常见的反代/网关,但很多人只会抄一段配置。本文讲清 Nginx 作反代的核心:location 匹配规则、proxy_pass、请求头透传(X-Forwarded-*)、负载均衡、HTTPS/TLS、性能调优(worker/buffer/gzip)、以及常见坑(WebSocket、大文件、流式)。

本文要点

  • 主题聚焦:DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年6月28日·更新于 2026年7月23日

我面试过不少自称"会用 Nginx"的候选人,让他们手写一段把 /api/ 转发到后端、并把真实 IP 透传过去的配置,八成写不对。不是忘了 proxy_set_header,就是 proxy_pass 末尾多写了一个 /,导致后端路由 404。Nginx 反代是部署里最常见的一环,但"能跑"和"配对"之间隔着一整本手册。这篇文章把反代最常踩的坑一次讲透,配置按生产可用标准写。

一、反向代理到底解决了什么

把应用直接暴露在 443 端口,是新人最天真的想法。一旦这么做,你就同时丢掉了:隐藏后端拓扑的能力、统一 TLS 卸载的入口、横向扩容时的负载分发、以及一层可以独立做缓存和限流的网关。

反代的核心价值,按重要性排:

  1. 隐藏后端:外界只看到 Nginx 的 IP 和端口,后端服务可以监听内网、用任意端口、跑多个实例,甚至滚动重启发布,客户端完全无感知。
  2. 统一入口与 TLS 卸载:证书只装在 Nginx 一处,后端走明文 HTTP 即可。续签、OCSP、HTTP/2、压缩全在网关层搞定,后端不用关心。
  3. 负载均衡:一个域名后挂多台后端,Nginx 按策略分发,某台挂了自动剔除。
  4. 缓存与限流:热点响应在 Nginx 缓存,后端压力直接砍半;突发流量在网关层限流,后端不被打爆。

一句话:Nginx 是你的应用面向公网的那道闸,不是可选的"转发器"。 它让后端可以专注业务,而不是去处理 TLS、路由分发、流量整形这些事。

二、location 匹配:新人踩坑最多的地方

反代第一步是"把哪个路径转到哪个后端",这就是 location。但它的匹配规则不是"从上往下命中第一个",而有一套优先级。80% 的"为什么我的 location 没生效",都是没搞懂这套优先级。

四类匹配符,按优先级从高到低:

text
=        精确匹配(最高,命中即停)
^~       前缀匹配(命中后不再走正则)
~ / ~*   正则匹配(区分 / 不区分大小写)
/xxx     普通前缀匹配(最低,最长前缀胜出)

实际选择顺序:先看 = 有没有命中;没有就记下最长的 ^~ 前缀;再看正则,正则按出现顺序第一个命中即用;正则都不命中,才用之前记下的最长普通前缀。一个典型反例:

nginx
location /api { proxy_pass http://backend; }   # 你以为它会处理 /api/users
location ~ \.php$ { ... }                       # 结果正则优先级更高

请求 /api/test.php 会被正则那行抢走。正则永远压过普通前缀,这是新人最容易错的地方。能用前缀解决的,别图省事写正则——前缀是哈希查找,正则是逐条线性扫描,性能也差。

三、proxy_pass 与请求头透传:让后端拿到真实 IP

location 决定转发到哪,proxy_pass 决定怎么转发。这里有个最经典的坑:末尾带不带 /,行为完全不同。

nginx
location /api/ {
    proxy_pass http://backend;     # 不带 URI:原样转发  /api/users → /api/users
    proxy_pass http://backend/;    # 带 URI:替换前缀   /api/users → /users
    proxy_pass http://backend/v2/; #                  /api/users → /v2/users
}

规则一句话:proxy_pass 带 URI(哪怕只是一个 /),就会把 location 匹配到的前缀替换掉;不带,就原样转发。注意,如果 location 用了正则,proxy_pass 就不能带 URI,只能写裸 host:port。

后端能拿到真实 IP,靠的是请求头透传。Nginx 默认不告诉后端客户端是谁,后端看到的全是 Nginx 的内网 IP。生产配置必须把这几行写上:

nginx
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;

X-Forwarded-For 是一条链,每经过一层代理就追加一个 IP。$proxy_add_x_forwarded_for 会把请求里已有的 XFF 和当前 $remote_addr 拼起来,正是你要的语义。X-Forwarded-Proto 告诉后端"原始请求是 https",后端才能正确生成 https:// 的回跳 URL。

后端侧也要配合:框架(Express 的 trust proxy、Django 的 SECURE_PROXY_SSL_HEADER)需要显式声明"我信任前面的代理",否则这些头可被任意客户端伪造。反代链路上,信任边界要划清:Nginx 可信,客户端不可信。

四、负载均衡:upstream 多后端

单台后端扛不住时,upstream 把多台后端捆成一个虚拟服务:

nginx
upstream backend {
    least_conn;                         # 调度策略:连接数最少者优先
    server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.3:8080 weight=2;      # 权重:收到的请求翻倍
    keepalive 32;                       # 到后端的长连接池
}

调度策略记住三种就够:

  • 轮询(默认):依次分发,后端配置一致时用;
  • ip_hash:同一客户端 IP 固定打到同一台,做会话粘性。代价是节点增减时 hash 大规模迁移——现代应用更该把会话存 Redis,而不是依赖 IP 粘性;
  • least_conn:最少连接数优先,后端处理能力不均时比轮询更合理。

max_fails + fail_timeout 是被动健康检查:某台在 fail_timeout 内连续失败 max_fails 次,就临时摘掉。注意是被动,Nginx 不会主动探测;主动健康检查需要商业版或第三方模块,开源版只能等错。

keepalive 32 是到后端的长连接池,避免每次请求都重建 TCP。开了它,别忘了在 location 里同时加 proxy_http_version 1.1; 和 proxy_set_header Connection "";,否则 keepalive 不生效。

五、HTTPS/TLS:证书、跳转、HTTP/2

TLS 必须在网关层卸载。最小生产配置:

nginx
server {
    listen 443 ssl;
    http2 on;                         # nginx 1.25.1+ 新语法,替代旧的 listen 443 ssl http2
    server_name example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
}

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;   # HTTP → HTTPS 永久跳转
}

几个要点:证书路径用 fullchain.pem(带中间证书),别用 cert.pem,否则部分客户端因链不全报错。HTTP/2 在 1.25.1 之后用 http2 on; 指令,不再写在 listen 里。证书续签交给 certbot renew + cron 自动跑,别等过期再手动搞,这是运维事故高发区。

想用 Brotli 替代 gzip,需要 ngx_brotli 模块且只对 HTTPS 生效,压缩率好 10%~20%,但要自己编译。流量不是特别敏感的团队,gzip 就够。

六、性能调优:worker、buffer、gzip、静态缓存

nginx
worker_processes auto;                 # 和 CPU 核数对齐
events { worker_connections 10240; }   # 单 worker 的最大连接数

http {
    sendfile on;
    tcp_nopush on;
    keepalive_timeout 65;

    gzip on;
    gzip_min_length 1k;
    gzip_comp_level 5;                 # 4–6 是甜点区,9 基本只烧 CPU
    gzip_types text/plain application/json application/javascript text/css;

    location ~* \.(js|css|png|jpg|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # 反代 buffer(默认偏小,大响应会被切到临时文件)
    proxy_buffer_size       16k;
    proxy_buffers           8 16k;
    proxy_busy_buffers_size 32k;
}

调优的核心是别瞎调。worker_processes auto 让 Nginx 自己跟 CPU 对齐,别手填;worker_connections 按并发预期调大,但要核对系统 ulimit -n,否则 Nginx 起不来。gzip_comp_level 超过 6 收益可忽略。

反代的 proxy_buffers 是常被忽略的瓶颈:默认值很小,后端返回大 JSON 时会被写到磁盘临时文件,延迟骤增。反代大响应体 API,记得调大这几个值。

静态资源带文件指纹的(文件名含 hash)可以 immutable,浏览器永不再回源;没指纹的 HTML 用短缓存或不缓存。

七、常见坑:WebSocket、大文件、流式

最后是四个生产事故高频来源:

1. WebSocket 不通。 Nginx 默认用 HTTP/1.0 跟后端通信,且会丢掉 Upgrade / Connection 头。需要协议升级的(WebSocket、gRPC)必须显式透传:

nginx
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_read_timeout 3600s;         # 长连接,别让默认 60s 把它掐了
}

2. 大文件上传 413。 Nginx 默认 client_max_body_size 只有 1m,上传稍大就报 413。按业务显式写:client_max_body_size 100m;,放在 http / server / location 任一层级都行。

3. 流式输出被吞。 LLM 的 SSE、逐字输出、Transfer-Encoding: chunked 的流式响应,在默认开启的 proxy_buffering 下会被缓存完才一次性吐出——用户体验直接崩成"等 10 秒然后全部出现"。流式接口必须关:

nginx
location /stream {
    proxy_pass http://backend;
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding on;
}

4. 后端拿到的 IP 是 Nginx 的。 就是第三节说的请求头透传没配。线上日志里全是同一个内网 IP,风控、限流、审计全部失真——这是最基础也最常见的配置缺失。

小结

  1. location 优先级是 = > ^~ > 正则 > 普通前缀,正则永远压过普通前缀,能用前缀就别写正则。
  2. proxy_pass 末尾带不带 / 行为完全不同:带 URI 会替换 location 前缀,不带则原样转发——这是最隐蔽的路由 404 来源。
  3. 请求头透传(X-Real-IP / X-Forwarded-For / X-Forwarded-Proto / Host)是反代的良心,不配后端就瞎了;后端侧还要显式声明信任的代理边界。
  4. WebSocket、大文件、流式是三大高频事故源:Upgrade 头、client_max_body_size、proxy_buffering off,分别对应长连接、上传、SSE。

反代配置的黄金法则一句话:永远假设你的配置是错的,然后用 curl -v 和后端日志逐项验证。 Nginx 不报错,不代表它做对了。

Related

相关推荐