Nginx反向代理配置详解:多站点转发与后端服务部署

一台 4 核 8G 的云服务器上,你同时跑了公司官网(Tomcat 8080)、订单接口(Go 服务 9000)、运营后台(Node.js 3000)和一个老旧的 PHP 站点(PHP-FPM)。防火墙只愿意放行 80 和 443,客户也不接受访问 example.com:8080 这种带端口的网址。这时候最省事、也最可靠的做法,就是让 Nginx 站在最前面当统一入口:对外只有一个域名和一个端口,对内按域名或路径把请求转发给不同的后端服务。这就是反向代理(Reverse Proxy)最典型的落地场景。

本文从“反向代理到底替你做了什么”讲起,逐层展开 proxy_pass、请求头透传、upstream 负载均衡、多站点分发的两种写法,以及上线前必须做的检查。所有配置均以 Nginx 1.18+(Ubuntu 20.04 / CentOS 7 默认源版本)为准,路径采用 /etc/nginx/nginx.conf 与 /etc/nginx/conf.d/ 的通用约定。

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

很多新手把反向代理简单理解成“转发端口”,其实它承担的职责远不止转发。理解清楚这五件事,后面配置时才不会乱:

  • 端口收敛:对外只暴露 80/443,后端服务可以只监听 127.0.0.1,从根本上减少攻击面。
  • 统一入口与域名路由:一台机器上托管多个域名,靠 server_name 区分,互不干扰。
  • TLS 终结:证书只在 Nginx 上配置一次,后端全部走 HTTP,省去每个服务自己配证书的麻烦。
  • 负载分发:通过 upstream 把流量摊到多个后端实例,并自动剔除故障节点。
  • 缓冲与抗压:Nginx 先把后端响应读完再慢慢吐给慢速客户端,避免慢连接把后端进程拖死。

1.1 正向代理与反向代理的区别

两个概念经常被混着说,判断标准很简单:代理的是谁。正向代理代理客户端,服务端不知道真实用户是谁;反向代理代理服务端,客户端不知道真实服务是谁。

对比项正向代理反向代理
代理对象客户端(浏览器、内网机器)服务端(网站、接口)
典型用途内网上网、访问加速、隐藏访问者发布网站、负载均衡、隐藏后端
客户端是否知情需要手动配置代理地址无需配置,以为直连
典型软件Squid、ShadowsocksNginx、HAProxy、Envoy
配置关键指令resolver + proxy_pass $scheme://$host$request_uriproxy_pass http://backend; + proxy_set_header

1.2 一个典型的多站点拓扑

公网客户端
    │  https://www.a.com        https://api.a.com
    ▼
┌─────────────────────────────────────────┐
│ Nginx  (80/443,TLS 终结,反向代理)       │
│  www.a.com      → 127.0.0.1:8080 (Tomcat)│
│  api.a.com/api  → 127.0.0.1:9000 (Go)    │
│  admin.a.com    → 127.0.0.1:3000 (Node)  │
└─────────────────────────────────────────┘

注意拓扑里后端全部绑定 127.0.0.1。这是多站点部署的第一条安全纪律:数据库、缓存、内部接口一律不监听 0.0.0.0。

二、最小可用配置:proxy_pass 与请求头透传

先写一个能跑起来的最小配置,放在 /etc/nginx/conf.d/www.a.com.conf:

server {
    listen 80;
    server_name www.a.com;

    location / {
        proxy_pass http://127.0.0.1:8080;

        # 透传客户端真实信息,缺一不可
        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;
    }
}

逐条解释几个容易漏掉的点:

  • Host $host:默认 Nginx 会把 Host 改写成 proxy_pass 里的地址。后端若按 Host 做虚拟主机路由,不写这一条会全部落到默认站点。
  • X-Real-IP / X-Forwarded-For:后端日志里想看到真实访客 IP 就必须透传。$proxy_add_x_forwarded_for 会在原有值后追加,比直接写 $remote_addr 更安全。
  • X-Forwarded-Proto $scheme:后端据此判断当前是 http 还是 https,否则生成的跳转链接可能退化成 http,造成循环重定向。
  • proxy_http_version 1.1 + Connection "":配合 upstream 里的 keepalive 使用,能显著减少 TIME_WAIT 和握手开销。

2.1 proxy_pass 结尾带不带“/”,行为完全不同

这是 Nginx 反向代理配置中最高频的坑,务必记牢:带 URI(以 / 结尾)时会替换掉 location 匹配到的前缀,不带时原样透传完整 URI。

locationproxy_pass 写法请求 /api/user?id=1 转发结果
location /api/proxy_pass http://127.0.0.1:9000;/api/user?id=1(原样透传)
location /api/proxy_pass http://127.0.0.1:9000/;/user?id=1(前缀被替换)
location /apiproxy_pass http://127.0.0.1:9000/v1/;/v1/user?id=1
location ~ ^/api/(.*)$proxy_pass http://127.0.0.1:9000/$1;由正则捕获组决定

实践建议:如果后端服务的路径和前端访问路径一致,就不要在 proxy_pass 后面加 /;只有在需要“剥掉前缀”时才加。

三、upstream 与负载均衡:从单实例到多实例

当后端跑多个实例时,把地址写死在每个 location 里会失控。upstream 块把后端池单独定义,还能配置健康检查和调度策略。

upstream backend_api {
    # 默认轮询;least_conn 适合长连接/耗时不均的接口
    least_conn;

    server 127.0.0.1:9001 weight=3 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:9002 weight=1 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:9003 backup;

    keepalive 32;
}

server {
    listen 80;
    server_name api.a.com;

    location / {
        proxy_pass http://backend_api;
        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 "";
    }
}

3.1 调度策略与状态参数速查

指令/参数含义适用场景
(默认)轮询 round-robin依次分发,配合 weight 加权后端性能接近、请求耗时均匀
least_conn优先给当前连接数最少的节点长连接、请求耗时差异大
ip_hash按客户端 IP 前三段哈希固定节点无 Redis 的会话保持(不推荐长期依赖)
hash $request_uri consistent按 URI 一致性哈希后端自带本地缓存,提高命中率
weight=N权重,默认 1机器配置不一致时按比例分配
max_fails / fail_timeout30s 内失败 3 次则摘除 30s(默认值即 1 次 / 10s)故障自动摘除
backup仅当主节点全挂时接管灾备实例
down手动标记为不可用灰度摘除、发版维护
keepalive 32每个 worker 保持的空闲长连接数高并发下减少握手

提醒:max_fails 与 fail_timeout 是“被动健康检查”,只有真实请求失败才会触发。开源版 Nginx 没有主动心跳探测,如需主动探测可用 nginx_upstream_check_module 第三方模块,或直接上 Nginx Plus。

四、多站点转发的两种写法

4.1 按域名分发(推荐)

不同业务用不同域名,各写一个 server 块,彼此完全隔离,日志、证书、限流都能单独配。这是多站点部署的首选方式。

server {
    listen 80;
    server_name www.a.com a.com;
    access_log /var/log/nginx/www.a.com.access.log main;
    location / { proxy_pass http://127.0.0.1:8080; include /etc/nginx/snippets/proxy_headers.conf; }
}

server {
    listen 80;
    server_name admin.a.com;
    access_log /var/log/nginx/admin.a.com.access.log main;
    location / { proxy_pass http://127.0.0.1:3000; include /etc/nginx/snippets/proxy_headers.conf; }

    # 后台只允许办公网访问
    allow 203.0.113.0/24;
    deny  all;
}

把重复的 proxy_set_header 抽到 /etc/nginx/conf.d/proxy_headers.conf,用 include 引入,配置会清爽很多,也避免漏配。

4.2 按路径分发

只有一个域名、需要把 /api/ 打到独立后端时用路径分发。注意 WebSocket 需要额外升级头:

server {
    listen 80;
    server_name www.a.com;

    location / {
        root /data/www/a;
        index index.html;
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:9000;
        include /etc/nginx/snippets/proxy_headers.conf;
    }

    location /ws/ {
        proxy_pass http://127.0.0.1:9001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
    }
}

4.3 location 匹配优先级

多站点配置出 Bug,八成是 location 没按你以为的方式匹配。优先级从高到低:

  1. = 精确匹配,命中即返回,如 location = /health
  2. ^~ 前缀匹配且不再往下看正则,如 location ^~ /static/
  3. ~ / ~* 正则匹配(区分/不区分大小写),按配置出现顺序取第一个
  4. 普通前缀匹配,取最长的那条
  5. / 兜底

判断口诀:先看有没有精确和 ^~,再看正则,最后比前缀长度。

五、上线前的检查与验证流程

改完配置直接 reload 出事是运维常态。按下面清单走一遍,能挡掉绝大多数低级错误。

  1. 语法与完整配置检查:nginx -t 校验语法,nginx -T 打印合并后的全部配置(排查 include 覆盖问题时尤其有用)。
  2. 平滑加载:systemctl reload nginx 或 nginx -s reload,不要用 restart,避免断连。
  3. 确认后端真的在听:ss -lntp | grep -E '8080|9000|3000',重点看监听地址是不是 127.0.0.1 还是被误绑到公网。
  4. 本机直连后端验证:curl -H 'Host: www.a.com' -I http://127.0.0.1:8080/,先确认后端本身健康。
  5. 走代理验证:curl -I http://www.a.com/,对比状态码与响应头。
  6. 确认 IP 透传生效:在后端打印 X-Forwarded-For,或用 curl -H 'X-Real-IP: 1.2.3.4' 配合日志观察。
  7. 看错误日志:tail -f /var/log/nginx/error.log,出现 connect() failed (111: Connection refused) 说明后端没起;(110: Connection timed out) 说明被防火墙或超时拦了。

5.1 顺手加上的安全与性能项

# 隐藏版本号
server_tokens off;

# 限制客户端请求体大小,防止大文件上传打爆后端
client_max_body_size 20m;

# 关闭代理重定向改写(后端返回 301 时交给客户端自己跳)
proxy_redirect off;

# 失败重试策略:超时/502 时换节点,但 POST 不重试避免重复下单
proxy_next_upstream error timeout http_502;
proxy_next_upstream_tries 2;

小结

Nginx 反向代理的核心就三件事:用 proxy_pass 把请求送到正确的地方,用 proxy_set_header 把客户端信息完整交给后端,用 upstream 把多个实例组织成一个池子。多站点部署优先按域名拆分 server 块,其次才考虑路径分发。

最后列一份避坑清单,建议贴在工位上:

  • proxy_pass 结尾的 / 决定路径是否被替换,改之前先想清楚。
  • 漏配 Host 头会导致后端虚拟主机路由错乱;漏配 X-Forwarded-Proto 会导致 HTTPS 站点无限重定向。
  • 换了 proxy_pass 地址后必须 nginx -t 再 reload,不要直接 restart。
  • 后端服务一律监听 127.0.0.1,端口不开防火墙,代理即边界。
  • proxy_read_timeout 默认 60s,明显短于后端业务耗时的接口(导出、报表)要单独调大,否则必然 504。
  • 用 ip_hash 做会话保持是权宜之计,长期方案是将会话外置到 Redis。
  • 日志按 server_name 分文件,否则多站点共用一个 access.log,排查时根本无法定位。

关键词回顾:Nginx 反向代理配置、多站点转发、proxy_pass、upstream 负载均衡、proxy_set_header 透传、后端服务部署。