1. 版本选择与环境准备:被忽略的第一道坎
很多人学Tomcat时习惯直接从网上下一个最新版,解压后点startup.bat,看到黑窗口一闪而过或者浏览器打开localhost:8080出了一只猫,就以为学会了。但实际部署到测试环境才发现各种版本错乱:JDK版本不匹配、配置文件路径不对、日志乱码满天飞。这篇文章我会把Tomcat从安装到生产部署的完整链路拆开讲,每一步都给到可直接抄作业的配置和检查方法,适合刚接触Java Web的小白,也适合需要系统梳理一遍Tomcat运维细节的开发者。
1.1 JDK与Tomcat版本对应关系:为什么这是第一个学习重点
Tomcat本身是用Java写的,它启动的本质是启动一个Java进程,所以它和JDK版本之间的对应关系是硬约束。版本选错了,轻则服务起不来,重则跑起来之后出现各种奇怪的ClassNotFoundException或NoSuchMethodError。
这里我整理了一份常用对应关系,大家直接对照选型:
| Tomcat版本 | 支持Servlet规范 | 最低JDK版本 | 推荐JDK版本 |
|---|---|---|---|
| Tomcat 9.x | Servlet 4.0 | JDK 8 | JDK 8 / 11 |
| Tomcat 10.0.x | Servlet 5.0 | JDK 8 | JDK 11 |
| Tomcat 10.1.x | Servlet 5.0 | JDK 11 | JDK 17 |
| Tomcat 11.x | Servlet 6.0 | JDK 17 | JDK 17 / 21 |
特别提醒一件事:Tomcat 10之后包结构变了,原来javax.servlet.*全部迁移为jakarta.servlet.*。如果你手头的老项目还是基于javax.servlet写的,直接放到Tomcat 10里编译、部署都会报错。所以老项目的维护环境尽量稳定在Tomcat 9,新项目再用Tomcat 10或11。这个细节我见过太多人踩坑——项目换了个新Tomcat,结果应用起来后全是类找不到。
1.2 下载安装与JAVA_HOME、CATALINA_HOME配置细节
下载Tomcat时优先从Apache官网获取,下载zip压缩包(Windows)或tar.gz包(Linux)。解压完不要直接拷到带中文或空格的路径下,经典问题是路径带空格导致脚本解析失败。推荐Linux放到/opt/tomcat或/usr/local/tomcat这类纯英文路径,Windows放到D:\tools\tomcat这类简单目录。
安装Tomcat没有传统意义的installer,解压即用。但启动前必须确认两个环境变量:
JAVA_HOME:指向JDK安装根目录,必须存在,且$JAVA_HOME/bin/java能执行。Tomcat的启动脚本依赖它找到JVM。CATALINA_HOME:指向Tomcat解压根目录。这个变量用于存放Tomcat运行时产生的临时文件、日志、部署数据,起隔离作用。
验证方式在Linux下执行echo $JAVA_HOME、echo $CATALINA_HOME,Windows下执行echo %JAVA_HOME%。然后进入Tomcat的bin目录执行version.sh(Windows是version.bat),能打印出Server number和JVM Version就算环境通了。这里还要注意:CLASSPATH不需要手动设置,Tomcat启动脚本会自动处理,网上有些教程让用户手动加CLASSPATH,属于误导,加了反而容易出问题。
从这一节开始,后面所有路径均假设你的Tomcat根目录为$CATALINA_HOME。
2. 看懂Tomcat目录结构:问题排查一半靠它
新手遇到Tomcat报错时第一反应是百度搜报错信息,但很多问题光看报错信息是不够的,因为Tomcat的日志分散在多个目录、多个文件里。搞清楚目录结构和日志文件分工,你已经走在了正确排查路线上。
2.1 核心目录各司其职
先看一张目录全览,每个目录我都标注了实战中的用途:
bin:存放启停脚本,如startup.sh、shutdown.sh、catalina.sh。重点工作脚本是catalina.sh,startup.sh实质上调用它。conf:Tomcat所有配置文件,包括server.xml(核心)、web.xml(全局Servlet默认配置)、context.xml(全局Context配置)、tomcat-users.xml(管理后台用户)、logging.properties(日志格式和级别)。lib:存放Tomcat自身和所有Web应用共用类库,比如servlet-api.jar、el-api.jar。注意这里不要随意丢第三方jar,否则可能产生类加载冲突。logs:运行时日志,具体文件分工见第6章。temp:Tomcat运行时产生的临时文件,可以清空,但不要删除目录本身。webapps:Web应用发布目录,常见的ROOT、docs、examples、manager、host-manager五个默认应用都在这里。应用部署就是放war包或解压目录到这个目录下。work:JSP编译后的class和Java文件存放位置。JSP第一次访问时在这里生成Servlet源码和class,所以修改JSP后如果遇到页面没变化,优先清空work目录再重启。
2.2 logs与work目录的隐藏信息
work目录基本被很多教程忽略,但它实际承担了一个重要职责:JSP热更新的中间层。当修改一个JSP文件后,理论上Tomcat会检测文件时间戳变化并重新编译。但如果系统时间被手动改过,或者文件上的时间戳异常,Tomcat会误判“文件未变化”而继续用旧class,页面就一直是老的。此时第一件事不是清理浏览器缓存,而是删除work/Catalina对应项目的目录后重启,问题立刻解决。
logs目录下有几个文件容易弄混:catalina.out是Tomcat控制台输出的汇总;catalina.日期.log是主日志文件;localhost.日期.log是Web应用级别的日志;manager.日期.log和host-manager.日期.log分别对应两个管理应用的日志;localhost_access_log.日期.txt是访问日志。当代码里用System.out.println打印调试信息时,输出会进catalina.out;当应用抛出未被捕获的异常时,堆栈会进localhost.日期.log。搞混这两个文件,排查时会多走很多弯路。
3. 启动、验证与停止:别急着点startup
Tomcat启动看起来就是双击脚本,但里面的理念值得理清。前端开发可能觉得“启动服务”就是点一下,服务器运维角度则要考虑:前台还是后台?日志在哪?端口是否冲突?停止方式是否优雅?
3.1 启动脚本的前台后台差异
Windows下运行startup.bat会新建一个黑窗口,这个窗口本身就是Tomcat前台进程。窗口关了服务就没了。Linux下直接执行./startup.sh,会启动一个后台守护进程。但在某些精简系统或容器环境里,直接执行startup.sh可能会因为缺少写权限或适配问题失败。
我自己更推荐在调试阶段使用catalina.sh run,直接在前台运行,日志实时打印到终端。Ctrl+C可以直接停止进程,开发时反馈最快。需要后台常驻时再用./startup.sh,日志通过tail -f logs/catalina.out查看。注意不要用kill -9强杀Tomcat进程,Tomcat在正常停机时需要执行Context销毁、清理临时文件、释放端口等操作,强杀容易留下残留进程和端口占用,下次启动时就会出现“端口已在使用”。
生产环境如果要重启,标准操作顺序:
# 1. 优雅停机 ./bin/shutdown.sh # 2. 确认进程消失,不存在残留(超时未退出再考虑kill -9) ps -ef | grep java # 3. 启动 ./bin/startup.sh # 4. 观察启动日志,确认没有ERROR tail -100f logs/catalina.out3.2 端口冲突与乱码等启动期问题清单
端口被占用是启动期最常遇到的问题。默认Tomcat三个端口:8080(HTTP连接器)、8009(AJP连接器)、8005(关闭命令端口)。检查某一个端口是否被占用:
- Linux:
netstat -anp | grep 8080或ss -ltn | grep 8080 - Windows:
netstat -ano | findstr 8080,再配合tasklist | findstr PID
确认被哪个进程占用后,要么停掉那个进程,要么修改conf/server.xml里的port属性。修改后需要重启Tomcat才能生效。
启动时的中文乱码是另一个常见现象。Windows下Tomcat默认使用系统GBK编码,控制台打印时中文变成乱码。这通常不是Tomcat坏了,只是字符编码不对。最稳妥的做法是在conf/logging.properties中指定UTF-8:
java.util.logging.ConsoleHandler.encoding = UTF-8如果你是用catalina.sh run前台运行,IDE或终端的编码也要保持一致,否则仍然显示乱码。Linux下一般不会遇到这个问题,但容器镜像里如果没有显式设置LANG环境变量,某些系统和JDK版本下也会出现奇怪的编码问题。建议启动前统一设置JAVA_OPTS="-Dfile.encoding=UTF-8"。
4. 部署Web应用的三种方式与热部署技巧
部署Web应用是使用Tomcat的核心场景。实际项目里,“部署”至少有三种姿势,每种应用的场景不一样,很多人只会在IDEA里点一次Run,到生产环境就蒙了。
4.1 打包式部署:war包与目录拷贝
方式一:打war包上传到webapps目录。war包的命名决定了应用访问的上下文路径(Context Path)。比如上传myapp.war,启动后访问地址就是http://localhost:8080/myapp。如果想要应用作为根路径访问,文件名改为ROOT.war(并删除原ROOT目录)。Tomcat启动时会自动解压war包为同名目录,但更新war包时有一个坑:如果只覆盖war而不删除旧目录,解压操作可能会失败或新旧文件混杂。所以正确的步骤是:
# 1. 停止服务(或把应用移出webapps) ./bin/shutdown.sh # 2. 删除旧应用目录和旧war包 rm -rf webapps/myapp rm -f webapps/myapp.war # 3. 上传新war包 cp /upload_path/myapp.war webapps/ # 4. 启动服务,观察解压目录是否生成 ./bin/startup.sh方式二:直接拷贝已解压的目录到webapps下。适合内网快速发布或本地联调。这种方式不需要重启Tomcat就能被扫描到,但为了稳妥起见,建议还是完整重启一次。拷贝时注意目录权限,Linux下如果Tomcat运行用户是专用账号,需要保证该用户对目录有读写权限,否则JSP编译或日志写入会失败。
4.2 Context配置与自定义docBase
方式三是在conf/server.xml的<Host>节点下配置Context,指定docBase为磁盘上的某个路径,相当于把应用“挂”到虚拟主机上。典型配置:
<Host name="localhost" appBase="webapps" ...> <Context path="/myapp" docBase="/data/projects/myapp" reloadable="true"/> </Host>这里参数的意义:
path:访问路径,即浏览器URL中的上下文路径。docBase:应用实际所在磁盘路径,可以是相对路径,也可以是绝对路径。reloadable="true":当WEB-INF/classes或WEB-INF/lib下文件变化时自动重新加载。
这种方式的好处是应用代码和Tomcat目录解耦,不用频繁拷贝war包,适合开发环境和满足定制化部署要求的环境。但有两个注意点:第一,生产环境不建议开reloadable="true",它在类加载器层面频繁重建会引入性能损耗和内存泄漏风险;第二,如果目录不存在或权限不够,Tomcat启动不会报错,但访问时会404,排查时需要先确认路径可读。
路径还有一层:appBase和docBase的关系。appBase是主机的应用存放根目录,默认是webapps。docBase可以指向appBase目录之外,这时应用不会作为appBase中的自动部署目录出现。不要同时把应用放在webapps下又配一个Context指向同一份代码,那样会重复部署。
4.3 用manager做热部署的正确姿势
Tomcat自带manager应用,真正意义上支持热部署。先在conf/tomcat-users.xml里配置一个带manager-gui角色的用户:
<role rolename="manager-gui"/> <user username="admin" password="yourpassword" roles="manager-gui"/>然后访问http://localhost:8080/manager,登录后可以上传war包、部署目录、卸载应用。它的优点是免停服,比较适合中小团队在内网发布。但每个部署动作背后其实都会触发一次类加载器的重建,如果应用里用了大量静态变量或使用了某些本地资源(如文件句柄、数据库连接池),热部署后可能遇到“内存泄漏/句柄泄漏”相关问题。所以生产环境的发布我仍然建议走“停服替换再启动”的冷发布流程,manager更适合测试环境的快速迭代。
5. server.xml深度解析:端口、线程池与连接器调优
conf/server.xml是Tomcat最核心的配置文件,里面配置了Server、Service、Connector、Engine、Host、Context等核心组件。不懂这个文件,就只能停留在“能启动就行的层面”。理解它之后,你就能解释:为什么最大并发上不去,为什么某个端口连不上,为什么一个IP能跑多个域名。
5.1 主配置文件结构拆解
一个典型的简化版server.xml长这样:
<Server port="8005" shutdown="SHUTDOWN"> <Service name="Catalina"> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443"/> <Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/> <Engine name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Context path="" docBase="ROOT"/> </Host> </Engine> </Service> </Server>组件的层次关系可以理解成:一个Server实例就是一个完整的Tomcat进程;Service把若干个Connector和一个Engine组合起来,对外提供服务;Connector负责接待外部请求并转交给Engine,相当于前台接待;Engine内部再按Host(虚拟主机)分发到具体的Web应用。大多数情况一个Service已经够用,但多Service结构可以实现同一Tomcat进程内不同组件使用不同配置,比如一个Service暴露HTTP端口给前端,另一个Service只暴露给内部管理使用。
Server根节点的port="8005"和shutdown="SHUTDOWN"配合完成关闭Tomcat的机制:任何能访问这个端口的人,向它发送指定字符串即可触发关闭。默认配置等于对外裸奔,所以生产环境必须修改这个端口或限制访问来源。
5.2 Connector与Executor参数:哪些值得改
Connector的性能参数直接决定Tomcat的单机吞吐量。逐个参数讲明白,再给一套基础参考值:
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="400" minSpareThreads="50" prestartminSpareThreads="true"/> <Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" acceptCount="200" maxConnections="8192" compression="on" compressionMinSize="2048" noCompressionUserAgents="gozilla, traviata" compressableMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/json" URIEncoding="UTF-8"/>其中几个值得细讲的:
maxThreads:处理请求的最大线程数。默认200,线程多了有内存开销,少了并发不够。它不是越大越好——每线程大约占用1MB左右的栈空间,400个线程意味着额外400MB内存在线程栈上,还要考虑CPU核心数。如果机器是4核8G,建议先设300,再压测看线程使用率和响应时间。acceptCount:请求排队队列长度。当所有线程都在忙时,新进来的请求会进入队列。队列太长会导致用户等待时间过长,但立刻拒绝又会让用户看到Connection reset。一般和maxThreads配合调整,网络抖动或突发流量的场景下适当加大。maxConnections:连接数上限,超过后内核会按策略丢弃或拒绝。对HTTP/1.1来说,这个值通常可以比maxThreads大,因为连接会复用,有Keep-Alive的请求并不一直占着线程。compression:开启后,对compressableMimeType列出的内容做gzip压缩。静态资源如果由Nginx在更前面处理,Tomcat这边可以不开或只开API响应压缩,开启后CPU占用略增,带宽占用明显下降。URIEncoding:统一URI解析编码为UTF-8,解决GET请求中文乱码问题。
protocol参数值得单独说。Tomcat 8.5版本之后默认的protocol="HTTP/1.1"就是NIO模式,不需要写成org.apache.coyote.http11.Http11NioProtocol。Tomcat 9之后依然兼容BIO的显式写法,但官方已经不建议使用。NIO的最大意义是用少量线程处理大量连接,在高并发非大量长连接场景下,线程数可以开得更保守。
调整完server.xml后,重启后可以在catalina.out中看到一条类似"Initializing ProtocolHandler [http-nio-8080]"的日志,确认连接器已按新配置启动。
5.3 虚拟主机与域名访问
Host节点对应一个域名站点。默认只有localhost一个Host。如果你想在同一个Tomcat上跑两个域名,比如www.a.com和www.b.com,配置方式是再新增一个Host:
<Engine name="Catalina" defaultHost="www.a.com"> <Host name="www.a.com" appBase="webapps_a" unpackWARs="true" autoDeploy="false"/> <Host name="www.b.com" appBase="webapps_b" unpackWARs="true" autoDeploy="false"/> </Engine>defaultHost用于解析不了域名时的兜底。实际使用时还要注意:域名解析必须由DNS解析到这台服务器IP,Tomcat本身不做DNS解析,只按请求头中的Host字段来匹配Host name。如果访问时带端口号,比如http://www.a.com:8080/xxx,Host字段仍是www.a.com,只是Tomcat在HTTP连接器端口上进行筛选。
虚拟主机部署还经常配合appBase之外的路径使用。多个Host共用同一份代码但不同配置的场景,可以采用在各Host内配置Context指向同一个docBase,但要注意如果应用涉及全局静态变量,不同Host之间还是共享同一个ClassLoader,可能导致状态互相干扰。该问题一般出现在多租户部署的极端场景,常规项目不太会遇到,但值得知道。
6. 日志与故障排查:从入门到放弃的边缘
Tomcat真正考验人的不是功能,而是出了问题能不能快速定位。它的日志体系表面上简单,实际有四个文件容易混淆。不少人翻遍百度找不到报错位置,就是因为对着catalina.out猛看,却忽略了localhost.*.log里早就打印了堆栈。
6.1 各类日志文件能告诉你什么
我把日志文件和它们的侧重整理如下:
| 日志文件 | 代表内容 | 常见排查场景 |
|---|---|---|
catalina.<date>.log | Tomcat自身生命周期、连接器启动、部署信息 | 启动失败原因、端口绑定失败、部署状态 |
catalina.out | 控制台输出汇总(含System.out、System.err) | 调试输出、JVM参数验证、慢调用痕迹 |
localhost.<date>.log | SpringBoot置入的异常堆栈、未捕获异常 | 应用启动期Bean创建失败、Spring上下文异常 |
localhost_access_log.*.txt | HTTP访问请求日志 | 请求量统计、状态码排查、IP来源分析 |
manager.*.log | manager应用操作记录 | 热部署失败、身份验证失败 |
比如一个Spring Boot应用打成war部署到Tomcat后起不来,catalina.out里可能只有“Deployment is in progress”,真正的NoSuchBeanDefinitionException、ClassNotFoundException堆栈却打印在localhost.<date>.log。因为Spring的日志器使用的是应用类加载器的上下文,写到了应用日志文件里。正确做法是两个文件一起看,按时间戳比对,才能还原完整事件。
6.2 乱码、404、内存溢出的完整排查链路
问题一:页面请求参数出现中文乱码。
排查链路依次检查:
- 浏览器端编码:确认页面
Content-Type里带charset=utf-8。 - Tomcat连接器
URIEncoding:是否配置为UTF-8(见5.2)。 - 应用层编码过滤器:Spring的
CharacterEncodingFilter是否设置forceEncoding=true。 - 数据库连接字符集:MySQL连接串是否加
useUnicode=true&characterEncoding=utf8。
任何一个环节断了,都会出现乱码。特别提醒:URIEncoding只对URI中的参数生效,POST请求体可能被CharacterEncodingFilter覆盖。
问题二:404,但应用看起来部署了。
404的排查比很多人想的简单。按顺序查:
webapps下是否真的有应用目录或war包解压目录。- 访问路径是否拼对:
http://ip:8080/应用名/资源名。应用名要和目录名严格一致,不要以为/myapp一定对应myapp.war,目录叫myapp_web那路径就是/myapp_web。 - 检查
conf/server.xml中Context的path是否设置成了/或空串,导致应用覆盖了ROOT路径。 - 查看
WEB-INF/web.xml里的welcome-file配置,默认首页文件名必须存在且匹配。 - 检查应用是否真的启动成功:看
catalina.<date>.log里的Deployment of web application archive [myapp.war] has finished有没出现,缺少这行说明根本没部署完毕。
问题三:java.lang.OutOfMemoryError: Java heap space。
Tomcat默认-Xms和-Xmx是JVM按物理内存自动设置的,在容器和低配服务器上常常不够。遇到这类错误,先找到日志确认错误类型,再调整JVM启动参数。
Linux和Windows都通用的方法是修改bin/catalina.sh(或catalina.bat)里的JAVA_OPTS变量。在靠近文件顶部追加:
JAVA_OPTS="-Xms1024m -Xmx2048m -Dfile.encoding=UTF-8"其中,-Xms是JVM启动时堆内存初始大小,-Xmx是最大堆内存。两者设置成一样可以避免JVM运行时动态扩容造成的性能抖动。建议先确认服务器物理内存总量,堆内存上限不要超过物理内存的50%-60%,否则堆外内存(Metaspace、直接内存、线程栈)容易不足。另一个常见错误是java.lang.OutOfMemoryError: Metaspace,JDK8之后永久代由Metaspace代替,该错误需增加-XX:MaxMetaspaceSize。
诊断内存问题不能只靠调大内存,因为很多情况是应用存在泄漏。一旦发生频繁FGC,获取堆转储最优的方式是加-XX:+HeapDumpOnOutOfMemoryError参数,OOM时JVM自动生成java_pid*.hprof文件,之后可以用专业工具分析对象引用关系。线上无头绪硬调-Xmx是治标不治本的做法。
7. 安全加固与生产环境部署要点
网上大量Tomcat部署教程默认配置是“能跑就行”。一旦暴露到公网,默认配置漏洞比较多。这里给出一套可在短期内完成的基础加固清单。
7.1 默认配置的几种典型风险
第一,默认端口和默认管理页。8080非常容易被扫描器发现,docs、examples目录携带大量示例代码,信息泄露风险大,manager和host-manager如果开启且密码薄弱,极易被登录到管理后台。
第二,8005关闭端口。默认监听在所有网卡上,只要攻击者能访问到这个端口,向它发送SHUTDOWN字符串,Tomcat就直接关闭。这个机制在很多生产环境仍然全开着。
第三,AJP连接器8009端口。早期的Tomcat AJP连接器出现过反序列化漏洞,虽然说最新版本已修复,但如果业务上没有使用AJP协议(一般只有前端Apache HTTP Server配合mod_jk时才需要),没必要暴露8009端口。
7.2 最小化加固步骤
我整理过一套标准化操作,按顺序执行即可:
- 删除默认应用:清空
webapps下的ROOT、docs、examples,保留或不保留manager取决于是否用管理后台。内网测试环境可以留manager,公网环境建议全部删除。 - 修改8080端口为非默认端口,如
18080。若域名解析已明确,可直接修改为80端口配合Nginx使用。 - 删除8009端口对应的AJP Connector配置或显式注释掉。如果确实需要AJP,为它设置
address="127.0.0.1"只允许本机访问。 - 修改8005关闭端口:改端口后还需要在
Server节点配置address="127.0.0.1",或者直接删除Server的shutdown属性并外置监控脚本管理进程。 - 在
Connector上设置server="Apache Tomcat"或自定义伪装,避免版本号直接暴露。更彻底的方式是在应用层统一重写Server响应头。 - 以非root用户运行:创建专用账号
tomcat,把Tomcat目录的所有权交给它,使用su - tomcat -c './startup.sh'启动。这样即使Web应用被注入webshell,能控制的权限也有限。 - 关闭
autoDeploy="true"生产环境改为autoDeploy="false",防止攻击者利用上传漏洞写入应用目录后Tomcat自动加载。手动发布时再通过管理脚本操作。 - 限制
manager来源IP:在Host节点的Valve里配置RemoteAddrValve,只允许公司出口IP访问管理接口。
<Context path="/manager" docBase="manager" privileged="true" antiResourceLocking="false"> <Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="192.168.1.0/24,127.0.0.1"/> </Context>7.3 对外架构里Tomcat的典型位置
生产环境一般不会让Tomcat直接对外暴露80端口,而是采用“Nginx + Tomcat集群”的思路:Nginx监听80/443,负责静态资源、SSL终止、负载均衡和访问控制,动态请求转发给后端的多个Tomcat节点。
一个典型配置片段:
upstream tomcat_cluster { server 127.0.0.1:8080 weight=1 max_fails=3 fail_timeout=15s; server 127.0.0.1:18080 weight=1 max_fails=3 fail_timeout=15s; } server { listen 80; server_name www.example.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; } }如果应用本身需要用户会话保持,如HttpSession,Nginx采用ip_hash方式把同一IP固定到同一Tomcat节点,或者引入Redis Session共享。Tomcat官方提供Redisson这类Session Manager扩展方案,但对于新项目,更合适的做法是让应用退出本地Session,改用Token等无状态认证方式,从根本上规避Session共享问题。多实例部署的时候,Session本地模式就基本实现不了高可用需求。
8. 写在最后:几条比教程更值钱的运维经验
回头看整篇文章覆盖了很多内容,但真正在工作里帮我少踩坑的往往是几条小经验,最后一个主题再补齐这些散落点。
第一,用catalina.sh run排查,别用startup.sh排查。改完配置后想快速验证是否正常,用前台模式跑,Ctrl+C停掉,日志就在眼前。不再需要反复打开catalina.out。等确认没问题,再停掉改用startup.sh后台跑。
第二,发布前对conf/server.xml和conf/tomcat-users.xml的备份习惯比收藏任何教程都有价值。我遇到过某同事在发布脚本里把整个conf目录加密压低后覆盖,最终导致严重事故的例子。更好的做法是发布脚本里只下发差异文件,并维护一份发布前后的diff记录。Tomcat的配置不算复杂,但改动散落在多个文件,想回滚时没有变更是非常头疼的。
第三,别忽略JAVA_OPTS和CATALINA_OPTS的区别。CATALINA_OPTS只在启动Tomcat主进程时生效,JAVA_OPTS作用于所有Java相关子进程(包括停止脚本shutdown.sh)。像JMX端口、GC参数这种只管运行进程的配置放到CATALINA_OPTS里,停止脚本才不会被这些参数干扰。
第四,线上真需要加大内存时,先用jmap -heap <pid>看一下JVM当前堆占用。如果老年代持续在80%以上,调大内存只会延迟OOM的时间点,问题根源还是需要从代码侧找,比如连接池泄漏或集合缓存无限增长。Tomcat本身的设计再细腻,也弥补不了应用代码的缺陷。
最后再分享一个很实用的调试习惯:新装一个Tomcat、部署好应用后,立刻执行一遍“检测清单”:1)curl -i localhost:8080看Server头是否暴露版本;2)访问一个不存在的路径确认404页面长得像默认页还是有定制;3)查看logs/localhost_access_log确认请求都记录下来了。这三步花费2分钟,后续排障省下的时间可能是几小时。
Tomcat不是锦上添花的工具,它是Java Web服务的基础底座。把这套流程理顺,不管以后用不用Tomcat,Servlet容器的部署思路对你理解整个Java服务化架构都有直接帮助。