1. 项目概述:动静分离和负载均衡到底解决了什么问题
先聊一个让很多团队头疼的场景:一套Java Web应用上线后,初期用户量不大,一台Tomcat扛得住,响应也还行。等业务量起来了,并发一高,Tomcat线程池被打满,页面加载越来越慢,更别提图片、CSS、JS这些静态资源——明明和业务逻辑没关系,却也挤占了宝贵的应用线程。这时候,很多人的第一反应是加服务器、堆配置,但钱花了,问题往往没根治。
用Nginx配合Tomcat做“动静分离”加“负载均衡”,就是一套成熟、低成本、见效快的解法。Nginx在前面顶住静态资源的请求和并发连接,Tomcat在后面专心处理JSP、Servlet和接口请求;再用负载均衡把请求分摊到多台Tomcat上,既提升了单机吞吐,也为后续的水平扩展铺好了路。这套架构几乎是Java Web项目的标配组合,无论是刚毕业的初级开发,还是带了几年团队的技术负责人,都应该掌握其中的原理和落地细节。
这篇文章我不打算只贴一份能跑的配置,而是把思路、参数选型、踩坑点一起拆开讲。你会看到为什么静态资源一定要交给Nginx、upstream的几种策略各自适合什么场景、配置写好后怎么验证效果,以及线上最常遇到的几个诡异问题是怎么排查的。适合正在给项目做性能优化的人,也适合准备架构面试的人——面试官问Nginx配置,很少只问语法,基本都会追到“为什么这么配”。
2. 整体设计思路:为什么要用Nginx挡在前面
2.1 动静分离的价值拆解
在没有Nginx介入的纯Tomcat架构里,所有请求都进Tomcat:请求一个1MB的图片和请求一段复杂SQL的接口,占用的是同一批线程。Tomcat的默认最大线程数通常是200左右,意味着同一时刻大约只能同时处理200个请求。如果50个请求都在下大图片,真正能分给接口处理的线程就只剩150个,接口的响应时间会明显被拉长——这是典型的“资源错配”。
动静分离的本质是:谁擅长什么,就让它干什么。Nginx处理静态资源有天然优势:基于epoll的事件驱动模型,一个进程能扛住数万并发连接,处理小文件时开销极低,还可以开启sendfile、gzip、缓存等特性,把这些能力发挥到极致。Tomcat则专注于动态逻辑:Java的线程模型更适合处理计算、数据库访问、远程调用这类任务。静态请求不再进入Tomcat的线程池,动态接口的可用线程数相当于被完整释放了,整个系统的吞吐能力自然就上去了。
还有一个常被忽略的收益:安全性和稳定性。Nginx挡在最前面,可以对请求做访问控制、限流、请求体大小限制;静态文件也不需要Tomcat去读了,减少了文件句柄的占用和磁盘I/O竞争。如果哪天某个Tomcat节点挂掉,Nginx还能通过健康检查自动把它摘掉,对用户几乎没有感知。
2.2 负载均衡的几种策略和选型依据
Nginx的负载均衡基于upstream模块实现,核心策略有四种,我按实际场景的适用度从高到低说。
默认的是轮询策略,每个请求按时间顺序逐一分配到后端Tomcat。它背后的逻辑是假设每台服务器配置相同、每个请求耗时相近,适合绝大多数无状态服务。但如果后端机器配置差异明显,比如一台8核16G、另一台4核8G,轮询会把流量等分,弱的机器容易成为瓶颈。这时就该用加权轮询,在server后面加weight参数,例如weight=2表示这台机器承担两倍于weight=1的流量。这个参数需要结合机器配置和压测数据来定,不是拍脑袋写的——我见过有人给弱机器配了更高的权重,结果上线后那台机器直接被打挂。
ip_hash策略按照客户端IP的哈希结果分发请求,同一个IP会稳定落在同一台后端服务器上。它解决的核心问题是Session共享:如果后端没有做Session同步或外置存储,轮询会导致用户请求跳转到不同Tomcat,Session频繁失效,用户表现为“动不动要重新登录”。但ip_hash也有副作用:某个IP下的请求量特别大时,会集中在某一台机器上,造成负载不均;如果那台机器故障,该IP下的会话也会全部受影响。它更适合那种“暂时没条件做Session外置,先保住登录态”的阶段。
least_conn策略会把新请求分配给当前连接数最少的后端,适合请求处理时长差异很大的场景。比如有的接口快、几十毫秒就返回,有的接口慢、要好几秒,单纯轮询会让处理慢请求的机器积压越来越多的连接,least_conn则能动态平衡。实际部署中,如果所有后端都是同规格机器且做了健康检查,我倾向于用least_conn或者带权重的least_conn,它在多数业务模型下表现都比较稳定。
还有一个容易被忽略的细节:Nginx的负载均衡是基于反向代理实现的。客户端只跟Nginx建立连接,Nginx再把请求转发给后面的Tomcat。这意味着Nginx本身不能成为单点故障,生产环境至少需要两台Nginx做高可用,比如配合keepalived挂一个虚拟IP。这个属于架构层面的延伸,单机Nginx的训练环境不需要考虑,但上线前必须纳入规划。
3. 核心参数拆解:Nginx配置里每一项都是为什么
3.1 手写一份动静分离加负载均衡的配置
先拿一份最典型的配置,配置里每行后面我会标出关键的参数意图,然后逐个拆开讲。
# 定义Tomcat后端节点 upstream tomcat_cluster { # least_conn; # 按连接数分配,注释掉表示默认轮询 server 10.0.0.11:8080 weight=1 max_fails=2 fail_timeout=30s; server 10.0.0.12:8080 weight=2 max_fails=2 fail_timeout=30s; keepalive 32; } server { listen 80; server_name example.com; # 动静分离:先匹配静态资源,由Nginx直接处理 location ~* \.(gif|jpg|jpeg|png|css|js|ico|woff2?|eot|ttf|svg)$ { root /data/webapp/static; expires 7d; access_log off; sendfile on; log_not_found off; } # 动态请求:反向代理到Tomcat集群 location / { proxy_pass http://tomcat_cluster; 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; index index.jsp; } }先看location的匹配顺序。location ~* \.(gif|jpg|jpeg|png|css|js|ico|woff2?|eot|ttf|svg)$用了正则匹配,Nginx会先尝试匹配普通前缀location,再按顺序匹配所有正则location,一旦命中就停止。所以这段正则必须放在普通location之前吗?不一定,因为正则匹配单独一个阶段,只要它是所有正则中第一个匹配成功的就行。但为了维护清晰,我习惯把静态资源的location写在前面。
根目录root /data/webapp/static的坑需要重点说。这个root配合正则location使用时,Nginx会把完整的请求URL拼到root后面去查找文件。比如请求/images/login.png,Nginx实际查找的文件路径是/data/webapp/static/images/login.png。如果项目中的静态资源路径本身带了一个前缀目录,比如/assets/images/login.png,根目录就必须是/data/webapp/static/assets,否则会404。很多人第一次配置时在这里卡半天,查文件权限没问题、目录存在没问题,结果就是路径拼接逻辑没搞明白。
expires 7d是配合浏览器做强缓存的常用手段。静态资源一般不会频繁变动,给一个7天的Expires头,用户第二次访问时会直接从本地缓存加载,连Nginx都不需要访问。这里要注意,既然设了强缓存,那前端发布新版本时,文件名必须带上版本号或内容哈希,比如app-3f2a9d.js,否则用户会一直拿到旧的缓存文件。这个配合问题属于团队协作层面的坑,后端配好了缓存,前端不处理版本号,照样会出线上事故。
把静态请求的日志关掉是性能细节。静态资源的访问量通常远大于动态接口,每个请求都写访问日志会带来大量磁盘I/O。access_log off对静态location生效,动态请求则保留日志便于排查。log_not_found off的意思是:如果静态文件不存在,Nginx不往错误日志里刷404记录,避免日志暴涨。
3.2 proxy_pass与tomcat之间必须交代清楚的头信息
动态请求转发到Tomcat时,有三个头是必须设置的,而且各有原因。
Host $host保证Tomcat拿到的Host头和用户浏览器访问时一致。如果不设置,Tomcat里的request.getServerName()会返回Nginx的内网IP,很多基于域名做判断的逻辑会全部失效,比如链接生成、单点登录回调、某些安全校验。这个参数的实质就是:反向代理不能篡改业务对域名的感知。
X-Real-IP和X-Forwarded-For是为了传递真实客户端IP。Nginx转发后,Tomcat直接通过request.getRemoteAddr()拿到的是Nginx的IP,所有客户端的IP都成了同一个值,日志分析、风控、限流全部失真。X-Real-IP是Nginx自定义的一个头,通常只加一层代理时用;X-Forwarded-For则是行业标准的追加式头,每经过一层代理就追加一个IP。Java后端一般用X-Forwarded-For里最左边、且不是信任代理地址的那一个IP作为客户端真实IP,而不是无脑取第一个值——如果前面还有CDN,这个解析逻辑要判断多层。
proxy_set_header X-Forwarded-Proto $scheme解决的是协议传递问题。Nginx如果配置了HTTPS,转发到Tomcat时如果不带这个头,Tomcat里的request.isSecure()会判定请求不是安全的,导致redirect的地址生成成http://开头的,明明用户访问的是https,跳转后就变成了http,页面提示不安全。这个细节点很多人上线TLS后才会发现。
顺带一提keepalive 32的含义。它是在upstream块内配置的,指定Nginx到后端Tomcat的空闲长连接数量。默认情况下,Nginx每转发一个请求就和Tomcat重新建立一条TCP连接,高并发时连接建立的开销很可观。设置keepalive后,Nginx会复用和后端的空闲连接,减少握手耗时。但需要注意,这个参数不是越大越好,它需要在Nginx的http块里同时开启upstream模块的长连接支持,并且Tomcat的maxKeepAliveRequests如果设得太小(比如默认的100),长连接复用次数多了,后端可能主动断开,Nginx侧会出现连接重置之类的报错。
3.3 Tomcat侧必须同步调整的配套参数
光调Nginx不管Tomcat是不行的。生产环境下后端服务的代码不变,但线程池、连接超时这些参数要和架构匹配起来。
Tomcat的Connector配置在conf/server.xml里,核心参数是:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" maxThreads="400" minSpareThreads="20" acceptCount="200" maxKeepAliveRequests="500" redirectPort="8443" />maxThreads决定Tomcat最多同时处理多少个请求线程,默认200,压力大时容易成为瓶颈。但盲调没用——线程数调到500,如果数据库连接池只有20个连接,线程会挤在数据库等待上,只是把积压点从Tomcat线程池转移到了数据库连接池,整体吞吐不会提升。我见到很多团队给Tomcat加线程,却不看MySQL的max_connections,最后数据库先崩了。所以调参一定是链路级联动的:Nginx的worker_connections、Tomcat的maxThreads、数据库连接池大小、应用自身的线程池,要按流量预测量级一起评估。
还有URIEncoding="UTF-8"这个参数。以前老版本Tomcat默认是ISO-8859-1,参数里的中文会乱码,新版本默认UTF-8了,但如果你维护的是老项目,还是检查一下比较稳妥。另外,一旦用了Nginx转发,Tomcat的server.xml中最好去掉<Host>里的autoDeploy="true"这类开发期配置,避免生产环境上热部署导致奇怪的问题。
后端如果有多台Tomcat,Session同步怎么处理,是架构设计里绕不开的点。最省事的方案是在Nginx用ip_hash把同一用户固定到同一台Tomcat。但这个方案有两个先天问题:一是某个节点挂掉后,固定到该节点的用户会话直接丢失,需要重登;二是流量分布可能不均衡。更稳的方案是引入Session外置存储,比如Redis统一保存Session,这样任何一台Tomcat都能读到同一份会话数据,ip_hash就可以去掉了,负载均衡的调度完全自由。Spring Boot项目里集成Redis Session非常快,两三行配置的事,个人建议哪怕刚开始只有两台Tomcat,也直接上Redis Session,后面扩容零障碍。
4. 实操过程:从零搭建一套可复现的环境
4.1 节点规划与安装准备
为了演示,我规划一套最小规模的环境。用虚拟机或者云主机都行,我这里用一个简化的内网网段做示意。准备两台配置相同的Linux服务器,装CentOS 7或更新的Linux发行版都可以。这里为了说明给它们起虚构代号:节点A和节点B。
| 角色 | 服务 | 对应机器 |
|---|---|---|
| 反向代理 | Nginx | 节点A |
| 后端应用 | Tomcat | 节点A + 节点B |
| 数据库 | MySQL | 独立节点或复用节点B(演示用) |
这套环境中,节点A既跑Nginx又跑一个Tomcat实例,节点B跑另一个Tomcat实例,模拟两台后端。实际上生产环境不建议Nginx和Tomcat同机部署,因为Nginx需要保持轻负载来支撑高并发连接,Tomcat占用CPU和内存较高,两者同机容易互相争抢资源。演示环境中为了节省机器,同机操作可以理解,但要明确标注出这是演示妥协。
Nginx安装没什么特别,用包管理器装或者编译安装都行。Linux发行版自带的Nginx通常版本偏旧,如果要用到一些新特性,建议通过官方源安装。我这边习惯用编译安装,一是能指定安装目录、模块,二是方便日后排查问题时快速定位版本和编译参数。但编译安装对小白不太友好,依赖没装全容易报错。如果只是为了快速验证动静分离和负载均衡,用系统包管理器安装就够了,后文配置不依赖任何编译模块。
Tomcat这边,我用的Java版本和Tomcat版本需要注意兼容性。Java 8对应Tomcat 8.5/9.0都没问题,Java 17的话建议Tomcat 10.1以上,因为Tomcat 10把javax.servlet迁移到了jakarta.servlet,代码包名变了,老应用直接扔上去会报ClassNotFoundException。这一步是很多人在升级时踩的大坑——Tomcat版本不是随便换的,要跟着Java和Servlet规范走。
4.2 部署手册级别的操作步骤
按顺序执行以下步骤。第一,在节点A和节点B上分别安装JDK和Tomcat,确保两个节点的Tomcat能独立通过http://节点IP:8080访问,这个是最基本的连通性验证。
第二,在节点A上装Nginx,在/etc/nginx/conf.d/下新建一个虚拟主机配置文件。我用的是一份更完整的配置版本:
user nginx; worker_processes auto; worker_rlimit_nofile 65535; events { worker_connections 10240; use epoll; } http { include /etc/nginx/mime.types; default_type application/octet-stream; sendfile on; tcp_nopush on; keepalive_timeout 65; client_max_body_size 20m; gzip on; gzip_types text/css application/javascript application/json image/svg+xml; gzip_min_length 1k; upstream tomcat_cluster { least_conn; server 节点A内网IP:8080 max_fails=2 fail_timeout=30s; server 节点B内网IP:8080 max_fails=2 fail_timeout=30s; keepalive 32; } server { listen 80; server_name 你的域名或IP; location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { root /data/webapp/static; expires 7d; access_log off; add_header Cache-Control "public"; } location / { proxy_pass http://tomcat_cluster; 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; } } }第三,部署一个最简单的Java Web应用用于验证。不需要太复杂,一个会返回当前服务器IP和Session ID的页面即可。在所有后端Tomcat的webapps目录下放同一个应用包,保证两台节点的应用代码完全一致。
第四,静态资源目录准备。在节点A的/data/webapp/static下放一张测试图片和一个测试CSS文件,并用chown -R nginx:nginx /data/webapp/static调整属主,避免Nginx worker进程因权限不足返回403。这个权限问题也是高频踩坑点,很多403不是配置问题,就是Nginx进程对目录没有读权限。
第五,启动所有服务后开始验证。浏览器访问Nginx地址,连刷几次,观察应用页面返回的服务器标识是否在节点A和节点B之间切换;直接访问一张图片的URL,用浏览器的开发者工具看响应头,确认有Expires或Cache-Control字段,确认响应服务器头(Server字段)显示的是Nginx而不是Tomcat,就说明静态请求被Nginx直接拦截了。
4.3 验证和压测:配置有没有生效,用数据说话
很多人在验证动静分离时只看“页面正常打开了”就认为配置生效了。这不够严谨。我用两个方法做验证。
第一个是响应头验证。动态请求经过Nginx转发后,Tomcat返回的响应头里通常会带有X-Powered-By之类的标识,Nginx自己处理静态文件时则没有。更直接的方法是看响应头里的Server字段:Nginx返回的一般是Server: nginx/版本号;如果请求被发到了Tomcat,Tomcat 9默认不一定返回Server头,但它的响应样式和其他字段能看出来区别。
第二个是日志验证。Nginx的access日志里,请求方式那一行的处理时间是关键指标。静态资源的请求耗时通常是个位数毫秒,动态请求可能几十到几百毫秒。配置生效后,静态资源的耗时应该远低于动态请求。我见过有人把所有请求都转发给Tomcat,但配置里照样写了一段静态location,结果静态location命中不到(因为路径不对),所有请求都走了动态转发,页面也能打开,但压测数据一对比就露馅了——吞吐上不去,Tomcat CPU飙升。
压测工具我推荐wrk或者ApacheBench(ab)。ab的优势是自带且简单,wrk更吃性能也更准。压测的目标不是测出极限数字,而是验证两点:一是静态资源请求完全不经过Tomcat,二是多台Tomcat都收到了请求。压测几轮后,在节点A和节点B的Tomcat本地访问日志里,能看到请求来自Nginx的IP(说明转发正常),且两台都有访问记录(说明负载均衡生效)。如果只有一台有记录,优先检查upstream里节点配置是否有误。
5. 常见问题实战排查:我踩过的几个典型坑
5.1 静态资源404,但文件确实存在
这个坑我前文提过一次,但值得单独展开。现象是:浏览器访问http://Nginx_IP/static/css/style.css,Nginx返回404,但在节点A的/data/webapp/static/static/css/style.css里手动创建目录后,居然能访问通了——很多人在这里恍然大悟。
原因在root的拼接逻辑。配置里写的是root /data/webapp/static;,请求URL是/static/css/style.css,Nginx实际查找的路径是/data/webapp/static/static/css/style.css。多了一层static目录。理解这个拼接机制以后,解法就有两种:一种是调整root为/data/webapp;另一种是改用alias:
location ~* ^/static/.*\.(css|js|png)$ { alias /data/webapp/; }alias会直接把location匹配到的路径替换为指定路径,不像root那样拼接。但alias与正则location的配合有时会有限制,部分Nginx版本中正则location不能用alias。所以更稳妥的方案是直接用root,把目录结构设计成和URL路径一致,或者用location ^~ /static/加root的组合,前缀匹配配合root,语义最好理解:
location ^~ /static/ { root /data/webapp/; }这个前缀匹配会命中所有/static/开头的请求,root指定为/data/webapp/,实际查找路径就是/data/webapp/static/...,完美对应。注意^~的意思是命中后不再继续匹配正则location,但代价是static目录下的所有文件不会再走后面的正则规则。
5.2 动态请求Session老是丢失
应用部署了多台Tomcat,用户登录后访问几次就退出登录,排查Nginx配置没问题,后台日志能看到请求在不同Tomcat之间跳转。这个问题的根源就是Session不共享。
Nginx层面的临时解法是改用ip_hash,但长远方案一定是Session外置。我用某个有状态服务的真实案例说明:某系统为用户中心,三个后端节点,最开始是轮询,用户反馈登录状态老掉。后来先改ip_hash,现象消失,但一段时间后发现节点之间流量倾斜严重。最终方案是引入Redis Session统一存储,用户会话全部落在Redis里,Nginx改回least_conn,后端任意一台都能处理任意用户的请求,故障节点摘除后用户无感知。
Spring Boot项目改造Redis Session很简单,引入spring-session-data-redis依赖后,在配置类上加@EnableRedisHttpSession即可。遗留的Spring MVC项目可以用Spring Session的XML方式配置,原理相同。这里要额外注意Redis本身的可用性——Session放到Redis后,Redis成了关键节点,必须做好持久化和主从或集群部署,否则Redis一挂,所有人同时掉登录态,这个故障比之前更严重。
5.3 配置改了不生效,到底reload还是restart
这个属于操作习惯问题,但每天都有新同学踩。Nginx的配置修改后,需要用nginx -t先检查语法,再执行nginx -s reload重载配置。reload过程是平滑的:master进程会启动新的worker进程,新请求走向新配置,旧worker进程处理完手头请求后退出。所以线上环境的Nginx配置变更,永远用reload,不要restart——restart会断开当前所有连接,正在传输的请求直接中断。
Tomcat的server.xml修改后,一般需要重启Tomcat才能生效,因为Connector的线程池参数在启动时初始化。但是很多应用代码更新不需要重启Tomcat:你把新的war包扔进webapps目录,Tomcat检测到变化会自动重新部署。这个自动部署的功能在生产环境建议关掉,原因一是不可控,二是热部署偶尔会引发内存泄漏。生产环境统一走停服部署或者优雅下线流程:先从Nginx的upstream中把节点注释掉,reload Nginx,等该节点的存量请求处理完,再停Tomcat,发布完启动后重新加回upstream。
5.4 压测发现所有请求都堆在一台Tomcat
如果配置了least_conn,但压测时发现流量并没有均匀分摊,先检查是不是session粘连导致。least_conn模式下,在不启用ip_hash的情况下,Nginx本身不会做会话粘连,请求应该分散。但如果应用里自己通过Cookie做了会话保持,或者Nginx配置里同时保留了ip_hash指令,就会出现“看起来配了负载均衡,实际上全在一台”的假象。
另一个隐蔽点:upstream里的max_fails和fail_timeout设置不当会导致节点被临时隔离。比如max_fails=2意味着2次失败后30秒内不再向该节点分发请求,如果某台Tomcat因为应用发布或网络抖动出现了一个瞬时错误,Nginx会把它摘掉,后续流量全部打到另一台。压测时只盯着Nginx日志看“为什么只有一台收到请求”,很容易忽略这一点。排查方法是查看Nginx的错误日志,看是否有upstream timed out或connect failed之类的记录。
5.5 大文件上传失败
配置了client_max_body_size 20m;后,上传超过20MB的文件仍失败。先确认Nginx层面的错误信息,常见的是返回413 Request Entity Too Large。如果Nginx配置确实生效还报413,可能是上传请求经过了CDN或其他代理层,那层代理对请求体大小也有限制。另外,Tomcat的maxSwallowSize和maxPostSize默认值也有限制,Tomcat 9默认maxPostSize是2MB,超过会拒绝。所以大文件上传要同时调Nginx、Tomcat、以及应用本身(Spring的spring.servlet.multipart.max-file-size和max-request-size)。三层中任何一层没改,都可能卡住。而且这三层报错形式各不相同,Nginx报413、Tomcat可能直接断开连接、Spring可能抛出MaxUploadSizeExceededException,排查时需要逐层确认请求到底卡在哪。
6. 性能调优进阶:让Nginx发挥到极限
6.1 连接数模型的几个关键参数
Nginx性能调优的核心首先在于连接数模型。worker_processes建议设置为auto,让Nginx自动匹配CPU核心数,每个核心对应一个worker进程。worker_connections默认值是1024,意思是每个worker进程能同时处理1024个连接,总连接数就是worker_processes乘以worker_connections。调这块的时候,先把所有进程能打开的文件句柄上限调上去:worker_rlimit_nofile 65535;,然后worker_connections才能对应调大。如果不调rlimit,句柄到顶后Nginx会报too many open files,连接数再大也没用。
keepalive_timeout决定客户端与Nginx之间的长连接保持时间。默认65秒,如果前端是浏览器访问,这个值可以保持;如果前面还有CDN或网关,建议缩短到10~20秒,减少无效连接的占用。tcp_nopush和tcp_nodelay这两个参数经常被一起讨论,前者优化大文件传输的包发送效率,后者确保小响应不被延迟合并发送。对动态接口来说,tcp_nodelay on是必要的,否则触发Nagle算法,小数据包可能会等待后面的数据一起发,引入几十毫秒延迟。
6.2 缓存:除了浏览器强缓存,还有Nginx本地缓存
静态资源的浏览器缓存解决了重复访问的问题,但第一次访问时,Nginx仍然要从磁盘读文件。如果流量足够大,可以启用proxy_cache做Nginx本地的内容缓存。这种方案主要应用于Nginx直接转发到上游的场景,比如动态页面或接口的响应做了短时间缓存。
配置思路如下:先定义缓存路径和内存缓存区,再定义缓存有效期,然后在location中开启proxy_cache。需要注意几点:一是缓存键默认包含请求URL和参数,动态页面缓存时慎用,避免拿到脏数据;二是缓存清理机制要配套,可以用第三方模块配合管理,或者使用短有效期让自然过期;三是缓存目录的磁盘容量要监控,写满会导致缓存失效甚至返回错误。
我在实际项目里的体会是,proxy_cache更多用在接口网关层做「读多写少」的查询缓存,比如商品详情、配置信息这类数据,缓存10-30秒对数据库的减压效果非常明显。但纯粹的动静分离场景,Nginx给静态资源做磁盘缓存的意义不大——文件系统的page cache已经很高效了,再加一层反而多一次磁盘读写。这个点要区分清楚,不要为了追求配置完整性硬加。
6.3 HTTPS接入的注意点
架构里如果接了HTTPS,Nginx的SSL配置有一些细节必须处理。首先证书链要配置完整,ssl_certificate和ssl_certificate_key之外,中间证书如果缺失,部分老设备或特定客户端会报证书信任错误。其次,SSL会话复用的参数ssl_session_cache建议开启,能明显减少TLS握手的开销,尤其在高并发短连接场景下。
启用HTTP/2也是性价比很高的优化:现在主流Nginx版本都支持listen 443 ssl http2;的写法(新版本需要单独用http2 on;指令),HTTP/2的多路复用特性对少并发、多资源的网页加载体验提升非常明显,尤其是同一页面有几十个静态资源文件的场景。但HTTP/2强制要求HTTPS,所以如果项目还在用明文HTTP,想享受这层优化必须先上TLS。
7. 架构层面的进一步思考:这套方案还能怎么扩展
7.1 Nginx也可以做API网关的部分工作
动静分离加负载均衡只是Nginx在Java Web架构里的基础职能。实际项目中,Nginx还能承担不少API网关的职责。比如用limit_req和limit_conn做接口限流,防止某个接口被刷;用allow/deny做IP黑白名单;用return 403拦截恶意请求;用proxy_next_upstream实现在某个节点返回500或超时时自动重试另一个节点。这些能力叠加起来,Nginx就从简单的流量分发器升级为应用入口的守护层。
把Nginx当成网关用的时候,要注意一个配比问题:网关层配置越复杂,单机性能损耗越大。如果需要在网关层做大量鉴权、转发规则、限流策略,建议评估是否需要引入OpenResty或者基于Nginx做过深度定制的网关产品。换句话说,能用Lua写业务逻辑的网关是另一个技术栈的决策,不是简单修配Nginx配置文件能替代的。对于大多数中小团队,Nginx原生的配置能力加上后端应用自身的防护,已经足够了。
7.2 容器化时代的部署方式
现在的项目很多是容器化部署,Nginx和Tomcat各自跑在容器里。这时候动静分离和负载均衡的配置原理没有变,但有几个新问题要考虑。Nginx容器里的upstream server地址不再固定在宿主机IP上,而是通过服务发现机制解析到后端容器的动态IP。比较常见的做法是让Nginx所在容器和后端Tomcat容器处于同一个Docker网络或Kubernetes集群内,用服务名或者内部DNS解析替代IP写死。
容器环境下还多了一个层级:如果外部流量先进Ingress或Gateway,再转发到Nginx,再到Tomcat,那Nginx这一层是不是有必要保留,需要根据团队规模判断。在Kubernetes场景里,Ingress Controller本身已经具备一定的负载均衡能力,可以替代传统Nginx的角色。但如果Pod内的请求路径复杂,静态资源量很大,保留Nginx做动静分离依然有意义。方案没有标准答案,核心是与团队现有的基础设施和运维能力匹配。
7.3 日志与监控闭环
架构搭好之后,日常运维的监控是容易忽略的部分。Nginx访问日志里藏着大量信号:请求量趋势、响应状态码分布、upstream响应时间、磁盘I/O节奏。建议把Nginx的access日志格式做一次整理,加入$upstream_response_time、$upstream_addr、$request_time,这样能直接统计Nginx到后端Tomcat的响应耗时。配合Prometheus加Grafana,用nginx-exporter采集基础指标,能看到连接数、读写字节数、请求状态码分布这些运行指标。
Tomcat的JVM监控同样重要:用JMX导出堆内存、GC次数、线程池活跃线程数到监控系统,配合Nginx的指标联合排查。我遇到过一次线上卡顿,从Tomcat线程堆栈看到大量线程阻塞在数据库连接获取上,但因为监控里只配了Nginx指标,差点被误导去优化Nginx配置。这种跨组件的排查经验说明,监控一定要有链路视角,不能只看单一组件。
8. 最后再分享一个低成本但收益明显的验证用例
我在做培训分享时经常安排一个小实验,成本很低,但把动静分离和负载均衡的核心知识全串起来了,适合刚学完理论的同学自己动手验证。在部署好的环境中,把Nginx里的静态资源location临时注释掉,让所有请求都转发给Tomcat,然后用ab对同一张图片做压测。对比配置开启和关闭两组数据,你会看到Tomcat的QPS差距在一到两个数量级。这个实验能让每个人直观理解“静态资源交给Nginx处理”不是一句空话,而是实实在在的数量级差距。
另一个实验是人为制造后端故障。把节点B的Tomcat直接停掉,不修改Nginx配置,持续灌请求,观察Nginx的日志和返回结果。如果配置了max_fails和fail_timeout,节点B会在几次失败后被自动摘除,所有请求自动转到节点A,用户侧感知不到异常;如果没有配置,部分请求会超时或报502。这个故障实验能验证Nginx健康检查机制是否可靠,也是面试时“如何实现高可用”这个问题最直接的实操回答。
我个人在实际操作中的体会是:Nginx和Tomcat的组合方案,难点向来不在配置语法,而在理解每一层为什么存在。把静态和动态的边界理清楚,把会话和IP的关系想明白,把超时和重试的语义搞透彻,后面再叠Redis、容器化、服务治理,都是水到渠成的事。不要一上来就抄一份大而全的配置——先跑通最小闭环,再逐步加参数填细节,出问题时能快速定位到层,这比任何“万能配置模板”都可靠。