news 2026/11/3 11:47:22

Nginx反向代理Tomcat多项目路径转发配置与避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Nginx反向代理Tomcat多项目路径转发配置与避坑

1. 把 Nginx 摆在 Tomcat 前面,到底图什么

我第一次接触 Nginx 反向代理 Tomcat 多项目这套组合,是因为一个后台系统越堆越臃肿:前端页面、接口服务、文件下载、定时任务的管理页面全塞在一个 Tomcat 的 webapps 目录下,端口 8080 直接对着公网开放。上线三个月,问题集中爆发——几十个用户同时点导出,Tomcat 线程池瞬间打满,静态资源也跟着卡死;后来想加个新模块,必须整个应用停机重新部署,稍微出点岔子全站不可用。更麻烦的是,前端页面里的接口地址全是写死的http://ip:8080/xxx,换个环境就得改一遍代码重新打包。

这套组合解决的就是这类问题:Nginx 反向代理负责把外部请求接住、按路径分流、处理静态资源、控制超时与并发;Tomcat退到内网后面,只专注跑 Java 应用逻辑;多项目路径转发则让一台机器、一个域名、一个 80 端口,同时对外提供好几个独立项目的访问入口。如果你手上有一台服务器要跑两三个 Java 项目,又不想每个项目都买域名、配端口、开防火墙,那这套方案基本是最省事的落地方案。

这套东西适合谁?写过 Spring Boot 或者传统 SSM 项目、能看懂server.xml和nginx.conf、但没系统梳理过转发规则的同学;也适合那些项目已经跑起来了、但被路径错乱、静态资源 404、getRemoteAddr()拿到 127.0.0.1 这些问题折腾过的运维和后端。纯小白也能跟下来,我会尽量把每一步为什么这么做讲透,配置项逐行拆开说。整套东西不难,难的是细节对齐——十个踩坑的人里有八个栽在斜杠和 contextPath 上。

2. 环境规划:先把图纸画清楚再动手

2.1 端口、目录与项目数量的排布思路

动手之前我建议先在一张纸上把端口和路径画出来,这一步花十分钟,后面能省两小时。最基本的排布逻辑是这样的:Nginx 监听 80 和 443,对公网只暴露这两个端口;Tomcat 一律改成127.0.0.1或内网地址监听,端口用 8081、8082 这种不冲突的值。这样做的好处很直接——后端端口不对外,任何人扫不到你的 8081,攻击面缩小了一大截,而且以后想换 Tomcat 端口,只改一行 upstream 配置,外部完全无感知。

目录规划同样重要。我习惯把 Tomcat 按实例拆开,每个项目一个独立目录,比如/opt/apps/tomcat-portal、/opt/apps/tomcat-admin,各自带独立的logs、webapps、conf。之所以不共用一个 Tomcat 放多个war,是因为多个应用共享一个 JVM,一旦某个应用内存泄漏或者 Full GC 卡住,其他项目跟着遭殃;而且共用实例时,JVM 参数、连接池、日志配置都是全局的,一个项目需要调大堆内存,其他项目只能陪着。当然,如果服务器内存只有 2G、项目本身又都是小体量,共用一个大 Tomcat 也是合理选择,那就靠contextPath区分,后面会讲怎么配。

下面这张表是我一般会先填好的规划模板,你可以照着改成自己的:

项目后端端口对外路径前缀Tomcat 实例目录备注
门户站8081/或/portal/opt/apps/tomcat-portal主站,占用根路径
管理后台8082/admin/opt/apps/tomcat-admin需登录鉴权
接口服务8083/api/opt/apps/tomcat-api前后端分离,纯 JSON
文件服务8084/files/opt/apps/tomcat-file上传下载,超时给大

2.2 Tomcat 侧必须先改的两处配置

很多人上来就写 Nginx 配置,结果浏览器里request.getRemoteAddr()永远是127.0.0.1,日志系统里记的全是代理 IP,排查问题完全没法定位真实用户。这个问题必须在 Tomcat 侧解决。第一处是server.xml里的Connector节点,加上proxyPort和scheme:

<Connector port="8081" protocol="HTTP/1.1" connectionTimeout="20000" maxThreads="400" acceptCount="200" address="127.0.0.1" proxyPort="80" scheme="http" redirectPort="443" />

proxyPort和scheme这两个属性的作用,是让 Tomcat 在没有直接参与外部连接的情况下,知道"外面那层用的是 80 端口、http 协议"。不加的话,代码里request.getRequestURL()拼出来的地址会带上:8081,做重定向或者生成绝对链接时就会跳到一个外网访问不到的端口上,前端页面直接白屏。这个坑我见过太多次。

第二处是在server.xml的Host节点里开RemoteIpValve,专门用来从X-Forwarded-For头里还原真实客户端 IP:

<Valve className="org.apache.catalina.valves.RemoteIpValve" remoteIpHeader="X-Forwarded-For" protocolHeader="X-Forwarded-Proto" internalProxies="127\.0\.0\.1|10\.\d+\.\d+\.\d+" />

internalProxies是你信任的代理层地址列表,用正则写。这里要特别提醒:不要图省事写成.*,那等于告诉 Tomcat 任何人都能伪造X-Forwarded-For头,攻击者随手发一个X-Forwarded-For: 1.2.3.4就能把你的 IP 白名单、频率限制、审计日志全部绕过。只信任你自己的 Nginx 所在地址,这是底线。

2.3 Nginx 安装时的两个关键点

安装本身没什么可说的,包管理器一条命令就完事。但有两个点值得单独拎出来。第一是确认http_proxy和stream模块都在,用nginx -V看编译参数,--with-http_ssl_module和--with-http_realip_module这两个必须在,前者用于后面配 HTTPS,后者用于多级代理场景还原 IP。第二是确认编译时的--with-http_v2_module,如果你的 Nginx 是很多年前的老版本或者精简版,可能没有 HTTP/2 支持,那 HTTPS 场景下性能会白损失一截。

还有个小细节:worker_connections的默认值通常是 1024,worker_processes默认是 1。现代服务器上我一般配成worker_processes auto;加worker_connections 10240;,理论并发上限就是worker_processes × worker_connections。当然真正的瓶颈通常在后端 Tomcat 的线程数和数据库连接池上,Nginx 这边调得再高也没意义,这一点要有清醒认识——反代层不是性能瓶颈的解决者。

3. 反向代理配置的逐行拆解

3.1 proxy_pass 结尾那个斜杠,是最大的坑

这条规则我用一句话概括:proxy_pass后面只要带了 URI(哪怕只是一个/),Nginx 就会用这个 URI 替换掉location匹配到的那一段前缀;不带 URI 时,则原样把整个原始路径传过去。这句话听起来绕,但它是所有路径转发问题的根。

看第一个例子,前缀替换:

location /api/ { proxy_pass http://127.0.0.1:8083/; }

浏览器访问/api/user/list,location匹配上了/api/这个前缀,因为proxy_pass后面有/,Nginx 就把匹配到的/api/剥掉,把剩下的user/list拼到http://127.0.0.1:8083/后面,后端收到的实际是/user/list。这就是所谓的"剥前缀"。

第二个例子,原样透传:

location /api/ { proxy_pass http://127.0.0.1:8083; }

注意结尾没有斜杠。这时候 Nginx 不做任何替换,后端收到的还是/api/user/list。这就要求后端的 Tomcat 应用必须部署在/api这个 contextPath 下,否则就是 404。

两种写法都合法,关键是后端应用的 contextPath 和你的转发规则要对齐。我一般的做法是:Spring Boot 项目在application.yml里显式设置server.servlet.context-path: /api,Nginx 用不带斜杠的写法原样透传。这样做的好处是路径只有一份定义,前后端联调时不容易对不上;坏处是后端本地启动时访问地址也带前缀,有些同学不习惯。反过来,如果你用的是传统 war 包部署、contextPath 已经定死为ROOT,那就用带斜杠的剥前缀写法。

proxy_pass里还有个高阶用法,用变量实现动态转发:

location ~ ^/([a-z]+)/(.*)$ { proxy_pass http://127.0.0.1:8081/$2; }

这种写法能实现"前缀到端口的映射表",但要注意:一旦proxy_pass里用了变量,Nginx 就不再在启动时解析 upstream 里的域名,而是在运行时靠resolver解析,没配resolver就会报no resolver defined to resolve。除非确实需要动态分流,否则我不推荐这种写法,调试成本太高。

3.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;

Host这行的作用是让后端知道用户实际访问的是哪个域名。不设的话,Nginx 默认传给后端的是 upstream 里写的那串地址(比如127.0.0.1:8081),后端做多域名路由、生成绝对链接、校验来源时全部会错。$host的取值是"请求行里的主机名,没有则用 Host 头,再没有则用 server_name",这个优先级要清楚。

X-Real-IP和X-Forwarded-For的区别经常被搞混:前者只放一个 IP,就是直连 Nginx 的那个客户端;后者是一个逗号分隔的链,$proxy_add_x_forwarded_for的语义是"保留客户端已有的 XFF,再把自己看到的来源 IP 追加到末尾"。所以当链路是 CDN → Nginx → Tomcat 时,XFF 里会有两个 IP,还原真实 IP 要取从左数第一个可信的,这也是为什么前面RemoteIpValve的internalProxies要配准确——它决定了从右往左跳过几层可信代理。

X-Forwarded-Proto传的是原始协议。这个在后端判断"要不要跳 HTTPS"时至关重要,如果后端看到的是 http,可能会进入一个无限重定向循环。我踩过一次:加了强制 HTTPS 的逻辑,结果 Nginx 到 Tomcat 之间是 http,Tomcat 认为请求不安全又跳回 https,Nginx 再转成 http 给 Tomcat,浏览器直接报ERR_TOO_MANY_REDIRECTS。

3.3 upstream 块:为将来留个扩展位

即使当前只有一个后端实例,我也建议用upstream包一层,而不是在proxy_pass里写裸地址:

upstream portal_backend { server 127.0.0.1:8081 weight=1 max_fails=2 fail_timeout=10s; keepalive 32; }

好处有三:一是以后要扩成两台机器,只改 upstream 块,location里的proxy_pass http://portal_backend一动不用动;二是可以做健康探测,max_fails=2 fail_timeout=10s意思是 10 秒内失败 2 次就先摘掉这个节点,10 秒后再试;三是能用keepalive开启长连接。

keepalive这个参数需要配合两条指令才能生效,很多人加了但没起作用:

proxy_http_version 1.1; proxy_set_header Connection "";

默认 Nginx 对上游用的是 HTTP/1.0 并带Connection: close,所以每次转发都新建 TCP 连接。加keepalive 32表示每个 worker 进程为这个 upstream 保留最多 32 个空闲长连接。当并发量上去以后,省掉 TCP 三次握手和 TIME_WAIT 的开销,收益是实打实的。不过keepalive的值别设太大,一般跟后端 Tomcat 的maxThreads保持同一数量级就够了,设成几百上千反而会占着连接不放。

4. 一个域名挂多个项目,路径怎么切

4.1 location 匹配优先级:你必须背下来的顺序

多项目路径转发最容易翻车的地方就是 location 冲突。/api和/api-user同时存在时,到底谁生效?这取决于 Nginx 的匹配规则,顺序是:

  1. =精确匹配,命中即停,最高优先级;
  2. ^~前缀匹配,命中后不再尝试正则;
  3. ~和~*正则匹配,按配置文件里出现的先后顺序,第一个命中即停;
  4. 普通前缀匹配,记住最长的那一个,但会继续往下试正则。

我把它翻译成一句人话:精确优先,其次是被^~标记的前缀,然后是正则(谁写前面谁先试),最后才是普通前缀(最长者胜)。第五种情况容易被忽略——如果正则和普通前缀同时存在,正则优先于普通前缀,哪怕普通前缀更长。举个例子:

location /api-user/ { proxy_pass http://127.0.0.1:8082; } # 普通前缀 location ~ ^/api/ { proxy_pass http://127.0.0.1:8083; } # 正则

访问/api-user/list时,普通前缀/api-user/匹配上了,但 Nginx 仍会继续尝试正则,^/api/也能匹配上/api-user/...,于是正则赢,请求被转发到了 8083——这不是你想要的。解决办法是给/api-user/加上^~:

location ^~ /api-user/ { proxy_pass http://127.0.0.1:8082; }

加了^~之后,前缀一命中就停止后续正则匹配,问题解决。这类冲突在项目一多的时候几乎必然出现,我建议所有按项目划分的前缀 location 都统一加^~,除非你有意让正则来覆盖它,这样行为最可预测。

4.2 三种多项目组织方式的取舍

同一台机器跑多个项目,对外组织有三种常见思路,各有适用场景:

第一种,单域名 + 路径前缀。所有项目挂在www.example.com下,门户在/,后台在/admin,接口在/api。优点是只用一个域名、一张证书,省事省钱;缺点是对前端打包有要求——所有静态资源的引用路径和接口 baseURL 都得带上前缀,Vue 项目里要配publicPath和proxy,否则一刷新就 404。适合内部系统、管理后台这类用户不敏感的场景。

第二种,多域名(多 server 块)。每个项目一个server_name,各自独立的server块。这种组织方式最干净,项目之间完全隔离,路径互不干扰,静态资源全部从根路径开始,前端不用改任何配置。缺点是每个域名都要单独备案和签证书。对外提供服务的正式项目我优先推这个。

第三种,单域名 + 子路径 + 后端 contextPath 一致。这是第一种的加强版,核心是让后端应用的 contextPath 与 Nginx 的 location 前缀完全一致,Nginx 用不剥前缀的写法原样透传。好处是路径定义只有一份,请求进入 Nginx 是什么样,到 Tomcat 还是什么样,排查时链路短。

下面是我实际用得最多的目录式配置骨架,展示三种方式如何在同一个nginx.conf里共存:

http { upstream portal_backend { server 127.0.0.1:8081; keepalive 32; } upstream admin_backend { server 127.0.0.1:8082; keepalive 32; } upstream api_backend { server 127.0.0.1:8083; keepalive 16; } server { listen 80; server_name portal.example.com; # 门户:占用根路径,后端 contextPath 为 / location / { proxy_pass http://portal_backend; } # 后台:路径前缀,后端 contextPath 为 /admin location ^~ /admin/ { proxy_pass http://admin_backend; } # 接口:路径前缀,后端 contextPath 为 /api location ^~ /api/ { proxy_pass http://api_backend; proxy_read_timeout 60s; } # 公共静态资源,直接由 Nginx 吐,不落到 Java location ^~ /static/ { root /data/www; expires 7d; add_header Cache-Control "public"; } # 公共请求头 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_set_header写在server级别是可以被子location继承的,这样省得每个 location 重复抄一遍。但如果你在某个 location 里又写了一次proxy_set_header,不会合并,而是整组覆盖,这是很多人配置失效的原因——在 location 里只写了一行Host,结果X-Real-IP全丢了。

4.3 静态资源交给 Nginx,动态请求才进 Java

这条是性能优化的重点,也是多项目场景下必须做的分流。Tomcat 处理静态文件的能力实在一般,一个请求进来要占一个线程,读完文件还要走一遍 Servlet 容器,跟业务请求抢资源。我一般的做法是把前端打包产物(dist目录)直接放在 Nginx 能读到的地方,不塞进 war 包里:

location ^~ /admin/static/ { alias /data/www/admin/static/; expires 30d; add_header Cache-Control "public, immutable"; access_log off; }

这里用alias而不用root,原因是两者的路径拼接逻辑不同:root会把完整的 URI 拼到根目录后面,alias则是把 location 匹配到的那一段替换成指定目录。location /admin/static/配root /data/www时,实际找的是/data/www/admin/static/xxx;配alias /data/www/admin/static/时,找的是/data/www/admin/static/xxx。看起来结果一样,但当你的目录结构和 URI 前缀不一致时,alias才给得出你要的映射。alias结尾必须带斜杠,且要和 location 结尾的斜杠对应,不对应就会拼出staticstyle.css这种东西。

expires 30d加上带 hash 的文件名是常见组合,前端构建时给静态资源加了内容 hash,内容变了文件名就变,所以可以放心设长缓存。但index.html千万别设长缓存,否则用户永远拿旧的入口文件,新版本发布后浏览器还是加载旧 hash 的资源,找不到就白屏。

5. 完整配置与验证:跑通一遍再上生产

5.1 一份可以直接抄的完整配置文件

下面这份是我从多个项目里抽出来的通用骨架,去掉业务细节,你可以直接改成自己的端口和路径:

user nginx; worker_processes auto; worker_rlimit_nofile 65535; events { worker_connections 10240; use epoll; multi_accept on; } http { include mime.types; default_type application/octet-stream; charset utf-8; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" rt=$request_time ' 'up=$upstream_addr urt=$upstream_response_time'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; client_max_body_size 50m; gzip on; gzip_min_length 1k; gzip_comp_level 5; gzip_types text/plain text/css application/json application/javascript application/xml text/xml image/svg+xml; upstream portal_backend { server 127.0.0.1:8081; keepalive 32; } upstream admin_backend { server 127.0.0.1:8082; keepalive 32; } upstream api_backend { server 127.0.0.1:8083; keepalive 16; } server { listen 80; server_name portal.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name portal.example.com; ssl_certificate /etc/nginx/cert/fullchain.pem; ssl_certificate_key /etc/nginx/cert/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; proxy_connect_timeout 5s; proxy_send_timeout 60s; proxy_read_timeout 60s; 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 ""; location / { proxy_pass http://portal_backend; } location ^~ /admin/ { proxy_pass http://admin_backend; } location ^~ /api/ { proxy_pass http://api_backend; proxy_read_timeout 120s; proxy_buffering off; } location ^~ /api/export/ { proxy_pass http://api_backend; proxy_read_timeout 600s; proxy_buffering off; } location ^~ /static/ { alias /data/www/common/static/; expires 30d; access_log off; } location = /favicon.ico { log_not_found off; access_log off; } } }

几个值得单独解释的参数。proxy_connect_timeout 5s设得短,是因为连不上后端时没必要让用户干等,5 秒内没建立连接就赶紧报错;proxy_read_timeout默认 60 秒,导出这类长耗时接口需要单独放大到 600 秒,但别全站设成 600,否则后端卡死时用户会挂在那里不动,体验更差。proxy_buffering off用在流式导出和 SSE 推送场景,关掉缓冲后数据能立刻到浏览器,否则 Nginx 会攒满一个 buffer 才往下发,前端进度条就是不动。

5.2 命令行逐个验证,别等浏览器报错才回头

配置写完别急着systemctl restart,先跑nginx -t检查语法,这一步能拦掉 80% 的低级错误。然后我习惯从近到远按顺序验证,把问题一层一层剥开:

# 第一层:后端本身是否活着 curl -i http://127.0.0.1:8081/ curl -i http://127.0.0.1:8082/admin/ curl -i http://127.0.0.1:8083/api/health # 第二层:Nginx 到后端这一跳通不通 curl -i -H "Host: portal.example.com" http://127.0.0.1/admin/index.html curl -i -H "Host: portal.example.com" http://127.0.0.1/api/user/list # 第三层:真实域名(走 DNS 和证书) curl -i https://portal.example.com/api/health # 第四层:确认后端看到的真实 IP curl -i -H "X-Forwarded-For: 1.1.1.1" http://127.0.0.1:8083/api/whoami

第二层用-H "Host: ..."模拟域名,是非常实用的技巧——不用改 hosts、不用等 DNS 生效,就能在服务器本地验证server_name路由对不对。第四层用来确认你的RemoteIpValve配置是否生效,如果后端返回的还是127.0.0.1,说明 Valve 没加载或者internalProxies没匹配上。

再看日志,这是排查的命门。我在log_format里加了rt、up、urt三个字段,分别对应请求总耗时、上游地址、上游响应耗时。有了它们,502 和 504 的区别一眼就能看出来:up有值但urt是-,说明连接都没建立,后端端口没通;urt有值且接近超时值,说明是后端处理太慢。这两个字段我建议所有人都加上,排查效率提升不止一倍。

5.3 前后端联调时必须对齐的三处

配置跑通不等于业务能用。多项目路径转发最容易在联调阶段翻车,因为路径前缀在三个地方各定义了一次:Nginx 的location、后端的contextPath、前端的baseURL。这三处只要有一处不一致,表现就是接口 404 或者静态资源加载不出来。

我的做法是建立一个唯一事实来源,通常是一张链路表,写清楚每个项目的完整外部路径、Nginx 转发规则、后端 contextPath、前端 baseURL 四列,谁改配置谁更新这张表。比如:

外部访问路径Nginx 规则后端 contextPath前端 baseURL
/api/user/list^~ /api/→http://api_backend(不剥前缀)/api/api
/admin/index.html^~ /admin/→http://admin_backend(不剥前缀)/admin/admin/
/static/app.3f2a1.js^~ /static/→alias不经过后端/static/

这张表看着朴素,但在多人协作的项目里能挡掉大量扯皮。我遇到过前端同学把 baseURL 写成/api/(带尾斜杠)导致请求拼成/api//user/list,后端某些框架对双斜杠不敏感还能跑,某些就返回 404,查了半天才发现是多了一个字符。

6. 踩坑实录与排查速查

6.1 502、504、404 的定位路径各不相同

502 Bad Gateway的本质是 Nginx 连不上上游,或者上游提前关了连接。按这个顺序查:先ss -lntp | grep 808x看端口在不在监听;再curl直连后端确认应用本身活着;如果端口在但连不上,看是不是address绑成了127.0.0.1而 Nginx 在另一台机器上;如果直连没问题但走 Nginx 就挂,检查 SELinux 和防火墙——setsebool -P httpd_can_network_connect 1这条命令我被它坑过整整一个下午,配置全对、日志只写Connection refused,最后发现是 SELinux 拦了 Nginx 对外发起连接。

还有一种隐蔽的 502:后端改完代码重启中,Nginx 把请求打过去,连接被拒,用户当场看到错误页。解决办法是在 upstream 里加proxy_next_upstream error timeout http_502;并准备备用节点,或者干脆在发布时优雅停机。

504 Gateway Timeout是连上了但等太久。默认proxy_read_timeout60 秒,后端处理超过就断。判断标准很简单:看日志里的urt,如果接近 60 秒,那就是真超时;如果urt只有几百毫秒却报 504,那多半是client_body_timeout或者客户端上传太慢,问题不在后端。真正的解决办法通常不是无脑调大超时,而是去查后端为什么慢——数据库慢查询、线程池排队、外部接口阻塞,调大超时只是把问题往后推。

404 Not Found在多项目场景下几乎全是路径拼接问题。第一步看 access.log 里 Nginx 收到的原始 URI 是什么,第二步看你配的proxy_pass有没有斜杠,第三步看后端 contextPath 有没有对上。我总结了一个粗暴的判断:如果 404 页面的样式是后端返回的(比如 Tomcat 的猫头页面),说明请求到后端了但路径不对;如果是 Nginx 自己的 404 页,说明压根没匹配上 location。

6.2 静态资源 404 与路径重复拼接

这个坑值得单独讲,因为它太典型了。假设前端打包时publicPath设成了/admin/,页面里引用了css/app.css,浏览器实际请求的是/admin/css/app.css。如果你的 Nginx 规则是location ^~ /admin/ { proxy_pass http://admin_backend/; }(注意结尾有斜杠),前缀被剥掉,后端收到的是/css/app.css,而 contextPath 是/admin,对不上,404。修法有两个:要么去掉proxy_pass结尾的斜杠让它原样透传,要么把后端 contextPath 改成根路径。

还有一种是"路径重复拼接"——访问/admin/admin/index.html也能打开,说明某处多拼了一层。这通常发生在alias配置上,location /admin/ { alias /data/www/admin/admin/; }这种手误,或者 location 前缀和目录结构没对齐。判断方法是nginx -T导出完整配置,把root、alias、proxy_pass三者的组合在纸上推一遍。

再说个容易忽略的:location里的正则如果在项目前缀之前,并且能匹配上,那它会抢走本该给项目的请求。这就是前面说的^~的价值。我在一个项目里因为一条location ~* \.(js|css)$的全局正则写在前面,导致所有项目的 js/css 请求都被它拦下来去本地目录找文件,结果全站样式丢失,查了两小时。

6.3 常见问题速查表

现象最可能的原因快速确认方式处理方式
502后端未启动 / 端口不对 / SELinux 拦截ss -lntp+ 直连 curl启动服务、改address、开 SELinux 布尔值
504后端处理超时 / 线程池打满看日志urt是否接近超时值定位慢点,针对性加超时或扩容
404(Nginx 页)location 未匹配看 access.log 原始 URI补 location 或加^~
404(Tomcat 页)contextPath 与转发路径不一致对比 proxy_pass 斜杠与 contextPath统一路径或改剥前缀策略
静态资源 404alias/root拼接错误nginx -T导出配置推演修正alias结尾斜杠
后端拿到 127.0.0.1未配RemoteIpValve/whoami类接口验证加 Valve 并配internalProxies
重定向到 :8081未配proxyPort/scheme看响应Location头在Connector上补两个属性
无限重定向X-Forwarded-Proto未传看浏览器网络面板补proxy_set_header
上传大文件 413client_max_body_size太小看响应状态码调大该值并同步 Tomcat 的maxPostSize
上传大文件 504上传耗时长日志rt很长但urt为-放宽client_body_timeout
请求头丢失location 里重写了proxy_set_header对比 server 与 location 配置整组补齐或在 server 层统一

6.4 我踩过之后才想明白的几个经验

第一条,配置改动一定要灰度。我在一台机器上同时跑三四个项目时,改 Nginx 配置前一定先cp nginx.conf nginx.conf.bak.$(date +%s),然后nginx -t,通过了再nginx -s reload。reload是平滑的,不会断开现有连接,但如果配置有语法错,reload会失败且旧配置继续生效——这其实是好事,最怕的是直接restart,一错全站停。

第二条,不要在 location 里重复写请求头。前面提过,proxy_set_header在子级是整组覆盖不是合并,这个特性坑过我一次:在某个 location 里只补了一行proxy_set_header Host $host;,结果那个项目的 IP 还原全失效,日志里全是 127.0.0.1。后来我养成习惯,请求头只在server层写一次,location 里除非有特殊需求否则一个字都不加。

第三条,超时参数分接口设,不要全站一个值。普通接口 60 秒足够,导出接口要 600 秒,异步推送要关缓冲。如果全站统一 600 秒,后端一旦挂起,用户会盯着转圈十分钟,投诉电话直接打爆;如果全站 30 秒,导出功能又要频繁失败。按 location 分组配置是最合理的。

第四条,日志字段要把上游信息带上。只记$status的话,502 和 504 你分不出是连接失败还是响应超时;加了$upstream_addr和$upstream_response_time,一眼就能定位到是哪台后端、慢在哪个环节。这个投入产出比极高,改一行log_format的事。

第五条,多项目场景下给每个项目留独立的日志文件。用access_log /var/log/nginx/admin.access.log main;按 location 分文件,出问题时不用在大日志里翻半天,直接看对应项目的日志,还能顺便做每个项目的流量统计。文件多了以后记得配logrotate,不然几个月就把磁盘写满。

最后说个实际感受:这套 Nginx 加 Tomcat 的组合,配置本身不难,难的是把"外部路径、转发规则、后端 contextPath、前端 baseURL"这四者的关系在心里理顺。我现在的习惯是每接一个新项目,先在纸上把这条链画出来,四段路径标清楚,然后照着写配置,基本一遍就能过。反过来,如果哪次偷懒跳过了这一步,大概率要在 404 和静态资源丢失之间来回折腾一个下午。慢就是快,这句话在配置管理上体现得特别明显。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 11:45:39

OpenCode:AI驱动的前端开发效率革命

1. OpenCode初探&#xff1a;当AI遇上前端开发第一次听说OpenCode是在一个技术论坛上&#xff0c;当时看到有人分享用AI自动生成前端代码的案例。作为一名有五年经验的前端工程师&#xff0c;我的第一反应是怀疑——这玩意儿真能写出可维护的代码吗&#xff1f;但当我真正在VS …

作者头像 李华
网站建设 2026/11/3 11:45:38

MATLAB遗传算法工具箱GAOT安装配置与函数冲突解决指南

简介&#xff1a;这份文档面向使用 MATLAB 开展遗传算法优化的初学者与工程技术人员&#xff0c;系统梳理 GAOT&#xff08;Genetic Algorithm Optimization Toolbox&#xff09;遗传算法工具箱的安装流程与常见问题处理思路&#xff0c;帮助读者解决工具箱路径配置、启动警告以…

作者头像 李华
网站建设 2026/11/3 11:45:16

Java生态集成PyTorch:深度学习模型部署实践

1. 为什么要在Java生态中使用PyTorch&#xff1f; 作为一名长期在Java生态中工作的开发者&#xff0c;我最初对在Java中使用PyTorch持怀疑态度。毕竟Python才是PyTorch的原生语言&#xff0c;为什么要"舍近求远"呢&#xff1f;直到我在实际项目中遇到以下场景&#x…

作者头像 李华
网站建设 2026/11/3 11:45:09

SolidWorks流体模拟分析实战:风道散热与结果判读

做结构设计的同行大概都遇到过一个场面&#xff1a;结构件画完了、装配体也干涉检查完了&#xff0c;结果老板或者客户突然问一句"这个风道里风速够不够""这个电控箱里的板子会不会过热""这个阀门开一半的压降是多少"。以前这类问题要么凭经验拍…

作者头像 李华
网站建设 2026/11/3 11:45:07

rm -rf 危险拆解:从参数语义、通配符到误删恢复与防护

rm -rf 这条命令&#xff0c;我见过的翻车次数比任何一条 Linux 命令都多。它短&#xff0c;四个字符加一个减号再加两个字母&#xff0c;敲下去连个回车反馈都没有——没有进度条&#xff0c;没有确认提示&#xff0c;没有后悔药。等到你 ls 一下发现目录空了&#xff0c;事…

作者头像 李华
网站建设 2026/11/3 11:44:12

Android权限机制详解:从基础到高级实践

1. Android权限机制概述 在移动应用开发领域&#xff0c;权限管理始终是保障用户隐私和数据安全的核心防线。Android系统作为全球市场份额最大的移动操作系统&#xff0c;其权限机制经历了多次重大演进&#xff0c;从最初的粗放式管理到如今的精细化控制&#xff0c;形成了完整…

作者头像 李华