1. 先搞清楚Tomcat在Java Web技术栈里的真实位置
我刚接触Java Web开发那会儿,对Tomcat的理解就一句话:把项目打成war包扔进去,启动完就能访问。后来踩的坑多了才发现,Tomcat远不是一个“能跑就行”的东西,它在你整个技术体系里承担的角色、它内部的运行机制、以及那些配置文件里的细微参数,可能直接决定你线上服务稳不稳、快不快。
先对齐一个基本概念:Tomcat本质上是一个Servlet容器。它实现了Java EE规范里的Servlet和JSP规范,负责接收HTTP请求、把请求交给对应的Servlet处理、再把处理结果返回给客户端。很多人容易把它和Web服务器混在一起,其实Tomcat本身就内置了HTTP服务能力,也能直接处理静态资源,所以小项目直接用Tomcat做Web服务器没问题。但到了大流量场景,通常前面会再挂一层别的Web服务器或者负载均衡器,让它们处理静态文件、做反向代理和负载分发,Tomcat专注于动态请求处理。
Tomcat还经常被拿来和别的容器做对比。有人问“我的项目能不能用别的容器”,当然可以,但你得知道差异在哪。比如有些轻量级容器启动快、占内存少,适合微服务场景;还有些重量级产品商用化程度高,功能全但授权费用也是要考虑的成本。Tomcat的优势就是开源免费、稳定、生态成熟、资料丰富,绝大多数传统Java Web项目选它都不会错。
搞清楚了这一层,后面所有配置和调优才有意义。比如为什么线程池参数影响着系统的吞吐量,为什么有时候改一个连接数的数值系统就不报503了,根本原因都在于Tomcat的工作机制:它内部维护线程池处理请求,每个连接对应一个Socket,但并非每个Socket都占用一个线程,线程数和连接数互相约束,处理不好就会要么资源浪费、要么请求排队超时。
Tomcat的生命周期管理也是理解它的关键一环。它不像一些纯内存服务那样启动就完事,它内部有清晰的容器层级:Server下面挂Service,Service里面包含Connector和Engine,Engine下面挂Host,Host下面挂Context。这个层级可能第一次接触的人头大,但你只要记住一句话:Tomcat本质上是一套“把URL映射到Web应用”的机制,Host决定用哪个域名进来,Context决定落到哪个应用上,Connector决定用什么协议和端口接收请求。把这条线理顺了,后面改配置、排查问题都会茅塞顿开。
这篇文章我会从部署、配置、运维、调优几个角度,把Tomcat从头到尾掰开揉碎讲一遍。无论你是刚接触Java Web的新手,还是已经写了两三年业务代码但没时间深挖底层细节的开发者,这篇都值得花点时间过一遍。
2. 装好Tomcat之后,第一步不是写代码而是读懂目录结构
很多教程上来就让你下载、解压、启动,看控制台打出一串日志就完事了。这没什么不对,但我的建议是装完之后先花几分钟把目录结构过一遍。你后面写脚本、打包部署、改配置、排查故障,几乎所有操作都会跟目录结构打交道,提前熟悉能省下很多不必要的试错时间。
2.1 下载安装阶段容易被忽略的两个细节
首先是版本选择。别傻乎乎只认“最新版”,你要看自己项目的Java版本。Tomcat从8.0开始支持Java 7,8.5这个版本线要求Java 7及以上,Tomcat 9要求Java 8及以上,Tomcat 10则基于Java EE迁移到Jakarta EE,包名从javax.变成了jakarta.。这个变化是致命的:假如你把一个依赖javax.servlet的老项目直接丢到Tomcat 10里,大概率编译和运行都过不了,而很多人根本意识不到问题出在容器版本上。
其次是解压路径。Linux服务器上建议放在/opt或者/usr/local这类规范目录,不要解压到用root权限才能写的位置,也不要放在带中文和空格的路径下。Windows环境下尤其要注意路径空格问题,某些脚本执行时会因为路径解析失败报非常诡异的错误。我见过不止一个人因为偷懒解压在桌面上,结果后续写systemd服务脚本时路径怎么写怎么别扭。
对于生产环境,我还建议大家固定一个Tomcat版本,不要今天一个版本明天一个版本。不同版本之间不仅配置文件结构有差异,GC策略、JSP编译器、连接器实现都有微妙变化,频繁变动会让故障排查变得困难。最好是选一个长期维护的稳定版本,除非有明确的安全更新需求,否则不要动不动升级。
2.2 conf目录说得上话的几个文件
conf目录是Tomcat的配置文件大本营,你大部分时间只需要关心下面这几个:
- server.xml:Tomcat的核心配置文件。端口、连接器、虚拟主机、应用映射都在这里定义。改它的频率其实不高,但每改一次都要慎重,因为格式要求严苛,一个标签闭合错误整个服务都起不来。
- web.xml:全局的Web应用默认配置。默认的Servlet映射、MIME类型映射、Session超时时间都在这里。注意这个文件是“全局默认值”,每个Web应用内部还有自己的WEB-INF/web.xml,两者叠加生效。
- catalina.properties:类加载器相关的配置和一些全局属性。里面有两行配置值得记住:common.loader和webapps内的默认加载路径,多数人不会动它,但排查“类冲突”“找不到类”之类的问题时需要回溯到这里。
- logging.properties:日志配置。Java本身有个java.util.logging体系,Tomcat默认用它输出日志。我看过一些团队把Tomcat运行日志重定向到自定义位置,结果grep不到日志折腾一天的案例,问题往往就出在这份配置上。
- context.xml:全局的Context配置,可以理解成所有应用的默认参数模板。里面有个Manager类的配置,生产环境建议把自动部署开关关掉,这后面会细说。
很多操作手册把conf目录下所有文件列一遍,但从实用角度讲,你要优先掌握的是server.xml和web.xml,一个是“骨架”,一个是“默认行为”,剩下的用到再查也来得及。
2.3 webapps与部署方式的对应关系
webapps目录是Tomcat默认发布应用的位置。你丢一个war包进去,Tomcat启动时会自动解压并部署。这个机制很方便,但生产环境我反而不推荐依赖它。原因是它有一些隐含行为:如果你看日志发现应用被解压了两次,或者启动时反复重载同一个应用,多半和这个自动部署机制有关。
自动部署背后是后台线程定期扫描目录变化,扫到新的war包就会触发deploy,扫到目录变化就可能触发reload。开发环境这么干无所谓,生产环境上一旦代码更新触发服务抖动,影响面可能很广。所以我后来的习惯是:开发环境用自动部署节省时间,生产环境用手动部署,部署完成后通过管理接口或者重启实例来加载新版本。
webapps里还会出现一个ROOT目录,这是Tomcat的根应用。访问域名加端口不跟任何路径时,命中的就是它。如果你的项目不需要这个默认首页,可以把它清空或者替换成自己的应用,尤其有些安全扫描会因为这个默认首页给出低危告警,提前处理掉省得后面解释半天。
3. 部署应用的四种方式,我用一遍之后只推荐其中两种
讲完目录结构,马上就面临一个实际问题:我的项目怎么放上去?网上能搜到各种部署方法,但很多文章只是罗列步骤,没说清楚各自适用什么场景。我按自己的实践把它们分成了四类,对比完你就知道该选哪种。
3.1 直接把war包丢进webapps,开发环境最省心
这应该是入门最容易的方式。把项目打好的war包拷贝到webapps目录,启动Tomcat,它扫描到war文件就会自动解压部署。整个过程几乎零配置。这种方式适合什么场景呢?本地开发、联调环境、测试环境,讲究快速迭代,每次改完代码打个包丢进去,访问路径也一目了然。
但它有几个问题需要注意。
第一,Context路径默认取自war文件名。比如你放了demo.war,访问路径就是/demo。如果想改路径,就得改文件名再重新部署,操作很笨重。第二,每次更新war包时,如果文件名带版本号或者时间戳,Tomcat会把它当新应用部署,webapps目录里会同时存在多个版本,旧版本还占着内存。我见过有人用脚本自动打包后文件名带时间戳,结果服务器上堆了十几个版本目录都没清理。第三,默认配置下Web应用是支持热替换的,但Tomcat的reload机制本质上是重建类加载器,如果应用里有比较大的内存缓存或者后台线程,会出现资源释放不干净的情况。
我的建议是:自动部署只配开发环境用,生产环境坚决关掉。
3.2 利用server.xml显式配置Context,适合路径有特殊要求的场景
第二种方式是直接在server.xml的Host节点里加Context配置,比如:
<Host name="localhost" appBase="webapps"> <Context path="/api" docBase="/data/apps/myapp.war" /> </Host>这样配的好处是部署路径完全可控,war包甚至可以放在服务器任意位置,不必塞进webapps。路径和实际文件地址分离,维护灵活。坏处是修改server.xml后必须重启Tomcat才生效,而且server.xml一旦写错,整个服务直接崩溃,风险偏高。
我实际用这招的场景一般是:同一台服务器上部署多个应用,并且希望通过不同的访问前缀区分它们;或者应用文件不在默认目录里,想通过外部目录统一管理。但坦白说,我现在很少手动去改server.xml做映射了,因为有更干净的方式可以替代。
3.3 在conf/Catalina/localhost下新建独立的Context描述文件
这种方式平时不太被提起,但它比第二种优雅得多。在conf/Catalina/localhost目录下新建一个XML文件,文件名就是访问路径,文件内容指向实际的war包地址。举个例子,创建一个conf/Catalina/localhost/api.xml:
<Context docBase="/data/apps/myapp.war" />然后访问/API路径就能直接命中它。这样做的好处是:不需要重启Tomcat,部署时只要把文件放进去并确保war包就位,Tomcat会自动加载;移除应用时删掉对应XML文件即可。不修改主配置文件,降低了破坏全局配置的风险。
如果要说缺点,就是这种方式不够“统一”,同一台服务器上的多个应用分散在各处,新人接手时还需要排查一遍哪些应用是从这个目录加载进来的。但只要团队有文档习惯,这是我能接受的成本。
3.4 通过管理后台远程部署,适合有运维平台支撑的团队
Tomcat自带的Manager应用提供了网页和命令行两种部署方式。输入账号密码之后,可以上传war包、查看已部署应用列表、触发停止和启动。听起来很方便,但我一般会主动关掉它。原因很简单:Manager应用本身需要暴露一个管理端口,如果账号密码强度不够或者网络没有做隔离,等于把服务器的大门钥匙放在门垫下面。
生产环境如果一定要用远程部署,建议配合运维平台做堡垒机隔离,而不是把Manager端口直接暴露到公网。
下面用一个表格把这几种方式的适用场景收拢一下:
| 部署方式 | 改动位置 | 是否需要重启 | 适合场景 | 风险等级 |
|---|---|---|---|---|
| 拷贝war包至webapps | webapps目录 | 无需,自动扫描 | 开发/测试环境 | 低 |
| server.xml配置Context | server.xml | 需要重启 | 特殊路径映射 | 高 |
| 独立Context描述文件 | conf/Catalina/localhost | 无需 | 生产多应用部署 | 中 |
| Manager后台远程部署 | Manager页面 | 大多数情况无需 | 团队有运维支撑 | 高 |
综合来看,我实际项目中用得最多的是第一种和第三种组合:开发环境随便丢,生产环境用独立Context描述文件控制。简单、可控、可追溯。第四种直接不碰。
4. server.xml里的核心配置,逐项拆开讲明白
前文提到server.xml是Tomcat最核心的配置文件,但很多人对它只停留在“会改端口”的程度。端口其实是最简单的改动,我甚至不觉得它值得花精力去记,因为Connector标签里想表达的东西远不止端口号。
4.1 Connector标签的完整理解
先看一段常见的Connector配置:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxThreads="200" minSpareThreads="10" maxConnections="10000" acceptCount="100" URIEncoding="UTF-8" />逐个说。port是监听端口,没什么可解释的,但我想提醒一句:在Linux上如果端口小于1024,启动时必须用root权限,而Tomcat官方不建议用root跑服务,所以项目上常见两种处理思路——要么让前面的Web服务器或负载均衡把80端口的流量转发到8080,要么用端口转发或iptables规则跳一下。不要为了省事直接root启Tomcat,后患无穷。
protocol决定连接器使用的协议。默认HTTP/1.1走的是NIO模式,支持异步和非阻塞I/O。如果想用更现代的NIO2,可以配protocol="org.apache.coyote.http11.Http11Nio2Protocol"。相比之下,老项目里可能出现过BIO模式,就是传统的阻塞I/O,线程模型是“一个连接占一个线程直到处理完”,这种模式已经被时代抛弃了,碰到老教程让你配protocol="HTTP/1.1"但没说清内部实现时,心里要有数。
connectionTimeout是建立连接后等待请求的超时时间,单位毫秒。设太短的话,客户端慢一点就被断开;设太长的话,恶意连接会占住资源不放开。常规设置20秒左右,个别场景需要更长。
redirectPort指定SSL重定向端口。当你把某个Connector设置成支持SSL时,普通HTTP请求会判断它是否要求HTTPS访问,如果是就跳到这个端口。
然后是线程相关的一连串参数,这部分放后面第6节细讲,这里先不展开。
URIEncoding绝大多数情况下必须设成UTF-8。曾经默认值是ISO-8859-1,导致URL里带中文参数时解析出来的全是乱码。现在Tomcat版本默认已经是UTF-8,但老项目升级时要注意在显式配置里确认这一点。
4.2 Host、Alias和Context之间的协作关系
Connector负责接收请求,Engine拿到请求后要决定交到谁手里,这一步就是Host的任务。Host定义一个虚拟主机,通过请求头里的Host字段来做域名匹配。
最简单的配置就是默认那个:
<Engine name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> </Host> </Engine>一个Host对应一个域名。如果你想在同一个Tomcat上挂多个域名,或者一个域名下同时跑多个应用,可以配置多个Host节点。Alias属性可以给同一Host增加别名,比如主域名和CDN域名同时指向同一个应用:
<Host name="www.example.com" appBase="/data/apps" unpackWARs="true"> <Alias>example.com</Alias> </Host>appBase指定这个Host的应用基目录,Context相对路径和war包都会在这个目录下查找。理解了Host之后,前面说的独立Context描述文件的原理也就通了:conf/Catalina/localhost下的文件是“属于Catalina引擎的localhost这个Host的上下文描述文件”,文件名即路径。
很多人在这一步绕不过来的是Context、Host、URL三者映射关系。我打个比方:你这栋楼(Tomcat)有多个入口(Connector),进门之后前台(Engine)根据你要找的公司名称(域名)把你分配到对应楼层(Host),再根据具体办公室编号(Context路径)把人带到具体房间(Web应用)。这个类比虽然粗糙,但足够帮你建立第一层心智模型。
4.3 自动部署开关为什么在生产环境建议关掉
autoDeploy和unpackWARs这两个参数经常被忽略,但它们在部署时带来的影响特别大。autoDeploy为true时,Tomcat会周期性扫描appBase目录和Context描述文件的变动实现自动部署。开发时很方便,但生产环境真心不建议开。原因我在前面已经说了,热部署期间类加载器重建可能引起Full GC、请求超时、状态丢失。
unpackWARs指的是是否解压war包到同名目录。如果设为false,容器直接运行war包内的内容,启动速度会快一些,但修改静态资源会很不方便。实战中我更倾向于设为true,让war包解压后目录文件一目了然,排查问题也方便。
还有一个隐藏选项值得记:Host节点下的deployIgnore,可以忽略指定目录下的文件不自动部署。比如你有一些备份的war包放在webapps里又不想被误部署,用deployIgnore配一下正则就能拦住:
<Host name="localhost" appBase="webapps" deployIgnore=".*bak.*">这类小技巧属于配置文件里的“隐藏技能”,文档里写得模模糊糊,搜的时候往往想不起来关键词。收藏这篇,下次遇到就不用翻源码了。
5. 日常运维里那些几乎人人都会踩的坑
配置再熟练的人,也避免不了运维阶段遇到几个典型故障。Tomcat的报错信息有时候给得不够精确,你得有套排查逻辑,才能从现象快速定位到根因。这里写几个我反复见到的坑,以及完整的排查思路。
5.1 端口被占:能从日志里读出多少信息
最经典的现象:启动时日志里抛异常,提示端口已被占用。看日志的地方在logs目录下的catalina.out或localhost.log。如果你用的是systemd管理Tomcat,日志可能被journald接管,这时用journalctl查看。
排查链路第一步是确认到底谁占了端口。Linux上直接执行:
netstat -tlnp | grep 8080能直接看到PID和进程名。如果是Java进程占着,用jstack看一下它是什么应用,八成是另一个Tomcat实例没关干净。如果你确定旧实例已经不在了,但端口还被占着,有可能卡在了TIME_WAIT状态。这个状态本身不影响新监听,但某些Linux内核参数配置下可能出现绑定失败。
还有一种情况是同一个Tomcat里两个Connector端口配重复了。比如你加了一个HTTPS Connector,端口误写成和HTTP一样,也会报端口冲突。看到日志里连续出现两个Connector初始化失败时,优先查是不是端口重复。
Windows下排查也一样,命令是netstat -ano | findstr 8080,然后用tasklist查PID对应进程。
5.2 内存溢出:别一上来就调-Xmx
很多人的第一反应是“内存不够就加大内存”,这个思路没有错,但有个前置工作要先做:搞明白是堆内存不够还是元空间不够,还是干脆就是代码里泄漏了。
先看现象。如果日志里报的是java.lang.OutOfMemoryError: Java heap space,说明堆内存满了,可能同时伴随频繁Full GC,需要在catalina.sh里调整JVM参数,比如:
JAVA_OPTS="-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m"-Xms和-Xmx分别指定初始堆大小和最大堆大小,建议两者设置成一致,避免运行期堆扩容引发性能抖动。Metaspace在Java 8之后替代了永久代,主要装类的元数据,如果报的是Metaspace溢出,就调它。
如果日志里报的是java.lang.OutOfMemoryError: unable to create new native thread,那不是堆内存的问题,而是操作系统的线程数限制到达上限。排查方向变成了:检查ulimit -u、检查系统总线程数、检查程序里是不是无限制创建线程。我曾见过一个项目因为线程池参数配错,每秒新建几百个线程把连接池打满,最后连health check都返回不了。
还有一类最隐蔽的:堆内存一直涨,怎么调大都没用,最终还是OOM。这种往往不是参数问题,是代码问题,比如缓存没清理、数据库连接没释放、静态集合无限增长。遇到这种情况,得用jmap、jstat、MAT之类工具拉一份堆转储做离线分析。别人给你“调大-Xmx”的建议,治标不治本。
5.3 Java版本和Tomcat版本不匹配:最容易忽略的坑
升级Tomcat时最怕什么?怕老项目直接跑不起来,原因还不是代码问题,而是javax和jakarta包名变迁。Tomcat 10开始把Java EE迁移到了Jakarta EE,Servlet API的包名从javax.servlet变成了jakarta.servlet。如果你的项目是老代码,还在用javax.*包,直接部署到Tomcat 10会报ClassNotFoundException。这个坑有两个变体很容易踩:
第一种,项目本身是老代码,但团队为了“尝鲜”把开发环境升级到Tomcat 10,打开页面就白屏。第二种,项目里用了某些旧版本的第三方库,这些库内部还是javax.servlet的引用,你项目代码是新的也没用。
排查这个问题的思路也很简单:日志里如果出现NoClassDefFoundError或ClassNotFoundException,尤其报的类名带servlet字样,就去检查Tomcat版本和项目的编译目标。另外注意Tomcat的lib目录是容器级别的类库,项目自身依赖的Servlet API版本如果和容器不符,也常见这种报错。
5.4 403/404这类访问异常原因不在Tomcat而在应用
很多人访问应用报403时第一反应去查Connector配置,其实应该先看应用的web.xml里怎么配的。Tomcat的日志信息有时会告诉你更直接的原因。我清楚记得一次线上事故:所有接口正常,但根目录访问返回403,后来排查发现是应用里servlet-mapping配置把根路径拦截了,返回了禁止访问。这种故障和Tomcat本身的配置毫无关系。
排查这类问题,不要只盯Tomcat日志,还要看应用日志。应用日志的路径往往配置在logback或log4j里,打得全不全取决于应用自身。我曾经为了查一个白屏问题查了半天Tomcat,最后打开应用日志三秒钟就定位到问题。方向一错,效率就是天壤之别。
6. 从能用到好用:几个必须掌握的调优方向
最后一部分聊调优。Tomcat默认配置的目标是“跑起来不出错”,不是“高性能”。当你的系统并发量上来之后,默认值很快会成为瓶颈。调优不必一上来就把所有参数都改了,按照“先定位问题再调整”的思路,一个方向一个方向验证,效果才可控。
6.1 线程池、连接数、队列长度,三者怎么搭配
先理解Tomcat的线程模型。Tomcat维护一个线程池来执行请求处理,默认情况下,线程池里最大线程数对应maxThreads,初始空闲线程数对应minSpareThreads。当线程数不够用时,新的连接会进入等待队列,队列长度对应acceptCount。
这个模型最怕的情况是:maxThreads设得很小,队列又设得很大,大量请求积压排队,客户端长时间等待后超时,你还会看到一堆线程全部阻塞。反过来,maxThreads如果无限大,一旦遇到慢请求或下游接口变慢,线程会很快被打满,CPU上下文切换开销暴增,服务变得不可用。
我个人习惯的起点是这样的:maxThreads=200、minSpareThreads=20、acceptCount=100。这并非什么黄金参数,只是常规项目的安全起点。真正的调优永远要结合压测数据,调整一次压一次,观察吞吐量和响应时间的变化。
连接数maxConnections是另一层限制,它是Tomcat同时接受的TCP连接数上限,超过这个值之后,多出来的连接仍然排队,但不会直接进入处理阶段。NIO模式里Connector可以同时维持很多连接,但真正同时被线程处理的很少,所以maxConnections一般可以比maxThreads大一些。治本的办法是看压测结果,不是拍脑袋。
6.2 压缩与静态资源处理,能显著降低响应体大小
Tomcat支持HTTP压缩,配置在Connector节点上:
<Connector ... compression="on" compressionMinSize="2048" noCompressionUserAgents="gozilla, traviata" compressableMimeType="text/html,text/xml,text/plain,text/css,application/javascript" />开启后,超过2KB的响应体如果匹配MIME类型,Tomcat会做gzip压缩再返回客户端。效果特别直观:一个300KB的JSON接口可能压缩成40KB,前端传输时间大幅下降。但要注意,对图片、视频这些本来已经压缩过的格式不要开gzip,压缩没效果还浪费CPU。
静态资源的处理也值得优化。如果你的项目里大量使用前端静态资源,可以考虑加个缓存头配置,甚至把静态资源交给前置的Web服务器处理,Tomcat只负责动态接口。Tomcat处理静态文件的效率没有专业Web服务器高,没必要硬扛。
6.3 Session机制和JSP预编译,避免线上突发变慢
Tomcat默认的Session存储在内存里,每个在线用户都占一份内存。用户量上来之后,Session内存占用会很可观。另外Session默认超时时间由conf/web.xml里配置的30分钟决定。并发高时不定时清理过期Session可能触发较大GC。
需要注意的是,Tomcat的Session管理器默认实现是StandardManager,它会把Session信息周期性地序列化到磁盘,防止重启后Session丢失。这个“周期性持久化”其实是很多线上卡顿的来源。如果你们并不强依赖Session持久化,可以考虑换个策略或者调整持久化频率。
JSP方面,首次访问某个JSP页面时Tomcat需要把它编译成Servlet,这个过程耗时可能达到数秒。如果线上用户访问量高且页面第一次加载特别慢,可以直接做JSP预编译,把编译过程挪到发布阶段。发布脚本里加一步预编译操作,用户体验会好很多。
7. 写在最后的几句实在话
上面这些内容,算是我把Tomcat从“会跑”到“跑得明白”过程中攒下来的经验都摊开讲了。每次帮同事排查Tomcat问题,我都有一个相同的感受:绝大多数故障都不是因为Tomcat本身多复杂,而是因为我们对它内部的工作机制想得太简单了。端口改了能跑,war包丢了能开,好像一切都顺利,直到某一天高并发一来,参数设得不对,一切都暴露了。
我个人最想强调的一点是:别把Tomcat当作一个“黑盒”。花半天时间认真读一遍server.xml,用jmeter或者压测工具跑一遍简单脚本,看看不同线程数下的响应时间和资源占用,比收藏一堆“最佳配置”都管用。参数这个东西,没有一劳永逸的答案,只有不断验证出来的最适值。
另外还有个小建议:生产环境的Tomcat不要裸奔,无论前面挂Nginx还是别的负载均衡方案,都把状态检查、访问日志、监控指标接上。出了问题,日志和数据会让你很快定位到Tomcat还是应用本身的问题,省去的排查时间远超配置这些监控的成本。
如果你的团队还在用“启动、部署、忘了管”的方式维护Tomcat,希望这篇能帮你把这些年的弯路都避开。