
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和schemeConnector port8081 protocolHTTP/1.1 connectionTimeout20000 maxThreads400 acceptCount200 address127.0.0.1 proxyPort80 schemehttp redirectPort443 /proxyPort和scheme这两个属性的作用是让 Tomcat 在没有直接参与外部连接的情况下知道外面那层用的是 80 端口、http 协议。不加的话代码里request.getRequestURL()拼出来的地址会带上:8081做重定向或者生成绝对链接时就会跳到一个外网访问不到的端口上前端页面直接白屏。这个坑我见过太多次。第二处是在server.xml的Host节点里开RemoteIpValve专门用来从X-Forwarded-For头里还原真实客户端 IPValve classNameorg.apache.catalina.valves.RemoteIpValve remoteIpHeaderX-Forwarded-For protocolHeaderX-Forwarded-Proto internalProxies127\.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的默认值通常是 1024worker_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/listlocation匹配上了/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: /apiNginx 用不带斜杠的写法原样透传。这样做的好处是路径只有一份定义前后端联调时不容易对不上坏处是后端本地启动时访问地址也带前缀有些同学不习惯。反过来如果你用的是传统 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 之间是 httpTomcat 认为请求不安全又跳回 httpsNginx 再转成 http 给 Tomcat浏览器直接报ERR_TOO_MANY_REDIRECTS。3.3 upstream 块为将来留个扩展位即使当前只有一个后端实例我也建议用upstream包一层而不是在proxy_pass里写裸地址upstream portal_backend { server 127.0.0.1:8081 weight1 max_fails2 fail_timeout10s; keepalive 32; }好处有三一是以后要扩成两台机器只改 upstream 块location里的proxy_pass http://portal_backend一动不用动二是可以做健康探测max_fails2 fail_timeout10s意思是 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 的匹配规则顺序是精确匹配命中即停最高优先级^~前缀匹配命中后不再尝试正则~和~*正则匹配按配置文件里出现的先后顺序第一个命中即停普通前缀匹配记住最长的那一个但会继续往下试正则。我把它翻译成一句人话精确优先其次是被^~标记的前缀然后是正则谁写前面谁先试最后才是普通前缀最长者胜。第五种情况容易被忽略——如果正则和普通前缀同时存在正则优先于普通前缀哪怕普通前缀更长。举个例子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/svgxml; 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是否接近超时值定位慢点针对性加超时或扩容404Nginx 页location 未匹配看 access.log 原始 URI补 location 或加^~404Tomcat 页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 和静态资源丢失之间来回折腾一个下午。慢就是快这句话在配置管理上体现得特别明显。