
1. 理解Tomcat处理请求的完整链路调参前必须先搞懂的事很多人在给Tomcat调优的时候第一反应就是线程数不够把maxThreads调大然后对着server.xml一顿猛改。说实话我早年也这么干过结果并发上去了Tomcat反而更慢——不是线程数不行而是对Tomcat处理请求的完整链路缺乏认知。参数调优本质上是在正确的位置释放资源不知道资源在哪里被消耗调什么都是碰运气。先看一条请求从进入到返回在Tomcat内部到底走了哪些关卡。第一道关卡是Connector连接器它负责监听端口、接受TCP连接、解析HTTP报文。你配置的port、protocol、maxConnections、acceptCount、connectionTimeout都在这一层生效。Connector拿到请求后会把任务扔给Executor线程池由线程池中的工作线程执行后续的Servlet逻辑。工作线程会读取请求体、调用你的业务代码、把响应写回客户端。关键点来了maxThreads控制的是工作线程的数量但它不等于Tomcat能承载的最大并发连接数。连接数和线程数是两个独立的池子连接数由maxConnections控制超过maxConnections之后TCP连接会进入acceptCount指定的等待队列。很多人只调线程数忽略了连接数导致线程池根本没满连接先被拒了。关于这块的执行链路可以用一张简单的表来说明参数作用发生阶段maxConnections最多接受的TCP连接数连接建立前acceptCount等待队列长度连接数耗尽时maxThreads最大工作线程数请求被处理时minSpareThreads最小空闲线程数线程池运行时connectionTimeout等待接收请求报文的时间连接建立后再补充一个容易误解的点Tomcat处理请求是同步阻塞模型NIO模式下也是如此工作线程在业务执行期间是被占用的。只要你的业务代码里有耗时的操作——查数据库、调外部接口、读写文件——工作线程就在那里等着这个等待期间线程既不能处理其他请求也不能被回收。所以maxThreads并不是越大越好它受CPU核数、任务类型、JVM内存三方共同制约。知道了链路才谈得上调优。下面的内容全部围绕这个链路展开建议你把server.xml、catalina.sh或setenv.sh翻出来对着看一行一行核对。2. 核心线程池与连接器参数决定吞吐量的关键组合2.1 连接器IO模型选型不要还在用BIO先花一段说IO模型因为这个选错了后面的参数全是空中楼阁。Tomcat从8.5版本开始默认使用NIO从8.5.6开始增加了NIO2异步IO9.0.x默认协议是HTTP/1.1配合NIO。很多从老项目迁移过来的还在用BIO其实BIO在Tomcat 8.5之后已经移除了如果你在server.xml里看到protocolHTTP/1.1用的就是NIO。我不建议刻意换NIO2除非你的业务模式是大量长连接且IO密集否则NIO2的异步回调在代码复杂度上带来的收益并不明显。还有一个被忽略的选项是APR用本地库处理SSL和静态文件理论上性能比NIO好但需要安装native库而且在高版本Tomcat里APR对OpenSSL的版本有要求运维成本偏高。我的建议是默认NIO不折腾。除非你做过压测对比确认APR能带来肉眼可见的收益再考虑切换。连接器配置在server.xml的Connector标签里核心参数组合如下Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxConnections10000 acceptCount1000 maxThreads800 minSpareThreads100 keepAliveTimeout10000 maxKeepAliveRequests100/2.2 线程数计算不是拍脑袋是靠公式推出来的关于maxThreads网上流传的公式很多最著名的是IO密集型的线程数 CPU核数 × 2 1。这个公式出自《Java并发编程实战》但在Tomcat场景下直接套用容易出问题因为Tomcat线程不只是处理业务逻辑还要承担报文解析、响应编码这类损耗。我自己的经验是按这个公式做基准线程数 CPU核数 × (1 平均等待时间 / 平均计算时间)举例说明假设你的应用平均计算时间是50ms但每个请求要等数据库查询200ms才能继续那么等待/计算比就是4。4核机器上理论线程数 4 × (1 4) 20。这看起来比很多生产环境上的配置小得多对吧因为很多机器上你开了200个线程其中180个都阻塞在数据库连接等待上白白消耗内存和上下文切换开销。那生产环境到底配多少我建议分场景场景参考配置理由纯静态资源/短请求50~200请求处理快线程占用时间短典型Web应用有DB查询200~400线程会在IO上等待需要一定冗余高并发且下游依赖多400~800阻塞时间长但超过800基本得不偿失计算密集型应用CPU核数 1多线程反而增加上下文切换开销超过800之后线程切换成本和内存占用会急剧上升。以一个线程默认栈1MB计算-Xss1024k1000个线程光是栈就吃掉约1GB内存这还不算线程私有的其他数据。2.3 acceptCount与maxConnections连接池的双保险连接器有一对参数必须配合起来看maxConnections和acceptCount。maxConnections是Tomcat愿意同时接受的TCP连接数上限。在NIO模式下这个值默认是81928.5之后达到上限后新的连接请求不会立刻被拒绝而是进入acceptCount指定的队列等待。acceptCount默认值是100。这个队列的大小意味着最多还能容忍多少个连接在门口排队。想象一下银行的场景maxConnections是柜台窗口数量acceptCount是大厅里的排队等待区。窗口满了客户就在大厅排队大厅也满了新来的客户只能被拒之门外。这里有个很微妙的点连接进入acceptCount队列不代表请求会被处理。连接进了队列但线程池满了客户端依然要等。所以这两个参数要和线程池配合着看。我的经验是maxConnections建议比maxThreads大20%左右比如maxThreads400时maxConnections可以设到500~600。acceptCount不要设得太大几百就够否则大量请求堆积在队列里客户端超时重试会加剧服务端压力。一个小细节队列里的连接不会触发connectionTimeout它们只受acceptCount和Socket超时的影响设置不当会造成假死——客户端感觉服务卡死了但服务端其实满负荷运转。2.4 keepAliveTimeout与maxKeepAliveRequests容易被忽视的连接霸占HTTP keep-alive机制允许客户端复用同一个TCP连接发送多个请求省去反复握手的开销。但代价是连接被长时间占用尤其有些客户端会无限制地保持连接。你可以查一下生产环境上的连接状态很多TIME_WAIT或ESTABLISHED连接其实都在空转。keepAliveTimeout控制的是连接空闲多久后关闭默认20秒有的版本是5秒如果你知道客户端比如APP、小程序后端会周期性发起心跳请求可以把这个值适当调大比如30秒避免连接频繁建立。反过来如果服务端连接数经常打满可以缩短到5秒甚至3秒强制客户端重新建连释放空闲连接。maxKeepAliveRequests表示一条连接最多能复用多少次默认100。这个参数既保护你也保护客户端。太大的话连接一直不释放配合keepAliveTimeout可能造成连接数虚高太小的话客户端需要频繁建连握手开销上升。我把这两个参数看作一对节流阀需要压测后根据业务形态来定不要沿用默认值就不管了。3. JVM参数调整为什么调Tomcat前必须先调JVM3.1 堆内存从崩溃现场说起讲一个真实案例。去年有个朋友的项目上线前压测并发一上来Tomcat就报OutOfMemoryError: Java heap space日志里还能看到大量的GC overhead limit exceeded。他一直在调maxThreads和maxConnections换各种数值都没用最后发现是启动脚本里压根没设置-XmxJVM拿的是默认值——物理内存的1/4。那台机器是16G内存JVM堆只有4G压测到200并发就顶不住了。-Xms和-Xmx设置多大合适我给个保守的分步思路先统计应用自身的内存水位用jmap -heap pid或VisualVM看Full GC后的老年代占用得到基线。-Xms和-Xmx设为相同值避免堆大小动态伸缩带来的性能抖动。预留20%~30%余量给非堆内存、线程栈、直接内存和操作系统缓存。示例一台8G内存的机器跑Tomcat建议堆设置-Xms3g -Xmx3g。剩下5G留给Metaspace、线程栈、DirectMemory、操作系统分页缓存。你硬要-Xmx5g也行但线程栈、文件缓存、网络缓冲区都会受影响系统可能提前进入swap。再强调一个细节业务高峰期观察堆使用不是看used而是看老年代的增长曲线。如果老年代持续上升不回落说明对象在逃逸这不是加内存能解决的需要查代码里的大对象、静态集合、线程局部缓存ThreadLocal引用等问题。这已经超出Tomcat调优的范畴但它是Tomcat调优里最常遇到的隐形瓶颈。3.2 元空间、栈大小与直接内存堆外也有一片天JVM调优如果只盯堆等于只看了一半。-XX:MetaspaceSize和-XX:MaxMetaspaceSize控制元空间大小。元空间放的是类元数据、方法信息、常量池。Spring Boot应用的类加载量非常大而且每次部署热更新都会产生新的类加载器默认元空间是动态增长的。建议-XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m防止某些极端场景下元空间无限膨胀。如果你的应用加载了大量第三方依赖这个值可能要再往上调。-Xss控制线程栈大小默认1024K1MB。前面算过1000个线程就是1GB内存。如果你的业务方法调用层级不深比如简单的Controller→Service→Mapper三层调用-Xss256k一般够用如果涉及递归、复杂表达式引擎、深度框架调用链保守点用512k。用jdk.internal.vm.StackChunk这种新特性会比较纠结但生产环境还是求稳为主。还有一块容易被遗忘的是NIO的DirectMemory默认等于-Xmx。Tomcat的NIO模式、以及很多现代框架Netty、Spring WebFlux都会使用堆外内存做缓冲区。如果你设置-Xmx3gJVM理论上最多还能再分配约3G直接内存这两个加起来就是6G远超物理内存时会触发OutOfMemoryError: Direct buffer memory。稳妥的做法是显式设置-XX:MaxDirectMemorySize1g限制堆外内存总量。3.3 GC策略高并发短请求场景下的选择Tomcat这类Web容器请求生命周期短、对象朝生夕灭响应时间对GC停顿非常敏感。JDK 8默认的Parallel Scavenge在吞吐量上表现好但Full GC停顿时间长可能达到秒级线上会出现明显的卡顿尖刺。我个人的经验是4核以上、8G以上内存的机器优先考虑G1如果用的是JDK 11G1已经是默认不用折腾。关键参数是以下几个-XX:UseG1GC -XX:MaxGCPauseMillis100 -XX:InitiatingHeapOccupancyPercent45 -XX:G1NewSizePercent5 -XX:G1MaxNewSizePercent60MaxGCPauseMillis是期望的最大停顿时间设成100毫秒左右比较合理太小会导致GC过于频繁反而拖低吞吐。InitiatingHeapOccupancyPercent的老年代占用触发阈值默认45%如果你的老年代对象长期偏高可以降低到35%提前触发并发标记避免出现并发标记赶不上分配速度导致的To-space exhausted。GC调优没有万能参数判断标准只有一条压测时观察Full GC频率和停顿时间。如果Full GC每小时超过一次说明堆对于当前流量来说偏小或者有内存泄漏。用jstat -gcutil pid 1000观察一段时间的GC曲线比任何经验公式都准确。4. 压测前置与参数调整顺序让每次修改都有依据4.1 压测工具的选择与基本操作没有压测数据的调优就是自嗨。推荐三款工具按场景选工具特点适用场景ApacheBenchab轻量、单机命令、适合简单GET请求快速验证连接数、响应时间JMeter功能强、支持复杂业务脚本、分布式压测有登录态、参数化、混合接口的业务压测wrk高并发、基于事件驱动、输出指标丰富纯接口性能验证压满单机CPU以JMeter为例一个最小可用压测配置是这样的线程组设置并发数比如200、Ramp-Up时间建议10秒避免瞬间打满、循环次数如果做持续时间压测用Duration600秒。添加HTTP请求的时候记得配置KeepAlive和Follow Redirects这两个选项会影响压测结论。压测机如果跟被测机器在同一台结果会失真尽量用独立的压测机或者至少做好网络隔离。压测后重点看三个指标吞吐量TPS、响应时间分位数TP99、错误率Error%。只看平均响应时间没有意义要看TP99也就是99%的请求都在多长时间内返回。4.2 调优的先后顺序别一上来改线程池我见过太多人调优的顺序是反的。正确顺序应该是先确认系统资源水位。压测前先看CPU、内存、IO如果CPU已经跑满调Tomcat线程数没有意义。调JVM。先保证堆内存够用、GC正常再去调线程池。JVM内存不足时线程数越多GC越频繁反而更慢。调线程池与连接器。这是Tomcat最核心的调优环节。调业务层。数据库连接池、缓存策略、慢SQL这些往往比Tomcat参数的影响大得多。换句话说Tomcat参数调优是最后一步而不是第一步。数据库连接池太小、缓存命中率太低的情况下你把maxThreads从200调到1000结果只会是1000个线程同时阻塞在数据库上系统资源被无意义地耗尽。4.3 完整的调优闭环基线、改动、验证、回归我给自己定了一个固定流程每次调优都严格走完第一步跑基线压测。以当前生产配置跑30分钟压测记录TPS、TP99、错误率、CPU、内存、GC频次。第二步修改一个参数只修改一个。改两个以上参数后如果结果变差你根本不知道是谁的锅。第三步同条件复测。同样的并发量、同样的时长、同样的压测脚本记录结果。第四步对比决策。如果TPS下降或错误率上升回滚这个参数换另一个方向如果变好保留继续下一个参数的实验。这套流程很笨但非常有效。我常跟身边人说调优是个减法的过程——不是把所有参数都调大而是找到每个参数在当前业务下的合适值然后把不适合的减掉。5. 一场真实的连接数踩坑排查连接池满导致Tomcat线程耗尽分享一个真实的故障排查过程这是我印象最深的一次Tomcat参数调优事故因为问题的根子根本不在Tomcat参数上。5.1 故障现象接口全部超时线程数打满项目是一个典型的Spring Boot单体应用内嵌Tomcat部署在4核8G的机器上。某天上线后半小时生产环境开始频繁报警接口超时、HTTP 502。查监控发现Tomcat的活跃线程数接近配置上限maxThreads400大量的请求堆积在线程池的队列里。当时的第一反应是并发量上来了400个线程不够需要调大。于是临时把maxThreads改成800重启后确实缓解了一小会儿但半小时后再次打满。这时候才意识到问题不在线程池本身而是线程都在等某个下游资源。5.2 排查链路jstack定位线程阻塞点排查过程按顺序走用jstack pid抓线程快照连续抓三次间隔10秒。查看RUNNABLE状态的线程集中在哪个堆栈尤其关注java.net.SocketInputStream.socketRead0和com.mysql.cj.jdbc相关的调用。发现大量Tomcat工作线程阻塞在HikariPool.getConnection上也就是数据库连接池拿不到连接。根源一下清晰了数据库连接池最大连接数配置的是50而Tomcat有400个线程如果这400个请求同时需要数据库连接只能有50个拿到其余350个全部阻塞等待。阻塞的线程占用着Tomcat工作线程新的请求就只能排队等线程接口自然全部超时。5.3 根因修复不是Tomcat的锅修复方案分两步首先把数据库连接池从50调到了120这个行为要谨慎数据库端最大连接数也要同步调整否则只是把压力转移到数据库。其次在业务代码层面对外部依赖调用增加了超时和熔断机制避免下游缓慢时把线程全部拖死。调完之后Tomcat的活跃线程数稳定在200以内TP99从3500ms降到180ms。整个过程中Tomcat本身的参数几乎没怎么动之前的400个线程配置在正常情况下是够用的。这次事故给我的教训是Tomcat参数调优之前必须先搞清楚线程在等什么。线程池满只是症状不是病因。5.4 从这次故障中沉淀的监控指标后来我在生产环境补了几个关键监控项分享出来供参考监控项采集方式预警阈值Tomcat活跃线程数JMXCatalina:typeThreadPool持续超过最大线程数的60%Tomcat连接数ss -s或JMX超过maxConnections的70%数据库连接池活跃数HikariCP指标超过池大小的80%Full GC次数与耗时JMX或jstat脚本Full GC超过每小时1次这套监控看起来基础但非常实用。调优不是一次性动作而是持续观察、动态修正的过程。没有监控的调优改了参数都不知道有没有起作用。6. 几个日常运维中容易踩的看似调优实则添乱的细节最后写几个我在项目里反复看到的问题都是细节但如果踩中了前面的调优全白费。6.1 server.xml里多个Connector的混淆如果一个Tomcat实例里配置了多个Connector比如8080给HTTP8443给HTTPS要注意线程池是共享的还是独立的。默认情况下每个Connector会创建独立的线程池如果你只给其中一个调优另一个可能还是默认值而且两个Connector抢CPU资源整体表现不会达到预期。建议显式配置一个共享线程池在server.xml里定义Executor然后两个Connector通过executor属性引用它Executor nametomcatThreadPool namePrefixcatalina-exec- maxThreads400 minSpareThreads100/然后把Connector里的maxThreads、minSpareThreads删掉加上executortomcatThreadPool。这样线程池统一管理监控也方便。6.2 改完参数不重启自动生效的错觉Tomcat有不少参数支持通过JMX或者管理接口动态调整但仅限于部分线程池参数如maxThreads可以通过JMX修改。很多连接器参数如maxConnections、acceptCount在Connector初始化时就已经确定了修改后必须重启才能生效。我建议改完参数一律重启重启后立刻用jinfo或JMX验证参数确实已经加载避免出现改了但没生效的假象。6.3 操作系统层面对Tomcat的限制Tomcat的连接数还受操作系统两个隐藏参数限制文件描述符数量ulimit -n和TCP端口范围。一个高频踩坑场景你把maxConnections调到了20000但操作系统的ulimit -n还停留在1024Tomcat在800连接时就报Too many open files。调整方法# 查看当前限制 ulimit -n # 临时提高重启后失效 ulimit -n 65535 # 永久设置需修改/etc/security/limits.confTCP端口范围影响的是Tomcat作为客户端时的连接能力比如Tomcat去调其他服务的接口如果出方向的端口范围太小同样会报Cannot assign requested address。这个不在Tomcat配置里属于系统层调优但在高并发场景下往往成为瓶颈。根据我自己的经验Tomcat参数调优是比较看菜下饭的活儿。每个项目的基础环境、业务模型、下游依赖都不相同照搬网上的参数配置很难直接适用。按照上面这套方法——理解请求链路先确认JVM和操作系统再压测、再改参数、再验证——即使底子薄的项目也能稳扎稳打地把性能提上来。最后再啰嗦一句调优尽量用数据说话压测工具、监控指标比直觉可靠得多。