接手过很多带tomcat的环境,几乎都能看到nginx站在前面顶着流量。我直接说结论:nginx和tomcat这对组合,是当前Java Web服务里最常见、最实用的一套骨架,nginx负责接住百万级连接、处理静态资源和转发请求,tomcat专注跑业务逻辑。这篇文章从架构原理讲到安装部署、参数调优、前后端分离落地、日志排障,新手照着做能少走弯路,老手看细节也有可参考的地方。
1. 先想明白:nginx和tomcat到底各自负责什么
1.1 nginx擅长的是“接客”
nginx本质是一个高性能HTTP和反向代理服务器,核心模型是事件驱动异步非阻塞。说人话就是:一个nginx worker进程能同时维持成千上万个连接,不会因为某个请求慢就卡住整条线程。静态文件比如html、css、js、图片的响应速度极快,因为它省去了动态解析环节,直接从磁盘或内存缓存里把文件吐给客户端。
nginx还能干很多“接客”的活:HTTPS证书卸载、gzip压缩、请求限流、IP封禁、负载均衡。这些都发生在业务代码之外,处理起来几乎不消耗应用服务器资源。
1.2 tomcat是“干活的”
tomcat是一个Servlet容器,也是标准Java Web应用的运行环境。Servlet、JSP、Spring Boot打出的war包,最终都是运行在tomcat这类容器里。它内部基于线程池处理请求,每个请求占用一个线程,触达业务代码后由你的Spring、Dubbo或者其他框架去完成真正的数据计算和逻辑返回。
tomcat的并发能力受JVM堆内存、线程池大小、数据库连接池多重限制,它的强项是业务处理,弱项恰恰是静态资源:每次静态请求都要占一个tomcat线程,流量一多,真正需要计算的业务线程反而被饿死。
1.3 为什么必须把nginx摆在tomcat前面
我画过很多次这两个角色的对比,核心差异可以用一张表说清楚:
| 维度 | nginx | tomcat |
|---|---|---|
| 静态资源 | 异步IO,性能强悍 | 线程池模式,浪费线程 |
| 并发模型 | 单进程可维持数万连接 | 受JVM和线程池限制 |
| HTTPS终结 | 配置简单,性能稳定 | 也能配,但维护成本高 |
| 负载均衡 | 原生upstream支持 | 需要额外组件 |
| 故障隔离 | 上游挂了可快速切换 | 单点崩溃直接影响业务 |
所以标准架构是:外部请求先进nginx,nginx能自己处理的静态资源直接返回,处理不了的动态请求转发给后端的tomcat集群。tomcat的线程不至于被静态文件拖垮,nginx的并发能力又能扛住公网流量。这一层设计解决的核心问题是资源隔离和分层解耦。
有人问单台tomcat直接暴露公网行不行,小流量没问题,但一旦并发上百,CPU先飙起来的就是tomcat的线程调度和静态文件IO。nginx挡在前面最大的价值就是让tomcat只处理该处理的事。
2. nginx 安装实战:编译安装并没有那么神秘
2.1 编译前先把依赖备齐
我推荐编译安装而不是直接用发行版自带的nginx包,原因是编译安装能自己控制目录、模块,后续平滑升级和排查问题都方便。版本我建议选1.24.x以上稳定版,老版本有几个已知缓冲区漏洞,比如CVE-2022-41742在低版本里就存在,生产环境尽量避开。
编译安装前需要准备的依赖:
- gcc、make:C编译工具链
- pcre-devel:rewrite模块正则依赖
- zlib-devel:gzip压缩模块依赖
- openssl-devel:HTTPS支撑
CentOS系一条命令装齐:
yum install -y gcc make pcre-devel zlib-devel openssl-devel如果机器是aarch64并且纯内网环境,没法直接拉在线包,就提前在有外网的同样架构机器上把依赖rpm下载好,打包拷进去用yum localinstall *.rpm装。注意架构必须一致,x86_64的rpm在aarch64上装不了。
2.2 configure、make、make install 全流程
源码解压后进入目录,先执行configure生成Makefile:
./configure \ --prefix=/usr/local/nginx \ --with-http_ssl_module \ --with-http_stub_status_module \ --with-http_gzip_static_module这几个参数逐个解释一下:
--prefix指定安装目录,统一管理,卸载也干净--with-http_ssl_module是HTTPS必须的,漏掉之后想补就要重新编译--with-http_stub_status_module提供/nginx_status监测页面,线上排查并发很有用--with-http_gzip_static_module支持预压缩静态资源
然后编译安装:
make -j4 && make install-j4表示并行编译,如果你的机器是4核就用-j4,8核可以-j8,能明显缩短编译时间。编译过程不要着急,看到最后生成/usr/local/nginx/sbin/nginx就算成了。
验证安装:
/usr/local/nginx/sbin/nginx -t /usr/local/nginx/sbin/nginxnginx -t只测试配置语法不会真正启动,新手务必养成先-t再启动的习惯。启动后浏览器访问机器IP,能看到Welcome to nginx页面就说明基础环境OK了。
2.3 基础配置里的高并发调参
打开/usr/local/nginx/conf/nginx.conf,核心配置我建议这样起步:
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 20m; gzip on; gzip_types text/plain text/css application/json application/javascript; }worker_processes auto会自动探测CPU核数并启动相同数量的worker进程,每个worker能维持worker_connections个连接,整体并发上限就是两者相乘。注意这个上限是理论值,还要考虑文件描述符限制,必要时在/etc/security/limits.conf里把nofile调大。
client_max_body_size 20m不说清楚很多人会踩:nginx默认限制客户端上传体积1M,不加这个配置,上传文件超过1M直接返回413。调成多少取决于业务,一般20M够用,视频类业务再往上加。
gzip on是白给的性能优化,开启后nginx在返回静态资源时自动压缩,前端传输体积能省一半以上。但我建议同时开启gzip_static配合预压缩文件,能省掉请求时的实时压缩CPU开销。
2.4 平滑升级的正确姿势
我在升级nginx时踩过坑,直接kill老进程再启动新的,结果线上瞬间502。nginx的平滑升级是利用旧进程处理完存量连接后自动退出的机制:
# 备份老二进制 mv /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old # 拷贝新编译好的二进制 cp /path/to/new/nginx /usr/local/nginx/sbin/nginx # 发送USR2信号,启动新worker进程 kill -USR2 `cat /usr/local/nginx/logs/nginx.pid`新进程起来后会接管新连接,旧进程把存量请求处理完再退出。整个升级过程理论上不中断服务,升级完务必观察几分钟接口返回情况,有问题再回滚。
3. tomcat 部署与运行优化:配置细节决定服务质量
3.1 下载、安装、目录结构一次说清
tomcat下载认准Apache官网,但国内从官网拉文件经常很慢,我一般用国内镜像,速度快多了。版本选择看项目需要,Spring Boot内嵌tomcat无需额外安装,传统war包部署用tomcat 9或10都行。
解压到/opt/tomcat后,目录结构必须烂熟于心:
- bin:startup.sh、shutdown.sh、catalina.sh都在这里
- conf:server.xml、web.xml、logging.properties等核心配置
- lib:运行所需的jar包
- logs:catalina.out、localhost.log、access.log等日志
- webapps:war包解压目录
- work:JSP编译临时目录
启动tomcat直接执行bin/startup.sh,关闭用bin/shutdown.sh。千万别用kill -9直接杀tomcat进程,可能会导致文件缓存和数据不一致。
3.2 乱码问题的根源与根治方案
tomcat乱码是出现频率最高的怪现象,归纳下来其实是两个层面的问题:
第一层是控制台和日志乱码,根因是JVM默认编码和系统编码不一致。需要改两个地方:
在conf/logging.properties里确认:
java.util.logging.ConsoleHandler.encoding = UTF-8在bin/catalina.sh的JAVA_OPTS里加:
-Dfile.encoding=UTF-8第二层是请求参数乱码,尤其是GET请求的查询参数,在conf/server.xml中给Connector节点加:
URIEncoding="UTF-8"三层都改一遍,乱码基本能根治。需要说明的是,如果你用的恰好是tomcat 8.0.5之前的版本,默认编码可能不是UTF-8,一定要显式配置。
3.3 JVM参数这样调才不迷路
打开bin/catalina.sh,找到JAVA_OPTS区域,我生产环境常用的模板:
CATALINA_OPTS="-Xms2g -Xmx2g -XX:MaxMetaspaceSize=512m -Djava.awt.headless=true -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/tomcat/logs"几个参数的设定逻辑:
-Xms和-Xmx建议设成一样,防止JVM运行期频繁扩容堆内存导致停顿。大小取决于机器物理内存和预期并发,我习惯堆内存不超过物理内存60%,剩余留给Metaspace、线程栈和操作系统page cache。比如8G机器给tomcat 4G堆,留4G给系统。
-XX:MaxMetaspaceSize控制类元数据上限,Spring Boot时代的类加载很多,不给上限容易碰到“Metaspace OutOfMemory”。
-Djava.awt.headless=true是服务器环境必加项,防止某些图表生成、图片处理业务在无图形界面环境报错。
-XX:+HeapDumpOnOutOfMemoryError是保命配置,OOM时自动生成dump文件,排障没有这个dump会非常被动。
调好后重启tomcat,用jmap -heap pid确认生效。
3.4 systemd 管理 tomcat 开机自启
linux上设置tomcat自启动,最省事的做法是用systemd。我见过有人在/etc/rc.local里塞startup.sh,也能跑,但服务异常退出不会自动拉起,rc.local本身也不被新版系统默认加载了。
创建/etc/systemd/system/tomcat.service:
[Unit] Description=Apache Tomcat Web Application Container After=network.target [Service] Type=simple Environment="JAVA_HOME=/usr/local/java" ExecStart=/opt/tomcat/bin/catalina.sh run ExecStop=/opt/tomcat/bin/shutdown.sh Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target注意一个关键细节:ExecStart直接写startup.sh会以子进程方式启动tomcat,systemd会丢失对主进程的跟踪,服务挂了没法自动拉起。必须用catalina.sh run前台模式,让tomcat进程成为systemd直接管理的进程。
启动并设置开机自启:
systemctl daemon-reload systemctl enable tomcat systemctl start tomcat看到systemctl status tomcat显示active (running)就可以放心了。
4. 前后端分离项目部署:nginx 配置才是重头戏
4.1 静态资源直接让nginx吐给用户
前后端分离项目,前端构建产物通常是一整个dist目录,index.html和按路由拆分的js、css、图片都在里面。nginx配置只需要:
server { listen 80; server_name app.example.com; root /data/www/dist; index index.html; location / { try_files $uri $uri/ /index.html; } }这段配置的重点是try_files。前端用vue-router或react-router的history模式时,访问比如/user/1这个路径,后端没有任何真实文件对应,不加try_files直接404。加上try_files $uri $uri/ /index.html,nginx会把不存在的路径统一引导到index.html,由前端路由自己接管页面渲染。
另外给静态资源加缓存策略,性能提升非常明显:
location ~* \.(js|css|png|jpg|gif|svg)$ { expires 30d; add_header Cache-Control "public, immutable"; }4.2 API反向代理的路径陷阱
前端和后端分离后,动态接口需要通过nginx转发到tomcat。基础配置:
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; proxy_read_timeout 60s; }proxy_set_header这几行一定要保留完整,否则后端拿不到真实的客户端IP,统一显示成nginx的内网地址,日志分析和限流都会失真。X-Forwarded-Proto用于后端判断原始请求是HTTP还是HTTPS。
最容易踩坑的是proxy_pass末尾的斜杠:proxy_pass http://127.0.0.1:8080;不带斜杠时,转发到后端的是完整URI/api/user/list;proxy_pass http://127.0.0.1:8080/;带斜杠时,/api/这个前缀会被吞掉,后端收到的路径变成/user/list。具体用哪种取决于后端接口有没有加/api前缀,配置完一定要用curl实测一遍,这是个只看配置很难察觉的细节。
4.3 upstream 实现多实例负载均衡
后端做集群时,nginx的upstream模块是最直接的负载均衡方案:
upstream tomcat_pool { server 127.0.0.1:8081 weight=1; server 127.0.0.1:8082 weight=1; keepalive 32; } server { location /api/ { proxy_pass http://tomcat_pool; } }默认轮询策略,weight控制流量比例。生产环境我建议加keepalive参数,开启nginx到tomcat的长连接,减少频繁TCP握手开销。
如果业务依赖session共享,可以先在upstream里用hash $request_uri保证同一个URL固定打到同一台tomcat,但我强烈建议把session迁到redis,让tomcat随便扩缩容都无状态,这才是后端集群的正确打开方式。
4.4 HTTPS证书配置与常见证书文件来源
现在线上基本全面HTTPS,证书在godaddy这类服务商买的证书,下载的时候选nginx格式,拿到的是一串.pem/.crt证书文件和.key私钥文件。nginx配置:
server { listen 443 ssl; server_name app.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.key; ssl_protocols TLSv1.2 TLSv1.3; }配置完别忘了把80端口跳转到443:
server { listen 80; server_name app.example.com; return 301 https://$host$request_uri; }证书到期是这个环节最常见的故障源,我的做法是写一个定时任务每个月检查一次证书剩余天数,提前提醒续期。很多公司事故就是证书静默过期,用户访问直接安全告警。
5. 日志分析与高频问题排查实录
5.1 从日志链路快速缩小问题范围
一套nginx+tomcat环境,日志分两段看:nginx的access.log和error.log对应入口层,tomcat的catalina.out和应用日志对应业务层。我排查问题的固定顺序是:
先看nginx error.log,确认有没有上游连接失败;再看tomcat catalina.out,确认应用是否正常启动和报错;最后看应用业务日志,定位具体代码异常。
看access.log状态码分布可以用一条awk统计:
awk '{print $9}' /data/www/logs/access.log | sort | uniq -c | sort -rn结果里大量502、503、504基本锁定是网关层问题,大概率是tomcat挂了、连接数被打满或响应超时。大量499是客户端提前断开,用户等不及刷新页面。
5.2 高频报错速查表
我把实际运维中遇到最多的几类问题整理成表,基本能覆盖日常80%的排障需求:
| 现象 | 定位方向 | 常见解法 |
|---|---|---|
| 502 Bad Gateway | tomcat进程崩了或端口不通 | 检查tomcat状态和catalina.out |
| 504 Gateway Timeout | tomcat响应超过nginx等待时间 | 调大proxy_read_timeout |
| 404 | nginx路径和真实文件对不上 | 检查root、alias、location匹配 |
| 413 Request Entity Too Large | 上传体积超nginx限制 | 调大client_max_body_size |
| 请求中文乱码 | 编码链路不一致 | 按第3章三步统一UTF-8 |
| 定期503 | 后端连接池被打满 | jstack看阻塞线程和连接池配置 |
5.3 一起诡异502的完整复盘
说一个我印象深刻的线上排查过程。某系统白天正常,每到晚高峰就偶发502,nginx error.log里大量connect() failed (111: Connection refused) while connecting to upstream。
我的第一反应是tomcat挂了,但systemctl status tomcat显示active,端口也是LISTEN状态。继续翻catalina.out,发现大量等待数据库连接的异常。再往后看,tomcat线程全部阻塞在获取连接上,数据库连接池被打满了。
根源不在tomcat本身,而是数据库连接池maxActive设太小,晚高峰业务请求一上来,连接耗尽,线程全卡住,最终tomcat无法接收新连接,nginx只能向上游报502。最后方案是调大连接池、给慢SQL加索引、加了一层缓存,问题彻底消失。
这次复盘给我一个很重要的经验:nginx报502时不要只查nginx和tomcat,tomcat默默运行不代表它没被下游拖垮,要顺着调用链往下游继续看。
5.4 开发环境配置tomcat的两个高频问题
很多人在开发环境也被tomcat折腾过,最常见两个问题。
第一个是IDEA或vscode里配置tomcat后启动报源服务器未能找到目标资源的表示或者是不愿公开一个已经存在的,这个看着像后端错误其实多半是访问路径问题:URL路径和后端Servlet的映射对不上,要么项目没部署成功,要么访问的context-path不对。先查看IDEA的deployment配置确认Application context,再核对浏览器访问的URL。
第二个是tomcat闪退。Linux下执行startup.sh闪退基本是环境变量问题,最常见是JAVA_HOME没设置,启动脚本直接退出。用bin/catalina.sh run前台模式跑一遍,错误信息会直接打在屏幕上,比看日志更快。
几点维护心得
最后分享几个长期运维下来的经验。
nginx配置改动后一定先nginx -t再reload,tomcat同样先检查再重启,这条几乎能拦住一半以上的低级事故。把nginx的stub_status监控页面向监控系统开放,采集活跃连接数和accepts/handled计数,流量冲上来之前这些指标会先有动静。日志保留周期至少30天,出问题时要能往回翻历史。
nginx和tomcat这套架构短期内不会过时,把两者的安装、配置、调优、排障吃透,后面不管是容器化部署还是接入服务网格,你都会发现很多底层思路是通的。