前段时间刚把一个老旧的Tomcat 8.5环境升级到了Tomcat 9.0.x,整个过程从评估、备份、部署到验证,前后折腾了差不多半天时间。中间踩了几个坑,也顺手整理了一套相对稳妥的升级流程。这篇东西就是把我这次Linux环境下的Tomcat版本升级经验完整记录下来,包括升级前怎么评估影响、备份哪些文件、配置怎么迁移、启动验证怎么做,以及碰到问题怎么回滚。不管你是要把Tomcat 7/8老环境升上去,还是想从Tomcat 9跨到10甚至11,这套流程和思路都可以直接拿来参考。
先说结论:Tomcat版本升级这件事本身不复杂,本质就是换一个软件版本再把配置和应用迁过去。但真正容易出问题的地方,其实全在升级前的准备和升级后的验证这两个环节。只要这两块做得足够细,升级过程反而没什么风险。下面我把整个流程拆开讲清楚。
1. 升级前的准备与决策
1.1 先搞清楚为什么要升级
动手之前,你必须先明确一个问题:这个升级到底是为了什么。我在实际工作中见过不少团队,一看某篇文章说某版本Tomcat有漏洞,就急着升级,结果升完之后应用跑不起来,又急急忙忙回滚,搞得大家都紧张。Tomcat版本升级的常见动机,其实就那么几类,你对照一下自己的情况就清楚了。
最常见的原因是安全漏洞。老版本的Tomcat比如7.x、8.0这些,官方早就停止维护了,不再发布安全补丁。网上公开的CVE漏洞信息越积越多,等安全扫描工具扫出来一堆高危项,这时候不升级都不行。这类升级通常比较被动,但理由充分,容易推动。第二种原因是对新规范和新技术的需求,比如应用要支持HTTP/2、要使用新的Servlet规范特性、或者要配合Spring Boot等框架的新版本。第三种是因为JDK环境的升级,服务器换了新版本的JDK,比如从JDK 8升到JDK 17,这个时候老版本的Tomcat可能就没法正常工作在这么新的JDK上了。
还有一种情况比较隐蔽,就是性能优化。Tomcat老版本和新版本在并发处理能力、连接器实现上还是有明显差异的,新版默认使用NIO2,在高并发场景下表现更好。如果你当前的Tomcat在高峰期频繁出现连接超时、线程堆积之类的问题,升级可能会顺带缓解一部分压力。
不管是哪种原因,升级之前我建议你先做一个简单的评估表,把下面几个问题的答案写下来:当前Tomcat版本是什么、目标是哪个版本、是跨大版本还是小版本升级、升级会影响哪些应用、业务上能否接受短暂停机。这个评估表不用很正式,但一定要有,因为它决定了你后面所有操作的时间窗口和风险容忍度。
提示:如果你只是从Tomcat 8.5.x升级到8.5.y这种补丁版本,过程会轻松很多,因为大版本内的升级基本只是修bug和补安全漏洞,配置格式几乎不变。真正的风险点在跨大版本升级,比如8到9,尤其是9到10。
1.2 版本选型与影响评估
确定要升级之后,第一步是选目标版本。我的建议很简单:不追求最新,追求稳定和兼容。Tomcat官方当前维护中的版本主要是9.x、10.1.x和11.x,每个大版本的支持策略不太一样。9.x是长期稳定版,也是目前生产环境用得最多的。10.x开始转向Jakarta EE 10规范,API包名从javax.变成了jakarta.。11.x是当前最新的,要求JDK 17及以上,主要面向新项目。
这里最关键的就是JDK版本的匹配问题,一定要先搞清楚。Tomcat 8.5和9.0可以运行在JDK 8及以上,10.0需要JDK 8+,10.1要求JDK 11+,11要求JDK 17+。如果你的服务器还是JDK 8,那就老老实实升到Tomcat 9.x最高的小版本,千万不要去碰10和11,否则运行的时候会直接报不支持的错误。反过来,如果应用要求JDK 17环境,那Tomcat 8.5可能就带不动了,也得同步考虑升级。
版本确定之后,还要做一个影响面评估。这个评估的核心就是搞清楚你的应用到底依赖了Tomcat的哪些东西。最常见的坑有两个:一个是应用代码里直接引用了javax.servlet、javax.websocket这类API,如果升到Tomcat 10以上,代码必须全部改成jakarta.*,工作量可不小。另一个是使用了第三方的库或框架,比如某些老的Filter、Listener组件,它们内部打包的Servlet API版本和Tomcat版本不匹配,升级后可能直接ClassNotFound。
我做影响评估时,一般会先在测试环境做一次完整升级和回归测试,观察应用日志有没有异常,再看代码里是否大量出现javax.*引用。如果只是自己公司内部的小系统,代码可控,这个检查几分钟就能完成。如果是用了大量第三方组件的系统,就得提前准备应用改造方案,甚至考虑暂缓升级。
实操心得:评估阶段不要只看Tomcat本身,还要把配套的环境变量、启动脚本、JDK路径、部署方式(是不是用systemd管理)这些都列出来。因为升级过程中你很可能需要同步调整这些配套项,提前列好可以避免到时候手忙脚乱。
2. 环境备份与系统检查
2.1 配置与应用的完整备份
升级前最重要的一步就是备份。这一步做好了,哪怕后面升级失败,你也能在几分钟内恢复原状。很多运维事故的根源就是没做备份或者备份不完全,这个小习惯真的值得刻在骨子里。
Tomcat的备份范围很容易确认,核心就是那几个目录和文件。conf目录里存放了所有配置文件,其中server.xml是连接器和Host配置,context.xml是全局上下文配置,web.xml是默认的Servlet映射和过滤器声明,tomcat-users.xml管理用户角色。如果你改了server.xml等页面代码或自定义了部分配置,那建议把整个conf目录完整备份。webapps目录存放着所有应用,既有解压后的目录也有war包。lib目录里除了Tomcat自带的jar包,还可能放了一些你在生产环境里自定义添加的驱动或依赖包。bin目录有启动脚本,特别是setenv.sh这个文件经常被用来设置JVM参数和环境变量,容易被人忽略。
备份操作本身很简单,用tar打包就行。我习惯的做法是在Tomcat的上级目录执行打包命令,这样能保证打包后目录结构是正确的。比如Tomcat安装在/opt/tomcat,那就在/opt目录下执行打包,把整个tomcat目录打成一个带日期标记的tar包。
cd /opt tar -czf tomcat-backup-$(date +%Y%m%d%H%M).tar.gz tomcat/这个命令会把整个Tomcat安装目录都备份下来,包括conf、webapps、lib、bin,还有logs和temp这些运行目录。虽然logs和temp没啥必要,但多备份一点总比漏掉重要的好。打包速度一般很快,一个几个G大小的应用目录也就是一两分钟的事。
除了文件备份,数据库如果应用依赖的话也需要评估是否需要备份。不过一般升级Tomcat不会动数据库,只要数据库连接串配置没变,就不用额外备份数据库。如果你们内部有配置中心统一管理数据库连接信息,那Tomcat这边就更轻松了,只需要确认配置文件里有正确的引用即可。
注意:备份文件一定不要放在Tomcat目录内部,更不能放在webapps下。否则打包的时候把备份又包进去了,而且占空间。我习惯把备份放到独立的一个备份目录,比如/backup。
2.2 运行环境基线检查
备份做完之后,别急着下载新版本,先把当前环境的基线信息采集下来。这些信息在升级后对比问题时价值非常大,尤其是当新版本启动异常时,你可以通过这些基线快速定位是配置变了还是环境变了。
首先要记录当前Tomcat的版本号。进入Tomcat的bin目录,执行version.sh脚本就能看到版本信息,或者直接看catalina.out启动日志里的第一行。同时要确认JDK版本和Java路径,执行java -version,再看catalina.sh里的JAVA_HOME环境变量,这两个信息必须和Tomcat版本匹配,升级后也要继续保持一致。
端口信息也很关键。通过ss命令查看当前端口监听情况,特别是8080、8005、8443这些常用端口,还要留意AJP协议的8009端口有没有被用到。升级后如果端口被其他进程占了,启动必报错,提前记录好端口清单能帮你少走很多弯路。
服务管理方式必须先确认。你是通过systemd管理的服务,还是直接在catalina.sh脚本里的?这个直接决定了升级后的启动方式。如果原来用systemd,但升级时没更新unit文件,服务肯定起不来。
systemctl status tomcat这条命令能看到服务的启动命令、状态和PID,信息很全。如果是手工启动的方式,一定要记住原来启动Tomcat的用户和权限,最好用ps命令看一下当前Tomcat进程是以哪个用户运行的。
还有一个经常被忽略的点:环境变量。Tomcat启动时依赖CATALINA_HOME和CATALINA_BASE这两个环境变量,升级后这两个变量的指向也要跟着调整。加一个“tomcat环境变量信息”容易遗漏的项就是JAVA_OPTS里配置的JVM参数,比如Xmx、MaxMetaspaceSize等,升级后要用同样的参数跑。建议把当前进程的完整启动命令行记录一下,用ps的aux参数就能看到。
实操心得:把这些基线信息记在一个临时文档里,不需要多规范,自己能看懂就行。升级过程一旦出现奇怪的问题,回头翻这个文档,往往比上网搜索更快找到答案。
3. 新版本部署与配置迁移
3.1 新版本下载与目录规划
前期准备做完,进入实操阶段。新版本Tomcat的下载地址官网就能找到,选择合适的大版本下的最新小版本就行。下载的时候建议用wget或curl直接拉取.tar.gz包,不要下载zip格式,Linux环境用zip还得先装unzip工具,没必要多此一举。
下载下来的包,强烈建议做一下校验。官网上每个安装包都会提供SHA512校验和,你下载后用sha512sum命令核对一下。别嫌这一步麻烦,Tomcat作为分发广泛的中间件,历史上确实出现过第三方渠道被篡改的事件,校验一下也就是一条命令的事,安全习惯是从小处养成的。
wget https://example.com/download/tomcat-9.0.xx.tar.gz sha512sum apache-tomcat-9.0.xx.tar.gz至于安装路径,我的建议是不要覆盖旧目录,而是用新路径部署,比如把新版本解压到/opt/tomcat-9.0.xx目录,然后再通过软链接来切换。这样做的好处太多了,最直接的就是回滚极其方便,只需要改一下软链接的指向就可以。就算出了大问题,旧版本的完整目录还在原处,不会被动过一根毫毛。
部署时还需要关注目录归属和权限。Tomcat不能用root用户直接跑,一方面是不安全,另一方面有些版本的启动脚本会明确拒绝root运行。你需要新建一个系统用户,或者沿用原来已有的tomcat用户,然后执行chown把新解压目录的所有权交给这个用户。
useradd -r -s /sbin/nologin tomcat chown -R tomcat:tomcat /opt/tomcat-9.0.xx目录规划还有一个细节值得注意,就是CATALINA_HOME和CATALINA_BASE的划分。老项目可能没有单独设置CATALINA_BASE,所有东西都放在同一个目录下。如果你想保持简洁,一般直接使用默认CATALINA_HOME即可。
3.2 核心配置迁移与关键差异适配
新版本部署好后,核心工作就是把旧配置迁移过来。这一步是整个升级过程中最容易出问题的地方,也是需要你集中精力的环节。
先处理conf目录下的配置文件。最省事也相对稳妥的方式,是把旧版本的conf/server.xml、context.xml、web.xml等文件直接复制到新版本的conf目录。因为这些文件里保存了你对连接器端口、虚拟主机、数据源等内容的核心配置,重新手写一遍又费时又容易出错。拷贝前建议先diff一下新旧版本的默认配置差别,看看新版有没有增加新的配置项或调整了默认值。特别是server.xml里如果引入了新的Listener或Executor配置,你直接覆盖后可能丢掉了新版的优化项。
diff /opt/tomcat/conf/server.xml /opt/tomcat-9.0.xx/conf/server.xml特别要提醒的是conf/context.xml里的数据源配置。如果你的应用用的是JNDI数据源,这段配置在server.xml的GlobalNamingResources或context.xml里的Resource声明里。拷贝的时候要确认jdbc驱动jar是否还在新版本的lib目录里,很多升级后应用启动失败的原因就是驱动缺失。
然后就是javax到jakarta的迁移问题。如果你升级的是Tomcat 9或更高版本,应用还是老代码的话,这块就是最大的坑。这么说吧,Tomcat 10开始,Servlet、JSP、WebSocket等API的包名从javax.servlet.变到了jakarta.servlet.。如果你的应用代码和依赖库里全是import javax.servlet,直接丢到Tomcat 10上面运行,编译期不会报错因为运行时才加载,启动时直接ClassNotFoundException。
面对这个情况,有三个常见的选择。如果应用代码量大又不方便改造,最简单的方案是把目标版本定为Tomcat 9.x,这是支持javax.*包名的最后一个主流大版本。如果一定要上Tomcat 10以上,那就得对应用做一次代码改造,把所有javax.servlet相关的import替换成jakarta.servlet,同时还要检查pom.xml或build.gradle里的依赖坐标。如果你用的是Maven,可以利用一些自动迁移工具做批量替换,然后手工处理无法自动转换的部分。另外还有一条路子是加一个兼容层,让人替Tomcat做包名转换,不过这种方案我见过的不多,除非内部自己维护过类似组件,否则不建议在升级路上依赖它。
提示:判断你的应用用没用javax.*,最直接的办法是搜代码。在项目根目录执行grep -r "javax.servlet" src/,如果搜索出来很多命中,那就说明升级到Tomcat 10以上需要不小的改造量。
配置文件迁移还有一个容易被忽视的地方是web.xml的版本声明。老项目里web.xml会引用javax命名空间的schema,比如xsi:schemaLocation里带javax.servlet的URL,这个在Tomcat 10以上会直接解析失败。检查并修改web.xml的头部声明,改成Jakarta EE的命名空间。我做配置迁移的顺序是:先拷贝全部配置文件,再逐一对server.xml、context.xml、web.xml做diff,把差异和注意点整理出来,最后才启动应用。
3.3 启动验证与服务上线
配置弄完之后,就是启动验证的环节。这一步同样有讲究,不建议直接用startup.sh后台启动然后就不管了,那样出了问题连现场都看不到。我推荐的启动方式是先用前台模式跑一下,把所有启动日志直接输出到终端,有问题能第一时间看到堆栈。
$CATALINA_HOME/bin/catalina.sh run看到日志完整打印出Server startup信息之后,再按Ctrl+C停掉,改用正常的后台启动方式。这么做看起来多一步,但对于排查启动问题来说效率高太多了,尤其是配置文件有错误时,前台启动会直接打印报错,你在后台模式的日志里翻半天还不一定找到。
启动之后要看几个关键指标。第一是端口监听,用ss -lntp看一下8080等端口有没有正常监听。第二是进程状态,确认Tomcat进程在运行且用户正确。第三是日志里的关键信息,比如有没有出现SEVERE级别的报错,有没有部署失败的上下文。这些检查都通过之后,再开始完整的应用功能验证。
功能验证不能只看首页能不能打开。我一般会列一个验证清单,分成几个层面:先验证静态资源,比如HTML、CSS、JS这些能不能正常访问,这主要看webapps部署路径和默认Servlet是否正常。再验证动态接口,打几个业务接口确认应用逻辑没有因为Tomcat版本变化而出问题,重点检查依赖Servlet API的代码,像上传文件、Session操作、WebSocket连接这些。最后验证与外部系统的集成,比如数据库连接、消息队列、缓存服务这些,确保服务器间连通性没有因为环境变化受影响。
如果一切正常,最后一步是把服务固定下来。如果你原来用的是systemd管理,就在/etc/systemd/system/目录下维护好对应的unit文件。里面的ExecStart路径要更新成新版本的bin/catalina.sh。如果原来用的是init.d的方式,脚本路径也要做相应调整。systemd的unit文件里有一个小细节,如果是用catalina.sh run在前台运行,那么service类型建议写成simple。如果你还在用startup.sh的旧方式,systemd和它之间的配合会很不自然,容易出意外。
[Unit] Description=Apache Tomcat After=network.target [Service] Type=simple User=tomcat Group=tomcat Environment=JAVA_HOME=/usr/lib/jvm/java-1.8.0 Environment=CATALINA_HOME=/opt/tomcat-9.0.xx ExecStart=/opt/tomcat-9.0.xx/bin/catalina.sh run ExecStop=/opt/tomcat-9.0.xx/bin/catalina.sh stop Restart=on-failure [Install] WantedBy=multi-user.target看到ExecStop这个配置有人会问,用catalina.sh stop来关停Tomcat,那没跑的时候它会不会报错。实际定义就是这样,系统在stop阶段会调用这个脚本,如果服务已经停了,脚本自己会输出对应的提示并正常退出,不会对systemd状态产生负面影响。另外要记住修改完unit文件后执行systemctl daemon-reload代码重载一下。这样才能让新的路径配置生效。
注意:JVM参数不要写在catalina.sh里,更不要直接去改这个脚本。正确做法是在bin目录下创建setenv.sh文件,把JAVA_OPTS的配置放在里面。这样即使后续升级Tomcat版本,这个文件也独立于默认脚本之外,迁移配置的时候一份拷贝就能带走,不会因为覆盖catalina.sh导致参数丢失。
4. 常见故障排查与回滚方案
4.1 典型故障速查表
升级过程中遇到问题太正常了,关键是要有清晰的排查思路。我把自己遇到过的和身边同事踩过的典型故障整理了一张表,基本覆盖了Tomcat升级后最常见的几类问题。
| 故障现象 | 常见原因 | 排查思路 |
|---|---|---|
| 启动报Address already in use | 端口被其他进程占用 | ss -lntp查端口占用,确认旧Tomcat是否没停干净 |
| 启动后立刻退出,无明确日志 | conf配置解析失败 | 用catalina.sh run前台启动,直接看异常输出 |
| 应用部署失败Context启动失败 | web.xml或类加载异常 | 查看logs/localhost.日期.log里的详细报错 |
| ClassNotFoundException: javax.servlet | 应用还在用javax包 | 确认代码改用jakarta,或将目标版本改为Tomcat 9 |
| 应用启动成功但404 | webapps路径或应用解压失败 | 检查webapps下目录结构,确认war包格式 |
| 中文乱码 | URIEncoding或字符集配置问题 | 检查server.xml里的Connector配置,加上URIEncoding="UTF-8" |
| 内存溢出OutOfMemoryError | JVM参数不匹配 | 检查setenv.sh里的Xmx配置,必要时调大堆内存 |
| 访问特别慢 | JSP预编译未生效或线程池参数差异 | 对比新旧配置的Executor参数,确认NIO2连接器生效 |
这些故障里,前两类出现的频率最高。说实话,绝大多数启动失败都是因为端口被占或者配置状态不匹配,而这些在最开始的前台启动验证环节就能暴露出来。如果用的是catalina.sh run启动,启动失败时异常直接打到终端,定位问题会快捷很多。
还有一个容易忽略的场景是应用自身打印的日志和Tomcat日志混在一起。有时候tomcat正常启动了但应用内部报错,这类问题不会导致Tomcat退出,但功能上是不可用的。如果你发现Tomcat起来了、端口也监听了、curl访问却报500,就要重点查应用的日志文件和logs目录下的localhost.yyyy-MM-dd.log文件,通常报错会在这里出现。
提示:升级过程中遇到和旧版本完全一致的配置在新版本用不了的情况,请不要怀疑自己改错了,很可能是新版本对某个配置项的默认值或语法做了调整。先看官方变更记录,再做针对性调整,不要盲目回退到旧版本方式。
4.2 回滚机制与演练
最后说回滚。即使前面所有的检查和验证都做了,你还是要有随时能回到旧版本的能力,这是运维操作的基本素质。之所以一开始就强调用新目录而不是覆盖旧目录,核心目的就是给回滚留一条退路。
回滚操作本质上其实就是四个步骤。先停掉新版本的服务,用systemctl stop或者catalina.sh stop。然后把软链接或环境变量的指向改回旧版本目录。再启动旧版本的服务。最后验证旧版本服务正常,页面能访问、日志正常输出。整个过程如果事先演练过,熟练的话几分钟就能完成。
不过回滚有几个容易忽略的细节。如果升级过程中应用数据库表结构做过变更,回滚到旧版本后应用逻辑可能和新数据结构对不上,这种情况的复杂性已经超出了单纯的Tomcat升级。所以我要提醒一点:升级窗口期内尽量不要同时做数据库变更,否则出了问题你分不清是哪个环节引起的。
会话丢失的问题是回滚时一定会碰到的。升级过程中用户的登录会话会全部失效,回滚也一样,因为Session本身在Tomcat内存里。如果你所在的项目对会话连续性要求高,可以考虑升级窗口选在业务低峰期,或者提前配置Session持久化方案。不过多数内部系统其实不太依赖这个,在维护公告里写清楚让用户重新登录一次就能接受。
回滚是否成功,判断标准一定要预先定好。我的标准很简单:旧版本服务能正常启动、端口正常监听、关键业务页面能访问、应用日志持续输出没有报错。这里需要注意多观察一段时间,有些问题不是启动瞬间就暴露的,可能是在第一次请求进来之后才触发。所以说回滚不只是把服务切回去就完事了,还要花一小段时间观察运行状态。
我的体会
这次Tomcat版本升级做下来,我的感觉是:技术上没有太多高壁垒的操作,真正考验人的是流程的细致程度和遇事不慌的心态。升级前多花半小时把配置、版本对应关系这些底层信息梳理清楚,比出问题后抓瞎几个小时划算太多。几个印象最深的小细节:备份一定要完整足够独立、新目录代替老目录才能给回滚留余地、配置文件用diff一处一处核对、启动验证先前台跑一次再后台运行。这些建议不管你是新手还是老手,下次升级前都可以试试。如果你们的Tomcat还趴在老版本上,希望这篇流程能帮你少走几条弯路。