开头
在云上做Java应用部署,Tomcat依旧是绕不开的那个“工件”。如果把Spring Boot的内嵌容器也算进去,Tomcat内核在Java服务端的占有率比我见过的大多数中间件都要夸张。但我发现一个现象:不少人把Tomcat部署上去之后,基本保持默认配置,直接怼上生产环境,直到某天大促流量进来,连接池飙红、请求排队、CPU爆表,才开始到处找“为什么我的Tomcat这么弱”。
这篇文章给你说的,是我在HoRain云这类云环境下做Java中间件交付时积累的一套Tomcat调优与集群实战打法。内容会比较长,但都属于能直接落地的硬货,覆盖从单机调优到Nginx+多实例集群、Session共享、故障转移、常见问题排障这整条链路。适合刚接手Tomcat生产环境的人,也适合做了几年开发但没系统性搞过中间件性能调优的同学。
1. 从单机到集群:先摸清Tomcat的底细
1.1 为什么默认配置撑不住生产流量
Tomcat默认配置的目标是“开箱即用”,不是“高压抗揍”。它默认的线程池、连接器参数、JVM参数都是偏保守的,目的是兼容绝大多数低并发场景。我见过不少团队直接把默认的server.xml用到上线,然后压力一上来就报Connection refused或者504。
核心问题在哪里?Tomcat的默认配置里,maxThreads是200,acceptCount是100,maxConnections是8192(NIO下默认)。看着数字不小,但实际生产环境里,请求往往不是匀速到达的,而是突刺型的。一旦瞬时并发超过线程池处理能力,新的请求就会在acceptCount队列里排队,队列满了直接拒绝连接。更糟糕的是,JVM堆内存默认只给了物理内存的1/4,GC策略也是老的Parallel Scavenger,遇到大对象或者高吞吐场景,Full GC一来,整个应用就卡壳。
所以调优的第一步不是改参数,而是理解Tomcat处理一个HTTP请求的完整路径:客户端连接进来,Acceptor接收Socket,交给Executor线程池处理,解析请求、经过Valve管道、Filter、Servlet,最后返回响应。这条路径上任何一个环节成为瓶颈,表现都是请求变慢,但根因可能完全不同,这也是为什么要分层去做调优。
1.2 部署前的基线准备:JDK选择与安装细节
很多人在Tomcat调优时忽略了一个前置条件:JDK版本。Tomcat 9对Java 8支持很好,但如果服务跑在Java 8上,G1收集器其实还不够成熟,推荐直接用Java 11或者Java 17跑Tomcat 9/10。我在云主机上通常选择OpenJDK 11,不是因为版本越新越好,而是因为LTS版本在云环境里的兼容性验证最充分。
安装JDK时有一个细节很多人踩坑:云镜像自带的OpenJDK可能是不完整的headless版本,缺少jps、jstat、jmap这些调优工具。检查方法很简单:
bash复制java -version
jps -l
如果jps命令找不到,说明装的是headless JRE,需要补装完整JDK。在CentOS/RHEL系云主机上,我一般用yum install java-11-openjdk-devel一步到位。装完之后设置好JAVA_HOME和PATH,然后把Tomcat解压到固定目录,比如/opt/tomcat,建议建立软链接,后续升级版本只换软链接指向,不用改一堆脚本里的路径。
1.3 目录结构与关键配置文件的“责任清单”
Tomcat的目录结构虽然简单,但每个目录干什么是必须清楚的。bin目录放启动关闭脚本,conf目录是调优主战场,lib目录放Tomcat自身依赖,logs目录排障要看,webapps是部署目录,work是JSP编译缓存目录。
千万不要把业务JAR包丢进lib目录,不要把多个应用的不同版本JAR混在一起。我在云上排查过太多“类冲突导致诡异报错”的问题,最后发现全是依赖管理混乱惹的祸。
配置文件的优先级和职责也要理清楚:
server.xml:核心引擎配置,连接器、Host、Engine、Cluster都在这里,90%的调优动作都围绕它。web.xml:全局Servlet配置,默认Servlet、Session超时、MIME映射。catalina.properties:类加载器扫描路径控制,有安全加固作用。catalina.sh:JVM参数注入入口,JAVA_OPTS和CATALINA_OPTS的区别要搞清楚,CATALINA_OPTS只在Tomcat启动时生效,更适合放JVM调优参数。
关于CATALINA_OPTS和JAVA_OPTS的区别,有人可能觉得随便放哪个都一样,实际上区别很大。JAVA_OPTS会被Tomcat内部的其他Java进程继承,比如通过Tomcat启动的某些辅助工具,而CATALINA_OPTS只作用于Tomcat主进程。所以像-Xmx、-Xms、-XX:+UseG1GC这类参数,规范做法是放进CATALINA_OPTS,避免影响子进程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Tomcat连接器调优:瓶颈往往堵在入口
2.1 Connector线程模型:BIO/NIO/NIO2/APR怎么选
Tomcat从8.5版本开始移除了BIO模型,原因很简单:一个线程对应一个连接的方式,在高并发场景下线程数被连接数拖死,线程上下文切换消耗大量CPU。现在主流的NIO模型下,Tomcat利用Java NIO的Selector机制,用少量线程处理大量连接,连接和请求解耦,这是生产环境默认的最佳选择。
很多文章推荐APR(Native)模式,理由是性能比NIO高,但我必须说一下实战感受:APR依赖操作系统原生库(tomcat-native),在云主机上编译安装容易出问题,而且Tomcat 9之后APR的性能优势在新版NIO面前并不明显。除非你明确知道自己在做什么,否则不要为了“性能提升”去折腾APR。NIO2和NIO的区别在于NIO2使用异步I/O,性能提升有限,但排查问题时的行为模型更复杂,我在生产环境一律用默认的NIO。
2.2 核心参数逐个拆解:maxThreads/acceptCount/maxConnections
这三个参数是最容易混淆的,但理解了整个请求排队模型后就不难了。
maxThreads是Tomcat内部线程池最多能创建的请求处理线程数。这个值设太小,高并发时请求全部排队;设太大,CPU全部消耗在线程切换上。我一般建议的基准算法是:maxThreads = CPU核数 * 200,然后根据业务耗时调整。如果是纯IO密集型业务(比如频繁查数据库、调外部接口),可以适当放大学生容量;如果是CPU密集型业务(比如加解密、逻辑计算),线程数反而要压低,避免大量线程争抢CPU。
acceptCount是操作系统底层accept队列的长度。当maxThreads用尽后,新请求会进入这个队列等待。我习惯设为maxThreads的1/2左右,太小会直接拒绝连接,太大则让请求等待时间过长,用户端表现就是一直转圈。
maxConnections是Tomcat能同时保持的连接数。对于NIO模式,默认8192一般够用。但要注意,如果请求是短连接,maxConnections消耗很快,因为TIME_WAIT状态的连接还没释放,新连接又在不断进来。这时不能一味调大maxConnections,而是要看keepAliveTimeout和连接回收机制。
2.3 压缩、超时、keepalive与静态资源缓存
很多人调优时只盯着线程池,忽略了连接的超时和压缩配置。这两项配置不当,用户体验和带宽成本都会出问题。
connectionTimeout是指建立连接后,如果Tomcat在指定时间内没有收到请求数据,就会断开连接。默认20秒在公网环境下偏长,云主机前面一般还有SLB或者Nginx,内网链路延迟很低,我通常设成10000毫秒即10秒,既不会误杀正常慢连接,又能快速释放被占用的连接资源。
keepAliveTimeout是HTTP长连接的存活时间,默认和connectionTimeout一致。如果你的应用是Web页面,建议设置成15秒左右,太短会导致浏览器复用连接失败、频繁重新建连,太长则会让连接长时间占用线程。
compression配置一定要开启,尤其对于带大量文本响应(JSON、HTML、JS)的应用,压缩比通常能到60%以上。开启方式:
xml复制<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="10000"
redirectPort="8443"
compression="on"
compressionMinSize="2048"
compressableMimeType="text/html,text/xml,text/plain,text/css,application/json,application/javascript"/>
注意compressionMinSize设为2048字节,小于这个阈值的响应压缩无意义,白白消耗CPU。还有一个坑:如果你在Nginx那一层已经开启了gzip,Tomcat这一层就不需要重复压缩,建议让离用户最近的代理层做压缩。
静态资源处理上,DefaultServlet有一个缓存参数,线上环境建议调整一下:
xml复制<init-param>
<param-name>cacheMaxSize</param-name>
<param-value>10240</param-value>
</init-param>
默认缓存只有10MB,对于图片、CSS、JS较多的Web应用,缓存命中率很低。调大到10240KB(10MB)会显著提升静态资源访问速度。当然最彻底的做法是静态资源全部走CDN或者对象存储,Tomcat只处理动态请求。
2.4 实战配置示例
这段配置是我在云主机上用的标准模板,按2核4G实例的标准设计的:
xml复制<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="400"
minSpareThreads="50"
maxIdleTime="60000"
prestartminSpareThreads="true"/>
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
executor="tomcatThreadPool"
maxConnections="2000"
acceptCount="200"
connectionTimeout="10000"
keepAliveTimeout="15000"
maxKeepAliveRequests="1000"
compression="on"
compressionMinSize="2048"
compressableMimeType="text/html,text/xml,text/plain,text/css,application/json,application/javascript"
URIEncoding="UTF-8"/>
解释几个容易被忽略的点。第一,显式指定executor引用线程池,这样线程池是独立配置的,而不是用Connector内部的默认线程池,优点是minSpareThreads预热线程,避免突发流量到达时线程还在创建过程中。第二,maxKeepAliveRequests设为1000,防止某些客户端一直占用连接不放。第三,URIEncoding="UTF-8"必须显式声明,否则带中文参数的GET请求大概率乱码。
3. JVM调优:让Tomcat真正吃满这台机器
3.1 堆内存与元空间:为什么-Xms=-Xmx
JVM调优是Tomcat性能的深水区,也是最容易看网上一堆参数抄来抄去的地方。我的主张是先守住两条铁律,再谈具体参数。
铁律一:-Xms和-Xmx必须相等。不要问为什么,原因很简单:避免堆内存动态扩容缩容带来的性能抖动。JVM在堆扩容时要向操作系统申请新内存,在缩容时要回收内存,这些操作都会触发STW。云主机的内存通常固定分配,直接把堆设到目标值更可控。
铁律二:堆内存大小不要超过物理内存的50%~70%。很多人觉得服务器内存那么大,给JVM分配80%、90%怎么了?Java进程本身除了堆之外,还有元空间、线程栈、直接内存、JIT编译代码缓存。如果堆占满,GC的剩余空间不够,Full GC频率会急剧上升。我在2核4G的云主机上,通常给Tomcat分配2G堆;在4核8G的主机上,分配4G堆,剩余的留给系统和其他组件。
元空间(Metaspace)默认是无限的,只受系统内存限制,但为了防止类加载器泄漏导致内存失控,生产环境建议设上限:
bash复制-XX:MaxMetaspaceSize=512m
3.2 垃圾回收器选择:G1还是CMS
Java 8时代CMS还是主流,但从Java 9开始,G1逐渐成为默认收集器。我的选择很简单:Java 11以上的环境统一使用G1,参数如下:
bash复制-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=2
MaxGCPauseMillis是G1的目标停顿时间,200毫秒是平衡默认值,太激进会导致GC过于频繁,反而降低吞吐量。ParallelGCThreads建议和CPU核数一致或略低,ConcGCThreads一般设为ParallelGCThreads的1/4到1/3。
生产环境建议开启GC日志,排障时没有GC日志就像闭着眼开车。
bash复制-Xloggc:/opt/tomcat/logs/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
Java 11可以用新格式:
bash复制-Xlog:gc*:/opt/tomcat/logs/gc.log:time,uptime,level,tags
我强烈建议保留GC日志,哪怕平时不看。出了问题,先看GC日志,再决定是不是要调内存参数,这能省掉大量排障时间。
3.3 线程栈、直接内存与系统参数
线程栈大小默认是1MB,对于Tomcat这种线程数较多的服务,线程栈总量不容小觑。如果maxThreads设置为500,光是线程栈就会占500MB虚拟内存。线程栈调到512KB一般够用,能省下不少内存。
bash复制-Xss512k
直接内存主要用于NIO相关操作,Tomcat的NIO模式会用到DirectByteBuffer。建议显式设置:
bash复制-XX:MaxDirectMemorySize=512m
还需要加一个很多教程不会提的系统参数:-Djava.awt.headless=true。虽然无害,但能避免某些服务器环境下图形相关初始化的异常。
完整的CATALINA_OPTS参考:
bash复制CATALINA_OPTS="-server -Xms2048m -Xmx2048m -XX:MaxMetaspaceSize=512m -Xss512k -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=4 -XX:ConcGCThreads=2 -XX:MaxDirectMemorySize=512m -Djava.awt.headless=true -Dfile.encoding=UTF-8"
3.4 调优工具怎么用:jps、jstat、jmap、jstack、arthas
工具那么多,别贪多,先把老三样用熟。jstat是最常用的GC监控工具:
bash复制jstat -gcutil <pid> 1000 10
每秒打印一次GC情况,一共10次。重点看FGC(Full GC次数)和FGCT(Full GC累计耗时),如果Full GC频繁且耗时超过几百毫秒,说明堆内存配置或者代码的对象分配有问题。
jmap用于查看堆内存使用和导出堆转储:
bash复制jmap -heap <pid>
jmap -dump:live,format=b,file=/opt/tomcat/logs/heap.hprof <pid>
注意,线上环境执行jmap -dump时要小心,会STW,最好在低峰期操作。
jstack用于排查线程问题:
bash复制jstack <pid> > thread_dump.txt
线程Dump文件里最值得关注的是状态为BLOCKED和WAITING的线程,以及它们在等哪个锁。
如果线上排查更方便,Arthas是更好的选择。它能在不重启进程的情况下动态观察类加载情况、方法调用耗时、线程栈,甚至可以反编译线上类查看逻辑。trace命令跟踪慢方法非常实用:
bash复制trace com.example.service.OrderService createOrder
3.5 一次Full GC频繁的真实案例
有一次在云上排查一个Tomcat服务,现象是每过几分钟接口响应时间就飙到好几秒,监控面板上CPU却不高。用jstat -gcutil观察,发现FGC区域每3分钟增加一次,每次耗时1.2秒左右。
再看堆内存,老年代从500MB一直涨到接近1.5GB之后触发Full GC,GC后回落到300MB。典型的“对象大起大落”模式。继续深挖,jmap -dump之后用MAT分析,发现有一个静态Map集合在不断往里添加数据,且数据量与用户行为强相关,代码里只写了put没写remove。这其实就是内存泄漏的早期表现。
这个案例说明:JVM调优不只是改参数,还要配合工具去监控代码层面的异常行为。参数调优是“治标”,发现并修复代码问题才是“治本”。
4. Tomcat集群实战:Nginx负载均衡 + 多实例部署
4.1 整体拓扑设计
集群的意义不用多说,单机故障不宕机、流量能横向扩展、发布时可以滚动更新不中断服务。我在云环境中最常用的拓扑是:
text复制客户端 -> SLB/云负载均衡 -> Nginx(可选) -> Tomcat节点1/节点2/节点3
| | |
Session存储(Redis/Memcached)
|
数据库/缓存
云平台自带的负载均衡(SLB)可以承担第一层流量分发,后面如果还需要做域名规则匹配、路径转发、灰度发布,再加一层Nginx。在规模不是特别大的场景下,直接用Nginx做唯一的入口也是可以的,省去一层网络转发。
4.2 Nginx负载均衡配置与upstream策略
Nginx安装不赘述,直接给一份可用配置。假设有两个Tomcat节点,分别跑在192.168.1.11:8080和192.168.1.12:8080:
nginx复制upstream tomcat_cluster {
least_conn;
server 192.168.1.11:8080 weight=1 max_fails=3 fail_timeout=30s;
server 192.168.1.12:8080 weight=1 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
listen 80;
server_name yourdomain.com;
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 30s;
proxy_send_timeout 30s;
}
}
least_conn算法适合Tomcat这类每个连接处理时间不固定的应用,比默认的轮询更合理。max_fails=3 fail_timeout=30s表示30秒内失败3次就把节点从可用列表中摘除,这个是故障转移的第一道防线。
keepalive 32是Nginx到后端Tomcat的长连接缓存数,这一项很多人不配,导致Nginx每次转发请求都要和Tomcat新建连接,性能损耗很大。开启后,Nginx会复用和Tomcat之间的TCP连接,吞吐量提升明显。
还有一个容易被忽略的点:必须设置Host头转发,否则Tomcat里的request.getServerName()获取不到真实域名,重定向逻辑会出问题。
4.3 Session共享的两条路线:Redis方案与Memcached方案
集群部署后第一个头疼的问题就是Session。用户登录Tomcat节点1后,下一次请求被Nginx转发到节点2,如果Session不共享,用户就被迫重新登录。
解决思路有两条路线。第一条是让Nginx做Session粘滞(sticky session),把同一个用户的请求始终转发到同一个节点,但这一方案在节点重启、扩容时会失效,用户还是会掉线。
第二条是Session集中存储,把Session数据放到Redis或Memcached里,所有Tomcat节点都读写同一个Session存储,这是生产环境更推荐的方案。我在云上部署完全分布式应用时更倾向Redis方案,因为还能顺便做缓存、消息队列等,一套Redis集群多用。
具体实现上,我在Tomcat 9时代使用memcached-session-manager(后面简称MSM)+ Redis的架构比较多,因为它在Tomcat 9上直接可用,配置相对简单。需要下载三个JAR包放入Tomcat的lib目录:
- memcached-session-manager
- memcached-session-manager-tc9
- spymemcached
然后在context.xml里配置Session管理器:
xml复制<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:127.0.0.1:11211"
sticky="false"
sessionBackupAsync="false"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"/>
关键在于sticky="false",表示不依赖Nginx粘滞,Session完全存在Redis里。transcoderFactoryClass使用Kryo序列化,比Java原生序列化快得多,而且产生的数据体积小。
如果你的业务使用了Spring Session,那么更推荐的路线是直接用Spring Session Redis替换Tomcat的Session管理器,配置更简洁,和Spring生态结合也更紧密。区别在于:Spring Session是在应用层解决Session共享,不依赖Tomcat容器配置;而MSM是在容器层解决,对应用透明。
4.4 云环境下的注意事项:安全组、多节点网络、镜像差异
云上部署集群和物理机房不一样,有几个坑我说一下。
第一个是安全组。云平台默认的安全组策略只允许部分端口开放,Tomcat节点之间通信需要的端口(如Redis的6379、Memcached的11211、Session同步端口)都要在安全组里显式放通。我踩过一次坑:两个Tomcat节点的Session怎么同步都失败,查了半天发现是安全组只放通了80端口,Redis的6379端口在内网被挡了。
第二个是镜像差异。多节点集群如果镜像创建时间不同,节点上的JDK版本、Tomcat版本、配置文件可能会有隐性差异。上线之前一定要做配置比对,用diff命令把所有节点的配置文件统一核对一遍:
bash复制diff /opt/tomcat/conf/server.xml 192.168.1.12:/opt/tomcat/conf/server.xml
第三个是时区问题。云主机默认时区可能是UTC,会导致日志时间和业务时间对不上,排障时非常痛苦。统一设置成Asia/Shanghai:
bash复制timedatectl set-timezone Asia/Shanghai
4.5 部署验证与滚动发布
集群搭完之后,验证不是把服务跑起来那么简单。我把验证分成三层。
第一层是单节点验证:直接访问每个Tomcat节点的8080端口,确认应用正常启动、页面能访问、日志没有异常报错。
第二层是负载均衡验证:通过Nginx访问服务,连续刷新页面,在Tomcat的localhost_access_log里观察请求是否被轮流分发到不同节点。
第三层是高可用验证:手动sleep掉一个Tomcat节点,观察Nginx是否自动将请求全部转发到存活节点。
bash复制kill -9 $(ps -ef | grep tomcat | grep -v grep | awk '{print $2}')
然后正常访问服务,再启动这个节点,确认它重新加入集群。这个动作建议在每次变更后都做一遍。
滚动发布的思路很简单:先把节点1从Nginx的upstream中摘除,优雅停止节点1,更新应用或配置,启动节点1,确认正常后再处理节点2。摘除操作不用改配置文件,直接禁用:
nginx复制server 192.168.1.11:8080 weight=1 max_fails=3 fail_timeout=30s down;
改完执行nginx -s reload,配置立即生效,流量不会进入已摘除的节点。
5. 高可用增强:故障转移与Keepalived
5.1 为什么要做VIP漂移
如果集群前端只有一台Nginx,那这台Nginx本身就是单点。Nginx挂了,整个集群对外就不可用了。解决思路是做Nginx的主备模式,通过Keepalived实现VIP(虚拟IP)漂移:主Nginx持有VIP,承担所有流量;备Nginx健康检查发现主节点异常,自动抢占VIP接管流量。
这套方案胜在不依赖云平台的额外产品,两台北网机器就能做。但要注意,云环境里的VIP漂移是否生效,取决于云网络是否允许在一个子网内绑定同一个内网IP。多数云平台支持,但有些SDN网络可能限制,需要先在测试环境验证。
5.2 Keepalived配置与脑裂注意事项
主Nginx上配置/etc/keepalived/keepalived.conf:
bash复制global_defs {
router_id nginx_master
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 150
advert_int 1
authentication {
auth_type PASS
auth_pass 123456
}
virtual_ipaddress {
192.168.1.100
}
track_script {
chk_nginx
}
}
备Nginx的配置基本相同,只需要把state改BACKUP、priority改为比主节点低(比如100),virtual_router_id必须保持一致。
最关键的是健康检查脚本/etc/keepalived/chk_nginx.sh:
bash复制#!/bin/bash
if ! pgrep -x "nginx" > /dev/null
then
systemctl stop keepalived
exit 1
fi
如果Nginx进程不存在,停止本机Keepalived,VIP漂移到备节点。这里有个细节:不能用ps -ef | grep nginx | grep -v grep做判断,那个方式在Nginx进程僵死时也检测不到异常。更好的方式是加上端口探测:
bash复制if ! curl -s http://127.0.0.1/health > /dev/null 2>&1
then
systemctl stop keepalived
exit 1
fi
要注意脑裂问题。两台Nginx都认为自己是主节点,同时持有VIP,导致流量混乱。最常见的诱因是两台机器之间的VRRP通信被防火墙或安全组拦截。排查方法很简单:在主备节点上分别执行ip addr show查看VIP是否同时出现在两台机器上,如果同时出现,就是脑裂。防火墙放行VRRP协议需要的端口(通常是112协议,不是TCP端口)后,问题一般就解决了。
Keepalived也不是银弹,它只能解决Nginx挂掉的问题,解决不了Nginx进程活着但转发异常的问题。我在生产环境还会额外加一层云负载均衡的TCP健康检查,双保险才安心。
6. 常见问题排查与避坑实录
6.1 乱码问题全集
Tomcat乱码是出现频率最高的老问题,而且症状五花八门:页面中文乱码、日志中文乱码、GET参数中文乱码、POST请求中文乱码。
页面乱码的根源是编码不一致。JSP页面编码、Tomcat响应编码、浏览器解码编码三段必须统一。检查思路:
- JSP文件头部检查
pageEncoding是否设置为UTF-8 - HTML中
meta charset="UTF-8" - Tomcat连接器设置
URIEncoding="UTF-8" - 确认
server.xml中Connector的useBodyEncodingForURI没被错误设置
日志乱码通常是Log4j配置里编码设置不对,需要在log4j配置中指定:
properties复制log4j.appender.file.encoding=UTF-8
如果整个环境的默认编码都不对,检查file.encoding系统属性,在catalina.sh里强制指定-Dfile.encoding=UTF-8,比在应用代码里到处设置编码更彻底。
6.2 启动失败排查:端口、权限、资源冲突
Tomcat启动失败的常见原因,我按遇到频率排序:
第一个是端口被占用。8080端口被其他进程占用的概率很高,排查用:
bash复制ss -lntp | grep 8080
第二个是权限问题。云主机上如果Tomcat是用root用户启动的,在某些安全策略下会有风险,建议使用独立用户运行。如果/opt/tomcat/logs目录的属主不对,启动时无法写日志,也会启动失败。
第三个是配置文件错误。注意检查server.xml里的标签嵌套是否合法,Connector、Engine、Host的标签顺序有严格限制,多一个空格少一个斜杠都可能导致解析失败。
第四个是一个比较隐蔽的问题:启动时一直卡在Deploying web application archive不继续。这个绝大多数情况是应用启动时的静态资源扫描太慢,或者JSP编译卡住。可以开启parallelDeployment提升并发部署能力,但更重要的检查是catalina.properties里的tomcat.util.scan.StandardJarScanFilter.jarsToSkip配置,把不用的JAR包加入跳过名单,减少类扫描耗时。
6.3 Could not obtain connection to metadata:数据库连接池问题
热词里有“tomcat启动报错could not obtain connection to query metadata : cannot create”,这个报错我见得太多了。它本质上是Tomcat的数据源(连接池)无法从数据库获取连接,通常不是Tomcat本身的问题,而是数据库连接配置或者数据库服务状态的问题。
排查路径按顺序做:
bash复制# 1. 先从Tomcat所在机器直接测试数据库连接
mysql -h<数据库IP> -u<用户名> -p<密码> -e "select 1"
# 2. 如果数据库通,检查连接池配置
重点检查几个地方:数据库地址是否可达、用户名密码是否正确、数据库的最大连接数是否满了、防火墙是否放通数据库端口、数据库本身是否处于不可写状态。
还有一类情况是数据库连接空闲超时后,MySQL主动断开了连接,而连接池里的连接不知道,还在继续使用,就会报Communications link failure。解决办法是配置连接池的心跳检测,在context.xml的数据源里加:
xml复制<property name="validationQuery" value="SELECT 1"/>
<property name="testWhileIdle" value="true"/>
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
validationQuery用来验证连接是否有效,testWhileIdle让空闲连接定期接受检查,timeBetweenEvictionRunsMillis是检查间隔。这样即使数据库断开了空闲连接,连接池也能及时发现并重建。
6.4 性能与安全:不要让Tomcat裸奔在公网
这里要给一个安全性提醒,虽然没有高危漏洞的实操演示,但安全意识很重要。Tomcat历史上出过不少远程命令执行漏洞,大多和反序列化、JSP上传、管理后台弱口令有关。云上部署时,至少要做到以下几点:
- 删除
webapps下的默认应用(ROOT、docs、examples、manager、host-manager),减少攻击面。 - 修改
server.xml中的Server port,默认的8005关闭端口要改掉或者用防火墙封禁。 - 管理后台(
manager应用)不要直接暴露到公网,只允许内网IP访问。 - 生产环境不要用
JSP动态生成页面,换成静态资源+接口的方式,降低JSP注入风险。
有安全扫描工具结果说“Tomcat版本存在漏洞”的,优先升级到最新稳定版本,比任何配置加固都有效。
6.5 性能验证方法:压测与监控
最后给一个性能验证的操作清单。调优不能靠感觉,要有数据支撑。我常用的压测工具是ab(Apache Bench)和wrk,二者都很轻量。
bash复制ab -n 1000 -c 100 http://192.168.1.11:8080/api/health
这个命令表示1000个请求,并发100。重点看结果里的Requests per second、Time per request、Failed requests。如果Failed requests不为0,先看失败原因,再调参数。
压测时要观察Tomcat的localhost_access_log响应时间、GC日志、系统CPU和内存监控。我习惯在压测时开两个终端,一个跑压测,一个跑jstat -gcutil <pid> 1000,实时观察GC是否健康。
调优是循环过程,不是一劳永逸。每次压测发现瓶颈,修改配置,再压测,直到瓶颈转移到业务代码本身。到那个时候,说明Tomcat这一层已经不是罪魁祸首,该考虑优化SQL、加缓存或者拆分服务了。
结尾
做了几年Java中间件相关的云上交付,我最大的体会是:Tomcat调优的难点不在于某一项参数不会设置,而在于对整个处理链路有系统性的理解。线程池、JVM、连接器、集群、Session同步,每层都有自己的瓶颈和优化手段,只盯着一两个参数猛调,往往按下葫芦浮起瓢。
最后分享一个个人实操习惯:每台Tomcat节点上线后,我会把关键配置做一次快照备份,包括server.xml、catalina.sh、context.xml、web.xml。每次改动前先备份,改动后记录改动原因和时间。这个习惯在集群出问题回滚配置时救过我很多次,比任何监控工具都实在。希望这篇东西能帮你少踩一些我踩过的坑。
