☰
Nginx+Tomcat反向代理与多项目路径转发配置指南
2026/11/3 23:48:07 网站建设 项目流程

1. 为什么非要在 Tomcat 前面加一层 Nginx

很多人第一次接触这套组合,都是从"端口怎么变成 80 了"这个疑问开始的。我手上这套环境最初的形态很朴素:三个业务项目各自打包成 war,扔进同一个 Tomcat 的 webapps 目录,访问地址是http://内网IP:8080/项目名/xxx。用户嫌端口难看,运维嫌每次加项目都要改防火墙,我自己则被静态资源把 Tomcat 线程占满这件事折磨了很久。后来把 Nginx 放到最前面,做反向代理,再按路径把请求分发给后面的一个或多个 Tomcat 实例,这些问题才一次性收口。

Nginx 结合 Tomcat 做反向代理、再按路径转发多个项目,说白了就是让 Nginx 站在用户和 Java 应用之间当"前台接待":用户只看到 80 或 443 端口,Nginx 根据请求的路径前缀判断这个请求该交给哪个 Tomcat、哪个 context,然后把请求转过去、把响应收回来。它解决的是端口暴露、路径不统一、多项目共用域名、静态资源效率低、SSL 证书集中管理这几件事。适合阅读的人群也很明确:手里有一到多个 Java Web 项目、正在做部署架构收敛的开发或运维,以及被"项目加一个就要改一次配置"困扰的团队。哪怕你只会改server.xml和复制粘贴配置,下面的内容也能直接照着抄。

1.1 裸跑 Tomcat 会撞上的几个现实问题

先把这个话题的前提讲透,不然后面很多配置选择会显得没来由。Tomcat 本质是 Servlet 容器,它的强项是跑 Java 代码、管理连接池、处理会话,而不是坐在公网入口上分发流量。让它直接面向用户,最先暴露的问题通常是端口和路径:默认 8080 要开放出去,项目名会原样出现在 URL 里,http://x.x.x.x:8080/crm/order/list这种地址既不美观,也让后续换端口、换实例变得很麻烦——一旦实例迁移,所有前端写死的地址全得改。

第二个问题是静态资源。一个页面的 js、css、图片请求数量往往是接口请求的好几倍,这些请求全部由 Tomcat 的线程去读磁盘再返回,线程池被大量无意义的 IO 占住,真正需要计算的后端接口反而排队。把静态文件交给 Nginx 直接返回,Tomcat 的线程就能专心处理业务逻辑,这个收益在并发上来之后非常明显。

第三个是多项目共存时的隔离问题。多个 war 塞进一个 Tomcat,共享同一个 JVM 堆、同一个连接池上限,任何一个项目出内存问题或者长事务,其他项目跟着遭殃;而且 Tomcat 的context path一旦定死,前端路由和接口地址就得跟着它走。最后一个容易被忽略的是证书和入口治理:Tomcat 自己配 HTTPS 不是不行,但多实例时每台都要配一遍证书、每台都要处理跳转,运维成本直接翻倍。这四件事叠加起来,就是我把 Nginx 放到前面的直接动机。

1.2 反向代理究竟替我们挡掉了哪些事

"反向代理"这个词听着抽象,用前台接待来类比就很清楚:客户(浏览器)只跟接待(Nginx)说话,接待知道哪位同事(Tomcat 实例)负责哪块业务,把话带过去再把答复带回来,客户全程不需要知道同事坐在哪个工位。这个模型带来的是入口收敛——对外只有一个域名、一个端口,内部实例的 IP、端口、数量怎么变,用户完全无感。

具体到实际能拿到的好处,我按重要性排一下。端口与路径收敛是刚需,80/443 对外,8080/8081/8082 全部退回内网;路径重写让/crm/这种前缀可以映射到任意后端,甚至映射到 context 为/的应用;静态资源卸载让 Nginx 直接吐文件,顺手还能加缓存头;SSL 卸载把加解密集中到一个地方,后端走明文内网通信,证书更新只改一处;多实例负载靠 upstream 分发,某台挂了自动摘除;再往上一层,还能做限流、访问日志统一采集、灰度分流。这些能力不需要一次性全用上,但架构上留着这个位置,后面加什么都很自然。

1.3 多项目共存的三种拓扑,选错了后面全是返工

路径转发的落地方式其实不止一种,选之前先想清楚项目的演进方向。我把常见的三种整理成表,方便对照自己的场景:

拓扑形态部署方式优点明显短板适用场景
单 Tomcat 多 context一个实例,多个 war配置最简单,省内存故障域重合,一个项目拖垮全部内部系统、项目数量少、流量低
多 Tomcat 单项目每个项目独立实例和端口隔离彻底,可单独重启扩容内存开销翻倍,端口要规划对外业务、有独立发布节奏
多 Tomcat 多实例每项目 1-N 个实例 + upstream可水平扩容、可灰度配置和运维复杂度最高有并发压力、需要高可用

选择逻辑并不复杂:项目之间是否共用同一批用户、是否需要独立发布、单项目的峰值流量有多大。我见过最常见的一种错误是"一步到位",两三个小项目上来就铺六七个 Tomcat 实例,结果内存被 JVM 的固定开销吃掉大半,机器负载反而不高——JVM 本身是有基础内存成本的,实例数量要和实际并发匹配。反过来,把所有项目长期塞在一个实例里,遇到一次 Full GC 就全线超时,这种代价通常比多占几 GB 内存更难接受。我的建议是:先按"单实例多 context"起步,把 Nginx 的路径映射写好,等某个项目的发布节奏或流量明显独立出来,再把它拆成独立实例——因为 Nginx 侧的配置几乎不用改,只换 upstream 指向。

2. 把proxy_pass那条斜杠彻底搞明白

这套架构里 90% 的诡异问题,都出在两处:location匹配到了不该匹配的规则,或者proxy_pass的斜杠让路径被"吃掉"或"多出"了一段。这两件事必须单独拎出来讲清楚,否则后面照着配置抄也会抄错。

2.1 location 的匹配优先级,记住这四条就够了

Nginx 收到请求后按固定顺序筛选 location,顺序错了就会出现"我明明写了更具体的规则却没生效"。优先级从高到低是:=精确匹配最优先,命中即停;其次是^~前缀匹配,命中后不再尝试正则;然后是~和~*正则匹配,按配置文件里出现的先后顺序,先匹配到的赢;最后才是普通前缀匹配,并且取"最长前缀"而不是"最先出现"。这里有个反直觉的点:普通前缀匹配虽然最后才用,但 Nginx 会先记住最长的那条前缀,只有在正则全部没命中时才用它。

举几个能直接对照的例子。location = /health只匹配/health这一个地址,/health/都不算;location ^~ /static/命中后立刻返回,哪怕后面有~ \.jpg$的正则也不会再试;location /app1/和location /app1/report/同时存在时,访问/app1/report/x走后者,因为前缀更长。还有一个坑要提前说:如果某个前缀 location 后面跟了proxy_pass,同时又存在能匹配到它的正则 location,正则优先,这在多项目配置里很容易造成静态资源被转到了后端应用上。我的习惯是把静态资源单独用^~前缀锁死,避免被正则抢走。

注意:location末尾有没有斜杠是两回事。location /app1会匹配/app1、/app1/、/app1abc;location /app1/则不会匹配/app1abc。做路径转发时一律加尾部斜杠,能省掉很多跨项目的误匹配。

2.2proxy_pass带斜杠和不带斜杠,结果完全不同

这是最容易踩的一脚。规则可以浓缩成一句话:proxy_pass后面如果带了 URI(也就是域名端口之后还有路径),Nginx 会把location匹配到的那部分前缀替换成这个 URI;如果只写到域名端口、不带任何路径,则原样透传完整请求路径。更细一点:

location 写法proxy_pass 写法请求/app1/user/list实际转发到
location /app1/http://127.0.0.1:8081http://127.0.0.1:8081/app1/user/list
location /app1/http://127.0.0.1:8081/http://127.0.0.1:8081/user/list
location /app1/http://127.0.0.1:8081/app1/http://127.0.0.1:8081/app1/user/list
location /app1/http://127.0.0.1:8081/v2/http://127.0.0.1:8081/v2/user/list

看第二行和第三行的区别:同样是/app1/前缀,proxy_pass写单个斜杠就把前缀吃掉了,写/app1/就保留下来。所以后端应用的 context path 到底是什么,决定了你该写哪一种。如果 Tomcat 里部署的 war 包叫app1.war,context 是/app1,那就该写http://127.0.0.1:8081/app1/或干脆不带路径;如果 war 包改名为ROOT.war,context 是/,就必须用带单斜杠的写法把前缀剥掉。

还有一个硬性限制必须记住:当location使用正则(~或~*)时,proxy_pass不允许带 URI 部分,否则启动直接报 "proxy_pass cannot have URI part in location given by regular expression"。遇到这种需求,要么改用前缀匹配,要么用rewrite改写路径后再透传。

2.3 请求头、Cookie 与 context path 的连带影响

请求转发过去之后,后端应用眼里看到的"自己"是谁?如果不做处理,Tomcat 看到的是 Nginx 的地址,request.getScheme()拿到的是http,getServerPort()拿到的是 8081,生成的重定向地址和静态资源绝对路径就会指向内网端口,用户浏览器根本访问不到。这就是很多人在配置完代理后发现"登录后跳回 8080"或者"页面样式全丢"的根因。

标准做法是补三个头:Host透传原始域名、X-Real-IP传真实客户端 IP、X-Forwarded-For追加代理链、X-Forwarded-Proto标记原始协议。Java 侧如果用的是较新的 Tomcat,可以在server.xml的Host节点里加RemoteIpValve,它会读取这些头并修正getRemoteAddr()、getScheme()的返回值。老项目里也可以通过过滤器手动处理。

Cookie 的问题更隐蔽。Tomcat 的JSESSIONID默认 path 就是 context path,多个项目共用同一个域名时,理论上 path 不同不会互相覆盖,但只要有一个项目的 context 是/,它的JSESSIONID就会顶着Path=/覆盖整个域名,导致其他项目登录态反复失效。解决办法有两个:一是给不同项目配置不同的会话 Cookie 名称,二是把 context 统一成带前缀的形式,别让任何项目占用根路径。前者改起来更快,后面实操部分会给出具体配置。

3. 从零搭一套能跑起来的环境

讲完原理,落到具体操作。下面这套规划是我目前还在用的结构,三个项目分别跑在两个 Tomcat 实例上,Nginx 按路径分发,静态资源由 Nginx 直接返回。你可以按自己的项目数量等比调整。

3.1 端口与实例规划

先把表列出来再动手,能省掉后面反复改配置的时间。端口规划的原则是:对外只留 80/443,管理端口不对外,实例端口段固定且留出余量。

角色地址与端口说明
Nginx0.0.0.0:80 / 443唯一对外入口
Tomcat A127.0.0.1:8081承载/crm/、/report/两个 context
Tomcat B127.0.0.1:8082承载/portal/(ROOT 应用)
Tomcat 管理端口127.0.0.1:8005 / 8009只监听本地,关闭 AJP
静态资源目录/data/www/static/Nginx 直接读取,不经过后端

实例监听127.0.0.1而不是0.0.0.0这点很关键——后端端口不应该被同网段其他机器直接访问,全部流量必须经过 Nginx,否则限流、日志、鉴权这些入口治理手段形同虚设。AJP 连接器如果不用,直接在server.xml里注释掉,少一个暴露面。

3.2 Tomcat 侧要改的三处配置

第一处是端口,conf/server.xml里把Server port改成 8005 之外的端口(多实例共存时必须改,否则第二个实例起不来),Connector port改成规划好的 8081/8082,redirectPort保持一致。第二处是连接器参数,把默认线程数按实际并发调一下,maxThreads给到 200,acceptCount给 100,connectionTimeout20000 毫秒,URIEncoding="UTF-8"一定要显式写上,避免中文参数在某些环境下乱码。

<Connector port="8081" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="200" minSpareThreads="20" acceptCount="100" connectionTimeout="20000" maxConnections="10000" URIEncoding="UTF-8" redirectPort="8443" /> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="false"> <Valve className="org.apache.catalina.valves.RemoteIpValve" remoteIpHeader="X-Forwarded-For" protocolHeader="X-Forwarded-Proto" protocolHeaderHttpsValue="https" /> <Context path="/crm" docBase="/data/apps/crm" sessionCookieName="CRMSESSION" /> <Context path="/report" docBase="/data/apps/report" sessionCookieName="REPORTSESSION" /> </Host>

第三处就是上面这段里的Context与RemoteIpValve。autoDeploy="false"是刻意关掉的,生产环境不希望 Tomcat 自己扫描目录决定部署什么,部署动作应该由明确的脚本控制。sessionCookieName给每个项目起独立名字,就是为了解决上一节说的 Cookie 串号问题——这比纠结 context 前缀省事得多。RemoteIpValve则是让后端能拿到真实 IP 和原始协议,日志里记录的访问来源才有意义。

3.3 Nginx 的完整配置

配置我按"先全局、再 upstream、最后 server"的顺序组织,完整贴出来,可以直接改路径使用。

worker_processes auto; events { worker_connections 10240; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; client_max_body_size 50m; map $http_upgrade $connection_upgrade { default upgrade; '' close; } upstream tomcat_a { server 127.0.0.1:8081 max_fails=2 fail_timeout=10s; keepalive 32; } upstream tomcat_b { server 127.0.0.1:8082 max_fails=2 fail_timeout=10s; keepalive 32; } server { listen 80; server_name app.internal; # 静态资源直出,前缀锁死,避免被后面的正则抢走 location ^~ /static/ { root /data/www; expires 7d; access_log off; } # 前端 history 路由兜底 location /portal/ { proxy_pass http://tomcat_b/; proxy_http_version 1.1; proxy_set_header Connection ""; include /etc/nginx/proxy_common.conf; } location /crm/ { proxy_pass http://tomcat_a/crm/; proxy_http_version 1.1; proxy_set_header Connection ""; include /etc/nginx/proxy_common.conf; } location /report/ { proxy_pass http://tomcat_a/report/; proxy_http_version 1.1; proxy_set_header Connection ""; include /etc/nginx/proxy_common.conf; } location = /health { default_type text/plain; return 200 "ok\n"; } location / { return 404; } } }

被include引入的公共片段,单独放一个文件,所有 location 复用,改一处生效全部:

# /etc/nginx/proxy_common.conf 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; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_connect_timeout 5s; proxy_send_timeout 60s; proxy_read_timeout 60s; proxy_next_upstream error timeout http_502 http_503;

注意proxy_pass后面带不带 context 是从后端实际部署形态反推的判据:/crm/和/report/这两个项目 war 包名没改,context 就是/crm、/report,所以proxy_pass里把 context 带上了;/portal/部署成了ROOT.war,context 是/,所以只写单斜杠把前缀剥掉。这一段如果搞混,表现就是访问/portal/返回 404,而 Tomcat 的localhost_access_log里能看到请求确实到了后端,只是路径不对——这类问题对照上一节的换算表一看就明白。

location / { return 404; }这条兜底是我特意加的。多项目环境下,如果没有兜底规则,未匹配的请求可能落到某个默认 location 上被转给后端,白跑一趟还污染日志。直接 404 反而让问题暴露得更快。

3.4 联调验证,按四步走

配置写完nginx -t过一遍语法,然后nginx -s reload。验证不要一上来就点页面,按下面顺序看,出问题能立刻定位是哪一层:

  1. curl -I http://app.internal/health确认 Nginx 本身活着,返回 200;
  2. curl -I http://127.0.0.1:8081/crm/绕过 Nginx 直连后端,确认应用自身正常;
  3. curl -I http://app.internal/crm/走代理,看状态码和响应头里的Set-Cookie路径;
  4. 打开浏览器实际点一遍,重点看登录、跳转、图片加载这三类动作。

第三步如果直连正常、走代理 404,问题基本锁定在proxy_pass的路径换算或 location 匹配;如果是 502,就看 Nginx 的error.log里connect() failed的地址是不是端口写错了;如果是 504,说明后端在超时时间内没响应,先把proxy_read_timeout临时调大确认是慢查询还是真的挂了。这套顺序看着啰嗦,但能避免"页面白屏然后到处乱猜"的无效排查。

4. 上线之后踩过的坑,以及怎么绕开

配置跑通只是开始,真正花时间的是上线后各种"本地好好的、线上不对"的问题。下面这几类是我这几年反复遇到的,按出现频率排。

4.1 静态资源 404 与路径丢失

最常见的表现:页面 HTML 出来了,但样式全裸、js 报 404。原因通常是页面里写的是相对路径或绝对路径/static/js/app.js,而实际请求被location /或某个正则匹配走了,转发到后端后逻辑路径还多了层 context。排查方法很直接——F12 看 404 请求的完整 URL,再对照 Nginx 的access_log,如果请求根本没进 Nginx,是浏览器地址错了;如果进了 Nginx 但被转给后端,是 location 匹配问题。

我现在的固定做法是给静态资源单独一条^~前缀,物理目录和 URL 前缀一一对应,页面上统一用绝对路径/static/...。配合expires 7d让浏览器缓存,但文件名里必须带内容哈希,否则改了代码用户还在用旧缓存。另外要提醒一句:root和alias别写混,location /static/ { root /data/www; }对应的是/data/www/static/,而alias是直接把 location 路径替换成指定目录,两者差一层目录,配错了就是全 404。

4.2 登录态丢失、Cookie 串号

这个问题的表现非常有迷惑性:登录成功,刷新一下又回登录页,或者 A 项目登录后 B 项目的会话被踢掉。根源基本都在 Cookie 的Path和Domain上。Nginx 默认会把后端的Set-Cookie原样透传,如果后端 context 是/crm,JSESSIONID的 Path 就是/crm,这本身没问题;但如果某个项目的 context 是/,它的 Cookie 会顶着Path=/覆盖整个域名下所有同名 Cookie。

我用的解决办法就是第 3.2 节里的sessionCookieName,每个项目一个独立名字,从根上杜绝同名覆盖。另一个坑是 Cookie 里的Secure属性——如果 Nginx 做 SSL 卸载、后端走 http,Tomcat 可能会认为当前不是安全连接而不下发Secure标记,浏览器就会拒绝在 https 页面保存这个 Cookie。加上X-Forwarded-Proto: https配合RemoteIpValve就能解决,配置里那两个头不是写着好看的。

提示:排查 Cookie 问题时,浏览器开发者工具的 Application 面板里能直接看到每个 Cookie 的 Name、Domain、Path、Secure,对照域名一看就知道是重复覆盖还是压根没下发,比翻日志快得多。

4.3 上传失败、接口超时、WebSocket 连不上

这三类问题本质上都是"代理层的限制比应用层更严格"。上传失败通常是client_max_body_size默认 1MB 卡住了,返回 413,日志里能看到client intended to send too large body,改成 50m 或按业务实际上限设置即可。接口超时则是proxy_read_timeout默认 60 秒,导出报表、批量处理这类接口很容易超过,需要单独给那个 location 放宽,而不是全局调大——全局调大会让真正挂掉的请求占着连接不放。

WebSocket 或者 SSE 长连接连不上,问题在于 HTTP/1.1 的Upgrade头没有透传。必须显式写proxy_http_version 1.1加上Upgrade和Connection两个头,配置里那个map就是为了动态判断。这里有个细节:如果你在 upstream 里开了keepalive,同时又写死Connection: upgrade,普通请求的复用会被打断,所以要用map按实际请求头来决定,而不是硬编码。

4.4 问题速查表

把上面这些整理成一张表,出问题时先对号入座,能省掉大半瞎猜的时间:

现象最可能的根因快速验证处理方式
访问路径 404,后端日志无请求location 未匹配或 proxy_pass 路径错误curl -I直连后端对比对照斜杠换算表修正
502 Bad Gateway后端未启动或端口写错ss -lntp看端口改 upstream 地址或起服务
504 Gateway Timeout后端响应慢或连接池耗尽看后端线程栈与慢查询单独调高该 location 的 read_timeout
重定向到内网端口未透传 Host / Proto看响应头 Location补 proxy_set_header
登录态反复失效Cookie 同名覆盖或 Path 冲突浏览器 Application 面板改 sessionCookieName
上传 413client_max_body_size 过小看 error.log按业务调整并单独 limit
样式全丢静态路径被转发到后端对比请求 URL 与 Nginx 日志加^~静态 location
WebSocket 握手失败未透传 Upgrade看 101 响应是否存在加 http_version 与两个头

5. 把入口做扎实:日志、限流与平滑发布

配置能跑之后,剩下的工作是让这套入口在出问题时"看得见、兜得住、改得动"。这部分不是必须做,但只要项目还在迭代,早晚都要补上。

5.1 统一日志格式,把耗时变成可查的数字

默认的combined格式记不下关键信息,至少要加三个字段:上游地址、上游耗时、总耗时。改了之后,慢请求能直接从日志里筛出来,不用靠用户反馈"页面很卡"再去猜是哪个项目。

log_format main_ext '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" "$http_user_agent" ' 'upstream=$upstream_addr ustatus=$upstream_status ' 'urt=$upstream_response_time rt=$request_time'; access_log /var/log/nginx/access.log main_ext;

三个时间字段的含义要分清:$request_time是从收到客户端第一个字节到响应发完的总时长,包含网络耗时;$upstream_response_time只是后端处理耗时。两者差距大说明瓶颈在网络或客户端慢,差距小但都很大说明后端本身慢。我一般按urt排序看最慢的二十条请求,基本能定位到具体接口。

5.2 限流不是可选项,是保护后端的最低成本手段

反向代理做限流比在应用里做限流省事得多,因为它在请求进入 JVM 之前就拦掉了。按 IP 限速用limit_req_zone,按连接数保护用limit_conn_zone,两个配合基本够用:

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s; limit_conn_zone $binary_remote_addr zone=conn_limit:10m; location /crm/ { limit_req zone=api_limit burst=40 nodelay; limit_conn conn_limit 20; proxy_pass http://tomcat_a/crm/; }

rate=20r/s是平均速率,burst=40允许瞬时突发的排队容量,nodelay表示超出速率的请求不排队直接处理(但要计入 burst 额度)。参数怎么定?看后端maxThreads和单请求平均耗时:如果单请求平均 100 毫秒、线程池 200,理论吞吐上限约 2000 QPS,那单 IP 限到 20r/s 对正常用户完全够用,对爬虫和异常重试则足够拦下。限流阈值宁可先宽后紧,一上来就卡太死,正常用户点快点就被拒绝,投诉比攻击还多。

5.3 HTTPS 与多域名的 server 块组织

证书集中到 Nginx 是这套架构最实际的收益之一。配好ssl_certificate之后,后端全部走内网明文,证书续期只改一处,多个 Tomcat 实例不用各自折腾。80 端口留一个只做跳转的 server 块,其余业务规则统一放到 443 的 server 里,避免两套规则各写一遍导致不一致。

server { listen 80; server_name app.internal; return 301 https://$host$request_uri; } server { listen 443 ssl; http2 on; server_name app.internal; ssl_certificate /etc/nginx/cert/app.crt; ssl_certificate_key /etc/nginx/cert/app.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 业务 location 与 3.3 节一致 }

多域名的情况,我倾向于一个域名一个server块,把业务 location 抽到一个include文件里复用。这样新增域名时只需要复制十几行的 server 骨架,路径规则不用重写,也不会出现"改了 A 域名忘了改 B 域名"的情况。server_name匹配不到时 Nginx 会用第一个 server 块作为默认,如果这个默认块恰好是业务站点,未备案域名解析过来也能访问,所以我会单独放一个返回 444 的默认 server 兜住。

5.4 平滑重启与灰度:让发布不再挑时间

配置文件改完不要在业务高峰直接reload了事,先nginx -t校验语法,再nginx -s reload。reload 是平滑的,老 worker 处理完存量连接后退出,不会掐断正在进行的请求。但有一个前提:配置文件里不能有语法错误,否则 reload 失败、Nginx 继续用旧配置跑,你以为生效了其实没有,这种"静默失败"最坑。我的习惯是校验通过后立刻curl -I打一次,确认响应头里是新配置的特征(比如新增的 header)再撤。

灰度靠 upstream 就能做,不需要额外组件。下面这种写法把流量按权重分给新老两组实例,观察一段时间没问题再逐步调权重,出问题把新组整组down掉即可回滚:

upstream tomcat_a { server 127.0.0.1:8081 weight=9; server 127.0.0.1:8091 weight=1; # 新版本,先接 10% 流量 server 127.0.0.1:8082 backup; # 兜底实例 keepalive 32; }

backup标记的实例平时不接流量,主组全部不可用时才启用,这比配一堆健康检查脚本简单有效。max_fails=2 fail_timeout=10s的意思是 10 秒内失败两次就暂时摘除,10 秒后再试探。这套机制对"进程假死但仍然监听端口"的情况无能为力——那种场景需要主动健康检查,开源版没有,通常靠外部探活脚本定期curl一下/health来兜。

5.5 几个我踩过之后才记住的细节

最后补几条零散但很实用的经验。proxy_pass里如果用了 upstream 名字(http://tomcat_a/),Nginx 只在启动时解析一次域名,后端实例 IP 变了必须 reload——用固定 IP 就绕开这个问题。keepalive在 upstream 里配了之后,location 里必须同时写proxy_http_version 1.1和proxy_set_header Connection "",否则长连接不生效,等于白配。文件描述符上限记得调,worker_connections写得再大,系统ulimit -n卡在 1024 也是白搭。还有个小技巧:临时排查问题时在 location 里加一行add_header X-Debug-Upstream $upstream_addr always;,浏览器响应头里直接能看到这次请求落到了哪台实例,比翻日志快,排查完删掉即可。

这套东西我前后折腾了大概两周才稳定下来,中间最浪费时间的一段,是一直用"改完直接刷新页面"的方式验证,出了问题不知道是 Nginx 没生效、路径算错了、还是后端本来就 500。后来养成习惯,任何一次改动都按"直连后端 → 走代理 → 浏览器实测"三步走,每一步都确认过再往下,看起来慢,实际比盲目试错快得多。多项目路径转发这件事本身不复杂,真正花心思的是边界——哪个路径归谁、前缀怎么剥、Cookie 怎么隔,把这些想清楚了,配置反而是最省事的一环。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询