
接手过很多带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前面我画过很多次这两个角色的对比核心差异可以用一张表说清楚维度nginxtomcat静态资源异步IO性能强悍线程池模式浪费线程并发模型单进程可维持数万连接受JVM和线程池限制HTTPS终结配置简单性能稳定也能配但维护成本高负载均衡原生upstream支持需要额外组件故障隔离上游挂了可快速切换单点崩溃直接影响业务所以标准架构是外部请求先进nginxnginx能自己处理的静态资源直接返回处理不了的动态请求转发给后端的tomcat集群。tomcat的线程不至于被静态文件拖垮nginx的并发能力又能扛住公网流量。这一层设计解决的核心问题是资源隔离和分层解耦。有人问单台tomcat直接暴露公网行不行小流量没问题但一旦并发上百CPU先飙起来的就是tomcat的线程调度和静态文件IO。nginx挡在前面最大的价值就是让tomcat只处理该处理的事。2. nginx 安装实战编译安装并没有那么神秘2.1 编译前先把依赖备齐我推荐编译安装而不是直接用发行版自带的nginx包原因是编译安装能自己控制目录、模块后续平滑升级和排查问题都方便。版本我建议选1.24.x以上稳定版老版本有几个已知缓冲区漏洞比如CVE-2022-41742在低版本里就存在生产环境尽量避开。编译安装前需要准备的依赖gcc、makeC编译工具链pcre-develrewrite模块正则依赖zlib-develgzip压缩模块依赖openssl-develHTTPS支撑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核就用-j48核可以-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后目录结构必须烂熟于心binstartup.sh、shutdown.sh、catalina.sh都在这里confserver.xml、web.xml、logging.properties等核心配置lib运行所需的jar包logscatalina.out、localhost.log、access.log等日志webappswar包解压目录workJSP编译临时目录启动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.encodingUTF-8第二层是请求参数乱码尤其是GET请求的查询参数在conf/server.xml中给Connector节点加URIEncodingUTF-8三层都改一遍乱码基本能根治。需要说明的是如果你用的恰好是tomcat 8.0.5之前的版本默认编码可能不是UTF-8一定要显式配置。3.3 JVM参数这样调才不迷路打开bin/catalina.sh找到JAVA_OPTS区域我生产环境常用的模板CATALINA_OPTS-Xms2g -Xmx2g -XX:MaxMetaspaceSize512m -Djava.awt.headlesstrue -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.headlesstrue是服务器环境必加项防止某些图表生成、图片处理业务在无图形界面环境报错。-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] DescriptionApache Tomcat Web Application Container Afternetwork.target [Service] Typesimple EnvironmentJAVA_HOME/usr/local/java ExecStart/opt/tomcat/bin/catalina.sh run ExecStop/opt/tomcat/bin/shutdown.sh Restarton-failure RestartSec10 [Install] WantedBymulti-user.target注意一个关键细节ExecStart直接写startup.sh会以子进程方式启动tomcatsystemd会丢失对主进程的跟踪服务挂了没法自动拉起。必须用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.htmlnginx会把不存在的路径统一引导到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/listproxy_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 weight1; server 127.0.0.1:8082 weight1; 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端口跳转到443server { listen 80; server_name app.example.com; return 301 https://$host$request_uri; }证书到期是这个环节最常见的故障源我的做法是写一个定时任务每个月检查一次证书剩余天数提前提醒续期。很多公司事故就是证书静默过期用户访问直接安全告警。5. 日志分析与高频问题排查实录5.1 从日志链路快速缩小问题范围一套nginxtomcat环境日志分两段看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 Gatewaytomcat进程崩了或端口不通检查tomcat状态和catalina.out504 Gateway Timeouttomcat响应超过nginx等待时间调大proxy_read_timeout404nginx路径和真实文件对不上检查root、alias、location匹配413 Request Entity Too Large上传体积超nginx限制调大client_max_body_size请求中文乱码编码链路不一致按第3章三步统一UTF-8定期503后端连接池被打满jstack看阻塞线程和连接池配置5.3 一起诡异502的完整复盘说一个我印象深刻的线上排查过程。某系统白天正常每到晚高峰就偶发502nginx 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和tomcattomcat默默运行不代表它没被下游拖垮要顺着调用链往下游继续看。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再reloadtomcat同样先检查再重启这条几乎能拦住一半以上的低级事故。把nginx的stub_status监控页面向监控系统开放采集活跃连接数和accepts/handled计数流量冲上来之前这些指标会先有动静。日志保留周期至少30天出问题时要能往回翻历史。nginx和tomcat这套架构短期内不会过时把两者的安装、配置、调优、排障吃透后面不管是容器化部署还是接入服务网格你都会发现很多底层思路是通的。