平时接手项目,最常被问到的就是“帮我配个 Nginx 转发”。一开始我以为就是写两行proxy_pass的事,直到自己踩了几次坑——路径不对、Cookie 丢了、页面无限重定向,才意识到请求转发配置看似简单,里面的门道一点都不少。
这篇东西就是围绕Nginx 请求转发配置写的。我会从最基础的场景聊起,把 location 匹配规则、proxy_pass带不带斜杠的区别、负载均衡、Header 处理这些核心点拆开讲,最后再把我平时排查 502、504、404 这类问题的思路整理出来。适合刚接触 Nginx 的开发、运维,也适合那些后端服务越拆越多、需要靠 Nginx 统一入口的同学。读完不敢说让你成为专家,但至少能让你配完不再提心吊胆。
1. 请求转发到底在解决什么问题
1.1 不止是“换个端口”那么简单
很多人对转发配置的第一印象,是把 80 端口的请求转到 8080,像是给服务开了个“后门”。但实际上,请求转发解决的核心问题是:让多个服务对外表现成一个统一的入口。
举个例子。你有个电商项目,用户服务跑在 8081,订单服务跑在 8082,支付回调跑在 8083。客户端不可能记住这么多端口,更不可能让这些端口直接暴露在公网。这时候 Nginx 在 80 端口守着,根据请求的路径、域名或者 Header,把流量分发给对应的服务。用户访问http://xxx/api/order,Nginx 识别出来这是订单服务,自动转到 8082。
所以说白了,请求转发就是在做“流量分发”,它解决的是多服务架构下入口混乱的问题。我见过不少团队一开始所有接口都写在一个应用里,后来拆服务的时候,发现客户端代码里写死的端口一大堆,改也不是,不改也不是。最后靠 Nginx 一层转发,把新旧路径全部映射好,客户端几乎没怎么动就完成了迁移。这就是转发的价值:它对调用方是透明的,后端怎么拆,前端无感。
1.2 开发联调、多环境复用与流量入口统一
除了多服务分发,请求转发在日常开发里还有一个很实际的用途:本地开发联调。
比如前端在自己电脑上跑了一个开发服务器,端口是 3000,后端接口在测试环境。如果直接让前端代码里写“http://测试环境IP:8080/api”,跨域问题马上来。更常见的做法是,本地 Nginx 监听 80 端口,把所有/api开头的请求转发到测试环境,其他请求转发到本地 3000。前端代码里只写相对路径/api/xxx,既没有跨域,又能随时切换后端地址,只需要改 Nginx 配置里的proxy_pass。
还有多环境复用也是一样。同一套前端代码,要对接开发、测试、预发布三个环境,传统做法是打包三次。但你只要在 Nginx 里配三份转发规则,或者用变量动态指定上游地址,一套产物到处跑。
从运维视角看,转发配置更大的意义在于流量入口统一,所有请求都从固定的 80/443 进来。这样在做限流、超时控制、日志审计的时候,只需要在 Nginx 这一层处理就行,不用每个后端服务各搞一套。安全感强了很多。
2. 配置前要先想清楚的两件事
2.1 location 匹配规则:你写的那条真的会命中吗
转发配置第一道坎不在proxy_pass,而在location。很多新手上来就写:
location /api { proxy_pass http://backend; }以为“以 /api 开头”的请求都能命中。但实际 Nginx 的匹配规则不是这么简单。它有一套优先级:
| 匹配类型 | 写法示例 | 优先级 |
|---|---|---|
| 精确匹配 | location = /api | 最高 |
| 前缀匹配(^~) | location ^~ /api/ | 高 |
| 正则匹配 | location ~ \.php$ | 中 |
| 普通前缀匹配 | location /api | 低 |
| 默认匹配 | location / | 最低 |
可以这么理解:Nginx 先看有没有精确匹配,有就直接用;再看有没有^~前缀匹配,有就直接用;然后从头到尾扫正则,命中就用;最后才看普通前缀匹配,然后挑最长的那个。
我记得有个项目就是这么出问题的。配了一个location ^~ /api/,又配了一个location ~ \.(js\|css)$。本来想让静态资源走文件系统,接口走转发。结果因为正则匹配的优先级高于普通前缀匹配,导致某些以/api/开头的请求里带了.js后缀,被正则规则抢走了,直接返回 404。排查了半天,最后在^~前面加了一个^~解决了。
所以配置转发之前,一定要先搞清楚规则命中的顺序。我的建议是:能用^~就用^~,它能直接跳过正则,避免规则打架;能不写正则就不写正则,正则越多越难排查。
2.2 proxy_pass 带不带斜杠,结果天差地别
这是整个转发配置里最容易翻车的地方,没有之一。同样是proxy_pass,后面带不带路径,转发过去的结果完全不一样。
# 方式 A:不带 URI(不带斜杠) location /api/ { proxy_pass http://backend; } # 方式 B:带 URI(带斜杠) location /api/ { proxy_pass http://backend/; }方式 A 是“原样转发”。请求/api/order/list到了后端,还是/api/order/list,前缀不会丢。
方式 B 是“替换转发”。Nginx 会把 location 匹配到的/api/部分,替换成proxy_pass里带的/,也就是请求到后端变成/order/list,前缀被吃掉了。
这么说有点抽象,我打个比方。方式 A 相当于你叫外卖,你告诉外卖员“到 A 座 301 室”,外卖员原话传给厨房,后端收到的是完整地址。方式 B 相当于外卖员自己先往前走了一层,到厨房门口直接说“301 室”,后端看到的就是去掉楼层后的房间号。
这个区别在前后端分离项目里尤其明显。比如后端接口路径本身是有/api前缀的,你在proxy_pass里多写了一个斜杠,然后所有接口全部 404。更恶心的是,有些请求本来能通,但带了文件后缀的静态请求又通了,让你误以为配置没问题,其实只是巧合。
我个人的习惯是:如果后端接口设计时带了/api前缀,proxy_pass就不带斜杠,保持原样;如果后端路径本来就没有前缀、或者需要剥离前缀,才用带斜杠的方式。而且每次改完配置,我都会用curl实际打一次请求看路径,绝不在浏览器里靠猜。
3. 一份能直接抄作业的请求转发配置
3.1 最基础的场景:按路径转发到后端服务
先来一个最简单的完整示例。场景是这样的:前端静态文件放在/var/www/html,后端接口跑在127.0.0.1:8080,需要让所有/api前缀的请求转发到后端,其余请求直接读本地静态文件。
server { listen 80; server_name example.com; root /var/www/html; location /api/ { 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; } location / { try_files $uri $uri/ /index.html; } }这里有几个点值得解释一下。proxy_pass http://127.0.0.1:8080;不带斜杠,所以/api/order会原样传给后端。proxy_set_header Host $host;是让后端看到原始请求的域名,不然后端拿到的是127.0.0.1:8080,如果后端用域名做路由或者生成链接,就会出错。
X-Real-IP和X-Forwarded-For是为了传递真实客户端 IP。因为 Nginx 转发后,后端看到的连接来源是 Nginx 本身的 IP,如果不加这两个头,后端的日志里永远只能看到 127.0.0.1,做风控和审计就麻烦了。
这个配置里我用了try_files $uri $uri/ /index.html;,这是给前端 history 路由用的,刷新子页面时不至于 404。如果只是简单的静态站点,可以只用index index.html;。
3.2 路径前缀剥离与改写:前端说接口多了一层
很多场景里,后端接口路径是不带/api的。比如后端实际路由是/order/list,而你希望对外暴露成/api/order/list。这时候就需要在转发时把/api剥掉。
location /api/ { proxy_pass http://127.0.0.1:8080/; }注意proxy_pass末尾的/,这就是上一节讲的“带 URI”写法。请求/api/order/list进来,Nginx 把/api/这一截替换成/,后端收到的就是/order/list。
但如果只是想改一部分路径,而不是整体剥离前缀,就需要用rewrite。比如你想把/api/old/开头的请求,转发成后端/new/开头:
location /api/old/ { rewrite ^/api/old/(.*)$ /new/$1 break; proxy_pass http://127.0.0.1:8080; }这里的写法是:先用正则把/api/old/后面捕获到的部分塞进/new/后面,break表示停止处理 rewrite,然后把改写后的路径通过proxy_pass转发出去。
为什么有时候用 rewrite、有时候直接用斜杠就行?我的经验是:如果只是去掉固定前缀,用斜杠最简单;如果要做前缀替换、或者前后路径结构不一样,就得用 rewrite。另外,rewrite 里的正则我要提醒一句:别贪多,尽量用非贪婪匹配,不然很容易把 query string 也吞进去。正常来说 query string 是 Nginx 自动传递的,不需要在 rewrite 里处理。
3.3 多后端负载均衡:upstream 的正确写法
服务规模大了,一台后端扛不住,就得用upstream配置一组后端服务器。
upstream backend_pool { server 192.168.1.10:8080 weight=3; server 192.168.1.11:8080 weight=1; server 192.168.1.12:8080 backup; } server { listen 80; server_name example.com; location /api/ { proxy_pass http://backend_pool; proxy_set_header Host $host; } }weight参数控制权重,权重越高分配到的流量越多。backup是备用节点,平时不接收流量,只有其他节点都挂了才会启用。
还有一个很多人忽略的参数叫keepalive。在高并发场景下,如果 Nginx 与后端之间频繁建立 TCP 连接,性能会受影响。可以在 upstream 里配置:
upstream backend_pool { server 192.168.1.10:8080; keepalive 32; }然后在 location 里加两行:
proxy_http_version 1.1; proxy_set_header Connection "";这个操作能让 Nginx 和后端之间的连接复用,实测在高 QPS 场景下能减少不少握手开销。我第一次做压测的时候,开了 keepalive 后,请求耗时平均值降了差不多 20%。
需要说明的是,upstream默认的负载均衡策略是轮询。想要更均匀或者更智能的分配,可以加least_conn,让 Nginx 把请求分给当前连接数最少的节点;想要同一用户的请求总打到同一台后端,就用ip_hash。有状态场景(比如带 session 的单体应用)用 ip_hash 能减少不少麻烦,但过度依赖它也不是好习惯,能用 Redis 之类的集中会话还是往那边走。
3.4 转发时的头部处理:host、真实IP和超时
转发过程中最容易忽略的就是 Header,而 Header 恰恰决定了后端能不能正确工作。我见过因为没转发Host头,后端做重定向时把用户导到了一个完全不对的域名上。
所以我在转发配置里基本都会固定带上这几行:
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;Host $host是让后端知道域名。X-Forwarded-Proto $scheme是告诉后端原始请求是 http 还是 https,否则后端在生成链接、做 HTTPS 重定向的时候会出错。
还有一个容易踩的坑是WebSocket 转发。WebSocket 协议升级需要额外两个头:
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";不配这两行,WebSocket 握手会一直失败,前端控制台报各种奇怪的错。我记得有一次查了半天,日志里一直报 400,最后才想起来忘了放这两个头。
超时参数也很关键。默认的proxy_read_timeout是 60 秒,如果后端处理一个请求超过了 60 秒,Nginx 就会直接掐断连接,前端收到 504。处理大文件导出、批量计算这类场景,建议把超时调大一点:
proxy_connect_timeout 10s; proxy_send_timeout 120s; proxy_read_timeout 120s;connect_timeout别调太大,默认 60 秒已经很久了。后端连不上,等几十秒才报错,用户早跑了。一般 10 秒够用。read_timeout才是按业务需求调的,要看后端的接口最长处理时间是多少。
4. 常见问题与排查技巧实录
4.1 502 和 504 到底是谁的问题
这是转发配置里遇到最多的两类错误,很多人一看到 502/504 就慌,其实排查思路很固定。
502 Bad Gateway,意思是你 Nginx 连接不上后端。先查后端进程到底起没起:
ps -ef | grep java再确认端口通不通:
curl http://127.0.0.1:8080/api/test如果 curl 能通,Nginx 还是 502,那就看proxy_pass写对了没有,特别是 upstream 里的 IP 和端口是不是写错了。还有一种情况是防火墙挡了,Nginx 所在机器访问不到后端机器的端口,也会报 502。下行链路的问题和上行链路的问题要分清楚,直接用curl -v看连接过程最快。
504 Gateway Timeout,意思是 Nginx 连上了后端,但后端在超时时间内没返回结果。先按上一节提到的,把proxy_read_timeout调大试试。如果调大还是 504,那基本可以断定是后端接口本身太慢,比如 SQL 没优化、第三方接口响应慢、线程池满了在排队。这时候与其纠结 Nginx 配置,不如去看后端日志。
我自己的排查顺序是:先 curl 后端接口测耗时,如果接口本身要 30 秒,Nginx 默认 60 秒应该够;如果接口要 90 秒,就得调proxy_read_timeout。如果 curl 也报错,那就是后端的问题,Nginx 只是个传话的,别冤枉它。
4.2 转发后一直 404,先查路径再查规则
转发后 404 是另一个高频问题。原因无非两种:路径不对,或者规则不对。
路径不对的情况,多半是proxy_pass带不带斜杠没搞清楚。我在第 2 节已经详细讲过,这里再给一个快速自查方法:打开 Nginx 的 access log,看看到底转发给了谁、转发路径是什么。
tail -f /var/log/nginx/access.log如果日志显示转发的路径是/api/order,而你的后端接口实际是/order,那就说明前缀没剥掉,检查proxy_pass末尾的斜杠。
规则不对的情况,是 location 匹配到了错误的位置。比如你写了两个 location:
location /api { proxy_pass http://backend; } location ^~ /api/admin { proxy_pass http://admin_backend; }本来想让/api/admin走管理后端,但因为^~优先级高于普通前缀,这个配置其实是对的。但如果把第二个写成不带^~的普通前缀,两个规则到底谁的路径长谁就赢,你可能根本分不清到底匹配了谁。所以我的建议是:规则多的时候,一定要用nginx -T把最终生效的配置打印出来看,再配合curl验证。
4.3 页面无限重定向,多半是 Host 头在捣乱
还有一个很经典的场景:转发配置好了,页面能打开,但登录后疯狂重定向,最后浏览器提示“重定向次数过多”。
这种情况十有八九是后端返回的重定向地址和实际访问地址不一致。举个例子,用户访问的是http://example.com/login,Nginx 转发给后端http://192.168.1.10:8080/login。后端处理完后,生成一个重定向地址http://192.168.1.10:8080/home返回给浏览器。浏览器拿到这个地址,直接去访问192.168.1.10:8080,而这个地址只有内网能通,或者根本没配转发,于是各种报错、反复跳转。
解决办法就是我在第 3 部分强调的proxy_set_header Host $host;。让后端认为用户访问的就是example.com,生成的链接自然就对了。
如果后端用的是 HTTPS 协议,还需要同步配置X-Forwarded-Proto $scheme,否则后端会认为进来的是 http,生成重定向链接时又跑回 http,和站点的 HTTPS 强制跳转互相打架,也是一样的死循环。
4.4 定位问题的三板斧:日志、curl 和 nginx -T
最后我把平时排查问题的工具和思路整理成一个表格,遇到问题照着走就行。
| 排查手段 | 用途 | 常用命令 |
|---|---|---|
| 实时日志 | 看请求实际转发到哪个地址、状态码多少 | tail -f /var/log/nginx/access.log |
| 错误日志 | 看连接失败、超时、DNS 解析错误等底层原因 | tail -f /var/log/nginx/error.log |
| curl 测试 | 模拟请求,带上 Host 头验证后端接口是否正常 | curl -v -H "Host: example.com" http://127.0.0.1:8080/api/test |
| 配置检查 | 确认当前生效的配置内容,防止配置文件多份导致混乱 | nginx -T |
| 验证配置语法 | 改完配置先检查,再 reload | nginx -t && nginx -s reload |
这里特别说一下nginx -T。很多线上环境 Nginx 配置被拆成很多个小文件,nginx -T能把所有配置合并输出,比较清楚地看到最终内容里到底有没有proxy_pass写错、location优先级是否如预期。每次改完配置,我都要跑一遍nginx -t,语法不对就直接报错,别等 reload 挂了才后悔。
还有一个技巧,把 access log 的格式稍微改一下,加上$upstream_addr和$upstream_response_time,就能看到每次请求实际打到哪台后端、后端响应花了多少毫秒:
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" ' 'upstream_addr=$upstream_addr upstream_response_time=$upstream_response_time';有了这个日志,调负载均衡的时候能看到每台机器的响应时间,排查慢请求也直观得多。
平时配置转发出问题,绝大多数不是什么高级故障,就是路径、Header、超时这三块没有处理好。按我上面说的思路,先确认后端接口本身可用,再看 Nginx 是不是把请求原样传对了,然后看 Header 有没有让后端误解,最后看超时时间够不够,基本都能解决。还是那句老话:Nginx 只是一个传话人,别让它替你背锅,但也别让它把话传歪了。