一台服务器上跑了三个服务:官网程序监听在 8080,管理后台是另一个语言写的跑在 3000 端口,还有个接口服务在 9001。这些端口都不想暴露给用户,用户只知道 example.com 这一个域名。解决办法就是反向代理——让用户统一访问 Nginx 的 80 和 443,由 Nginx 根据域名或 URL 路径,把请求转发到内部对应的服务。
反向代理看起来只是一个 proxy_pass 指令,实际用起来坑不少:末尾斜杠差一个字符转发路径就全错,不加请求头后端拿不到真实客户端 IP,WebSocket 握手会失败,转发的动态页面里的跳转会跳到内部地址。这篇文章把这些细节逐个讲清楚,并给出可以直接复制改的配置模板。
文中命令区分 RHEL、CentOS、Rocky 系(dnf 或 yum,配置放在 /etc/nginx/conf.d/,防火墙 firewalld)与 Debian、Ubuntu 系(apt,配置常放 /etc/nginx/sites-available/ 并通过符号链接启用,防火墙 ufw)。
反向代理是什么,和正向代理有什么区别
正向代理代理客户端。客户端主动配置代理服务器,由代理替它去访问目标网站。正向代理隐藏的是客户端身份,典型例子是内网出口代理。
反向代理代理服务端。客户端对此毫不知情,它以为自己访问的就是目标服务器本身,实际上请求被代理接收后转发给了后端真实服务。反向代理隐藏的是服务端结构。
对一个网站来说,反向代理带来几个直接收益:
统一入口。 外部只暴露 80 和 443,内部服务可以随意使用非标准端口。这既减少了对外暴露的攻击面,也让端口管理不再混乱。相反,如果每个服务各自对外开一个端口,防火墙规则和用户书签都会失控。
集中 TLS 终结。 证书只在 Nginx 上配置一次,Nginx 与后端之间走内网 HTTP。后端服务不需要各自管理证书,也不用处理 TLS 握手开销。
负载均衡。 同一个后端可以有多个实例,Nginx 按策略分发,某个实例故障时自动摘除。
缓冲与容错。 Nginx 可以先完整接收后端响应再发给客户端,这样慢速客户端不会长期占用后端进程——后端快速把响应交给 Nginx 就能继续服务下一个请求。
静态资源与压缩、缓存下沉到边缘。 静态文件由 Nginx 直接返回,不必劳烦后端应用。
代价也要清楚:多一跳会带来少量延迟,通常在同一内网里只有零点几到几毫秒的量级;日志里如果不做处理,记录的客户端 IP 会全是 Nginx 自身的地址;出问题时排查路径变长,需要在 Nginx 日志和后端日志之间对照。
proxy_pass 语法与末尾斜杠的坑
proxy_pass 后面的 URL 是否带 URI 路径,决定了 Nginx 如何改写请求 URI。这是最容易出错的地方,必须记住规则。
规则一:proxy_pass 不带 URI(只有协议、主机、端口),Nginx 会把完整的原始请求 URI 原样转发过去。
location /api/ {
proxy_pass http://127.0.0.1:9001; # 没有末尾斜杠,不带 URI
}
请求 /api/user/list 转发出去是 http://127.0.0.1:9001/api/user/list,location 的前缀被保留了。
规则二:proxy_pass 带了 URI(哪怕只是一个斜杠),Nginx 会把 location 中匹配的部分替换成该 URI。
location /api/ {
proxy_pass http://127.0.0.1:9001/; # 注意末尾的斜杠
}
请求 /api/user/list 转发出去是 http://127.0.0.1:9001/user/list,location 前缀 /api/ 被替换成了 /。
再举一个带具体路径的例子:
location /api/ {
proxy_pass http://127.0.0.1:9001/v2/;
}
请求 /api/user/list 变成 http://127.0.0.1:9001/v2/user/list。
用一张表对照:
| location | proxy_pass | 请求 URI | 转发后的 URI |
|---|---|---|---|
/api/ |
http://127.0.0.1:9001 |
/api/user/list |
/api/user/list |
/api/ |
http://127.0.0.1:9001/ |
/api/user/list |
/user/list |
/api/ |
http://127.0.0.1:9001/v2/ |
/api/user/list |
/v2/user/list |
/api |
http://127.0.0.1:9001/ |
/api/user/list |
/user/list |
实际场景中的选择很简单:如果后端服务的路径规划和外部 URL 完全一致,用不带斜杠的形式;如果后端本身不带 /api 前缀(比如后端直接提供 /user/list),就用带斜杠的形式让它被剥离。
还有一个常被忽略的限制:在带变量的 proxy_pass 里(比如 proxy_pass http://$backend$request_uri;),行为又有差异,且如果使用了变量并指向 upstream,需要配合 resolver 指定 DNS 服务器,否则会报"no resolver defined"错误。除非确实需要动态计算后端地址,否则不要引入变量。
必须设置的请求头
后端看不到客户端的真实信息,这是反向代理最常见的问题:程序记录的 IP 全是 127.0.0.1,生成的链接是 http 而不是 https,或者重定向到了内部域名。解决办法是显式传递请求头。
location / {
proxy_pass http://backend_app;
proxy_http_version 1.1;
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_set_header X-Forwarded-Host $host;
}
逐个说明:
Host。 后端如果需要根据域名做虚拟主机路由或生成绝对链接,必须拿到客户端请求的原始 Host。不设置时 Nginx 默认传的是 proxy_pass 里的主机名(如 backend_app 或 127.0.0.1:9001),会导致后端匹配错虚拟主机。变量方面,$host 是不带端口的规范化主机名,$http_host 保留原始请求里的 Host(含端口),多数情况用 $host 即可。
X-Real-IP。 直接给后端一个"客户端 IP"字段,值为 $remote_addr,即与 Nginx 建立 TCP 连接的对端地址。
X-Forwarded-For(简称 XFF)。 格式为逗号加空格分隔的 IP 列表,每经过一层代理就追加一个。用 $proxy_add_x_forwarded_for 而不是 $remote_addr 的原因就在这里:它会保留客户端传来的已有值并在后面追加当前对端地址。取真实 IP 时应从严到宽地解析这个链表,而不是取第一个值——第一个值是客户端可以伪造的。
X-Forwarded-Proto。 值为 $scheme,也就是 http 或 https。这是关键一项:Nginx 终结 TLS 后到后端走的是 HTTP,后端以为请求是明文的,于是生成 http:// 的链接或拒绝会话 Cookie,导致混合内容警告或反复重定向。凡是"开了 HTTPS 之后后台登录循环跳转"的问题,优先查这一项。
Nginx 侧还有一个配套模块 ngx_http_realip_module,可以在 Nginx 这一层就把 $remote_addr 改写成真实 IP,这样日志和后端都无需特殊处理:
set_real_ip_from 10.0.0.0/8; # 信任的上游代理网段
set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For; # 从哪个头取真实 IP
real_ip_recursive on; # 递归剥离受信任地址,取最后一个不受信任的
安全提醒:set_real_ip_from 只应填写你确实控制的可信代理地址。如果随意信任任意来源,攻击者伪造一个 XFF 头就能让 Nginx 记录错误的客户端 IP,进而绕过基于 IP 的访问控制。
用 upstream 定义后端组
直接在 proxy_pass 里写 IP 加端口能工作,但不利于扩展。定义 upstream 可以复用、可以配置多台做负载均衡、可以做健康检查参数。
upstream backend_app {
server 127.0.0.1:9001 weight=3 max_fails=3 fail_timeout=10s;
server 127.0.0.1:9002 weight=1 max_fails=3 fail_timeout=10s;
server 127.0.0.1:9003 backup;
keepalive 32;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend_app;
proxy_http_version 1.1;
proxy_set_header Connection ""; # 配合 upstream keepalive 复用连接
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;
}
}
server 指令的常用参数:
| 参数 | 含义 | 备注 |
|---|---|---|
weight=N |
轮询权重,默认为 1 | 新机器性能好可以给更高权重 |
max_fails=N |
在 fail_timeout 内失败 N 次判定为不可用 | 与 fail_timeout 配合使用 |
fail_timeout=N |
判定时长窗口,也是标记为不可用后的暂停时长 | 单位支持 s、m |
backup |
备份节点,主节点全不可用时才启用 | 适合做低成本灾备 |
down |
标记为永久不可用,常用于临时摘除 | 手动维护某台机器时使用 |
注意 upstream 中的 keepalive 与 proxy_http_version 1.1 加 proxy_set_header Connection "" 是配套的三件套,用来复用 Nginx 到后端的连接,减少握手开销。如果缺少后面两项,连接复用不会生效。
Nginx 开源版的健康检查是被动式的:请求转发失败达到阈值才判定节点故障。主动探测(health_check 指令)属于商业版或第三方模块能力。
负载均衡策略怎么选
| 策略 | 指令 | 行为 | 适合场景 |
|---|---|---|---|
| 轮询 | 默认 | 按权重依次分发 | 后端无状态、性能一致 |
| 最少连接 | least_conn; |
优先给当前活跃连接最少的节点 | 请求处理时长差异大,比如有快慢两种接口 |
| 源地址哈希 | ip_hash; |
按客户端 IP 前三段哈希固定到某节点 | 无集中会话存储时的简单会话保持 |
| 一致性哈希 | hash $request_uri consistent; |
按指定变量哈希,节点增减时影响范围小 | 需要缓存亲和性的场景 |
| 随机 | random two least_conn; |
随机挑两个再取连接少的 | 大规模节点时的轻量方案 |
upstream backend_app {
least_conn;
server 127.0.0.1:9001;
server 127.0.0.1:9002;
}
选策略的核心原则:优先让应用本身做到无状态(会话放 Redis 或数据库,文件放对象存储),这样默认轮询就够用,扩缩容最自由。只有当短期无法改造时,才用 ip_hash 之类的方案做会话保持——它的问题在于同一出口 IP 的用户(比如公司 NAT 后面的人)会全被分到同一台机器,负载可能严重不均。
超时与缓冲参数
默认超时值偏保守,遇到慢接口要显式调整。
location /api/ {
proxy_pass http://backend_app;
proxy_connect_timeout 5s; # 与后端建立 TCP 连接的超时,不宜过长
proxy_send_timeout 30s; # 向后端发送请求的超时
proxy_read_timeout 60s; # 等待后端响应的超时,按业务最慢接口调整
}
参数含义与调整思路:
| 参数 | 默认值 | 作用 | 调整建议 |
|---|---|---|---|
proxy_connect_timeout |
60s | 建立与后端连接的时限 | 一般 2 到 5 秒足够,太长会让故障节点拖住请求 |
proxy_send_timeout |
60s | 两次写操作之间的间隔上限 | 通常保持默认或按需微调 |
proxy_read_timeout |
60s | 两次读操作之间的间隔上限 | 长任务接口需调大,但要配合后端超时一起改,否则前后端不一致会出现重复提交 |
这里有一个常见误区:把 proxy_read_timeout 调到很大来"解决"502 超时问题。如果一个接口原本几秒返回、现在要几十秒,真正要查的是数据库慢查询或外部 API 卡顿,把超时调大只会让请求堆积、连接池耗尽,把局部问题放大成全局故障。超时值应该略大于业务的 P99 响应时间,而不是无限大。
超时导致的错误对应关系:
proxy_connect_timeout 超时 → 504 Gateway Time-out(Nginx 连接不上后端)
proxy_read_timeout 超时 → 504 Gateway Time-out
后端返回无效或未响应 → 502 Bad Gateway
后端返回内容过大Nginx缓冲不足 → 可能 502 并记录相关错误
缓冲相关配置:
proxy_buffering on; # 默认开启,先收后端响应再发给客户端
proxy_buffer_size 8k; # 用于存放响应头的缓冲区
proxy_buffers 8 8k; # 响应体的缓冲区数量与大小
proxy_busy_buffers_size 16k;
proxy_max_temp_file_size 0; # 禁止缓冲落盘,避免磁盘 IO 抖动
开启缓冲能让后端快速释放,这是它的价值。但对于服务端推送、SSE、大文件流式下载这类场景必须关掉,否则客户端要等整个响应收完才收到数据:
location /stream/ {
proxy_pass http://backend_app;
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
proxy_read_timeout 3600s;
}
WebSocket 代理
WebSocket 通过 HTTP 的 Upgrade 机制建立,Nginx 默认不会自动转发相关请求头,需要显式处理。
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream ws_backend {
server 127.0.0.1:9501;
}
server {
listen 80;
server_name ws.example.com;
location / {
proxy_pass http://ws_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 3600s; # 长连接要放宽读超时
proxy_send_timeout 3600s;
}
}
用 map 而不是直接写死 Connection "upgrade" 的原因是这样更严谨:只有客户端确实带了 Upgrade 头时才转发升级请求,普通 HTTP 请求保持 close,避免把非 WebSocket 请求也当成升级请求发出去,某些后端会因此报错。
proxy_read_timeout 必须放宽,因为 WebSocket 大部分时间是空闲的,默认 60 秒会被 Nginx 主动断开,表现为"每隔一分钟连接就掉一次",这是 WebSocket 配置里最高频的问题。
静态资源分离
动态应用处理图片、CSS、JS 是很浪费的——后端进程数量有限,把它们占用来传输静态字节很不合算。让 Nginx 直接返回静态文件,后端只处理真正需要计算的请求:
server {
listen 80;
server_name example.com;
root /var/www/example.com/public;
# 静态资源:本地存在就直接返回,带长缓存头
location ~* \.(jpg|jpeg|png|gif|webp|svg|ico|css|js|woff2|ttf)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
try_files $uri =404;
}
# 带版本号的构建产物可以设更长的缓存
location ^~ /assets/ {
expires 1y;
access_log off;
try_files $uri =404;
}
# 其余请求转发给后端
location / {
proxy_pass http://backend_app;
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;
}
}
access_log off 关掉静态资源日志能显著降低磁盘写入,日志量大的站点效果明显。带内容哈希的文件名(如 app.a1b2c3.js)可以放心用一年缓存,因为内容变化时文件名也会变。
路径转发还是域名转发
多站点部署有两种基本划分方式,各有适用情况。
按域名转发——不同域名指向不同后端:
server {
listen 80;
server_name www.example.com;
location / { proxy_pass http://web_backend; }
}
server {
listen 80;
server_name api.example.com;
location / { proxy_pass http://api_backend; }
}
优点是每个服务路径干净,不需要处理前缀剥离,Cookie 作用域、跨域策略都好写。缺点是需要为每个子域添加 DNS 记录。
按路径转发——一个域名下用不同前缀区分:
server {
listen 80;
server_name example.com;
location /api/ { proxy_pass http://api_backend/; }
location /admin/ { proxy_pass http://admin_backend/; }
location / { proxy_pass http://web_backend; }
}
优点是省 DNS 记录,前端统一域名天然同源,不需要处理跨域。缺点是后端程序里写死的绝对路径(比如 )在部署到子路径下会 404,需要应用本身支持配置 base path。
| 维度 | 域名转发 | 路径转发 |
|---|---|---|
| DNS 记录 | 每个子域一条 | 只需一条 |
| 后端改造成本 | 低,路径不需要感知 | 需要支持 base path |
| Cookie 作用域 | 天然隔离 | 共享域,需靠 Path 区分 |
| 跨域请求 | 存在,需配 CORS | 同源,无需特殊处理 |
| 证书 | 需要通配或多域名证书覆盖子域 | 单域名证书即可 |
建议优先用域名转发。 它对后端改造的要求最低,后续要拆分成独立机器时也更简单——只要改一条 DNS 记录即可,路径方案则需要改 Nginx 配置和应用的 base path。
完整配置示例与上线流程
下面是一个可以直接改用的多站点配置,放在 Debian、Ubuntu 的 /etc/nginx/sites-available/example.com,或 Rocky、RHEL、CentOS 的 /etc/nginx/conf.d/example.com.conf:
upstream api_pool {
least_conn;
server 127.0.0.1:9001 max_fails=3 fail_timeout=10s;
server 127.0.0.1:9002 max_fails=3 fail_timeout=10s;
keepalive 32;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
location /ws/ {
proxy_pass http://127.0.0.1:9501;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
location /api/ {
proxy_pass http://api_pool/;
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 $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
}
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;
}
}
上线流程:
# 语法检查,务必先执行,不通过就不要重载
sudo nginx -t
# Debian、Ubuntu 首次启用站点
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo nginx -t && sudo systemctl reload nginx
# Rocky、RHEL、CentOS
sudo nginx -t && sudo systemctl reload nginx
# 本机验证,排除外部 DNS 影响
curl -H 'Host: example.com' -o /dev/null -sS -w '%{http_code} %{time_total}s\n' http://127.0.0.1/api/health
# 放行对外端口
sudo ufw allow 80/tcp # Debian、Ubuntu
sudo firewall-cmd --permanent --add-service=http && sudo firewall-cmd --reload # RHEL 系
注意事项:内部服务端口(8080、9001、9501 等)不应在云安全组或公网防火墙中放行,只监听 127.0.0.1 或内网地址更安全。可以用下面命令确认它们没有对外监听:
sudo ss -lntp
若某服务显示为 0.0.0.0:9001,说明它对所有网卡生效,应改成只监听回环地址。
常见问题
后端日志里 IP 全是 127.0.0.1 怎么办?
确认 X-Real-IP 和 X-Forwarded-For 都已设置,并且应用读取这两个头而不是直接取 socket 地址。多数 Web 框架有对应的配置项(称为 trusted proxies 或 forwarded headers),只填代理头不配置信任列表的话,框架出于安全考虑会忽略它们。
开了 HTTPS 后后台一直循环跳转?
后端没有收到 X-Forwarded-Proto,以为请求是 HTTP 于是返回重定向,浏览器再请求又被重定向。加上 X-Forwarded-Proto $scheme 并在后端配置信任该头即可。
502 Bad Gateway 怎么排查?
按顺序查:后端服务是否运行(ss -lntp 看端口)、upstream 里的地址和端口是否与实际一致、proxy_pass 的斜杠是否导致路径错误、SELinux 是否阻止了 Nginx 发起对外连接(RHEL 系常见,可用 setsebool -P httpd_can_network_connect 1 处理,但先确认审计日志中的原因)、以及 Nginx 错误日志里的具体报错。
WebSocket 每分钟断线一次是什么原因?
proxy_read_timeout 默认值 60 秒,空闲连接被 Nginx 关闭。把该值调到大于客户端心跳间隔,同时确保客户端有心跳机制。
上传大文件失败?
反向代理场景下要查三处:Nginx 的 client_max_body_size、后端应用自身的体积限制、以及是否存在中间代理(如 CDN)层限制。Nginx 侧修改示例:client_max_body_size 50m;,可放在 server 或 http 上下文。
upstream 里的域名解析结果变了不生效?
Nginx 在启动时解析配置文件里的域名并缓存结果。如果使用 proxy_pass http://域名/,该域名更换 IP 后需要重载 Nginx。频繁变动的场景建议在 upstream 使用 IP,或用商业版支持的动态解析方案。
总结
proxy_pass末尾有无斜杠决定 location 前缀是否被替换:不带斜杠原样转发完整 URI,带斜杠则把匹配前缀替换成该 URI,迁移时这一字符的差别会让整个转发路径出错。- 必设的请求头是
Host、X-Real-IP、X-Forwarded-For、X-Forwarded-Proto;缺X-Forwarded-Proto是"HTTPS 后台循环跳转"最常见的根因,取值 IP 时要从信任链末尾往回找而非取第一个。 upstream配合weight、max_fails、fail_timeout、backup实现负载均衡和被动摘除;keepalive必须与proxy_http_version 1.1和proxy_set_header Connection ""同时使用才会生效。- 超时参数要略大于业务的 P99 响应时间,盲目调大
proxy_read_timeout只会让请求堆积;WebSocket 场景必须放宽读超时并转发 Upgrade 头。 - 多站点优先用域名转发而非路径转发,内部服务端口只监听回环或内网地址并在云安全组与主机防火墙两侧都不放行对外公开。
企业QQ咨询




