Tomcat 部署与调优全攻略:从目录结构到性能排查与安全加固
TomcatJava Web 开发者绕不开的老伙计。每次给新手讲部署翻来覆去都是那几个问题为什么启动脚本报错为什么 war 包放上去没反应为什么端口访问不了为什么感觉没跑多久就卡了这篇关于 Tomcat 使用教程的内容我不打算罗列官网文档的翻译而是把从下载安装、目录结构、核心配置、应用部署到参数调优、日志排错、安全加固这些环节挨个拆开讲清楚。这篇文章适合谁刚学 Java Web 想搞懂容器原理的学生刚入职要接手公司后端项目的新人以及需要自行维护测试环境的开发。如果你已经玩了几年 Tomcat也可以看看里面的参数取舍和排错思路多少能有些参考价值。1. 先弄清楚 Tomcat 到底干了什么活1.1 它本质上是个 Servlet 容器Tomcat 的官方定位是 Servlet 容器加 HTTP 服务器。换句话说它按照 Java Servlet 规范来管理 Servlet 的生命周期把浏览器发来的 HTTP 请求解析成 Java 对象再交给你的业务代码去处理。一个请求从浏览器出发经过 TCP 连接、HTTP 报文解析、请求头处理找到对应的 Servlet调用service()方法再到返回响应这整条链路就是 Tomcat 的主战场。很多人把它当成简单的“Web 服务器”其实静态文件处理只是它顺带的功能真正核心价值在于对动态请求的管理、线程池调度、Session 追踪以及对applicationContext和Listener等组件的统一装配。我见过不少新手问那我直接用 Nginx 不就行了确实Nginx 处理静态文件和并发连接更高效但 Java 应用跑的是 Servlet 规范必须有这样一个容器把请求翻译给 JVM 里的代码。所以实际部署中常见的组合是 Nginx 在顶层拦静态和转发Tomcat 在后面跑应用两者各干各擅长的。1.2 三个最容易踩的认知误区第一个误区Tomcat 的版本号越大越新越牛不完全对。Tomcat 版本和 Servlet 规范、JDK 版本是绑定关系的比如 Tomcat 10 开始包名从javax.*变成了jakarta.*老应用不改造包名根本跑不起来。我遇到过一次把老项目直接丢进 Tomcat 10 的结果一大片ClassNotFoundException排查了半天才发现是包名命名空间变了折腾一整个下午。第二个误区Tomcat 能自动加载所有 Java 类不对。它有自己的类加载体系lib目录下的 jar 由公共类加载器加载webapps/应用名/WEB-INF/lib下的 jar 由 Web 应用类加载器加载。新旧 jar 冲突的时候那个烦人的NoClassDefFoundError和ClassCastException基本都是这么来的。第三个误区把 war 包丢进webapps就算部署完成实际上 Tomcat 对这个 war 包有一系列处理步骤解析web.xml、读取注解、实例化Context容器、加载 Filter 和 Listener。任何一个环节出错应用都不会启动成功。新手看到的“Tomcat 启动成功了”往往只是容器起来了应用并没有起来。2. 环境准备从下载到成功跑起来2.1 版本选择很关键先选对版本再动手否则后面全白搭。我整理过常用的对应关系Tomcat 版本Servlet 规范最低 JDK 版本注意事项Tomcat 8.53.1JDK 7老项目兼容性好但官方已不推荐新项目使用Tomcat 9.04.0JDK 8目前接受度最高javax.*命名空间稳定省心Tomcat 10.16.0JDK 11使用jakarta.*命名空间老项目需要改包名Tomcat 116.1JDK 17更新适合全新项目尝试如果只是平时学习或快速搭测试环境我建议直接用 Tomcat 9JDK 8 或 11 都行兼容性最稳。生产环境要结合项目本身使用的框架版本去选比如老项目用了大量javax.servlet的代码硬升 Tomcat 10 就要承担改包名的成本不划算。下载的时候注意区分zip包和 Windows 的exe安装包。Linux 环境下直接解压tar.gz包就行Windows 下老老实地解压 zip 后配置环境变量即可不建议用安装版省得它自动写一堆系统服务后面想卸载都麻烦。2.2 目录结构必须心中有数解压出来以后Tomcat 根目录下有几个关键目录我一个个说bin存放启动和关闭脚本。Linux 下是startup.sh、shutdown.shWindows 下是startup.bat、shutdown.bat。这里的脚本只是封装了更底层命令真正的启动逻辑在catalina.sh/catalina.bat里。conf配置文件所在。server.xml是核心管理端口、连接器、虚拟主机web.xml是全局 Servlet 配置tomcat-users.xml管用户角色logging.properties控制日志格式。lib存放 Tomcat 自身和公共依赖的 jar 包。这里特别提醒一句千万不要为了“省事”把业务用的 mysql 驱动 jar 直接丢进lib会污染公共类加载器。logs日志输出目录。catalina.out是主输出文件localhost.log记录应用启动和初始化细节。webapps默认的部署目录。war 包丢进来后Tomcat 会自动解压并部署。workJSP 翻译成 Java、Servlet 编译后的 class 文件存放位置。有时候改完 JSP 不生效清一下这个目录经常能“治好”老毛病。temp临时文件目录一般不用关心。理解这些目录遇到问题的时候才能快速定位。比如老有人问为什么日志不完整其实 stdout 输出到catalina.out而 Tomcat 自己的框架日志又走java.util.logging两者是分开的。2.3 环境变量和首次启动Linux 环境先确认 JDK 装好了java -version如果在/usr/local/java下装了 JDK建议在/etc/profile里加入export JAVA_HOME/usr/local/java/jdk-17 export PATH$JAVA_HOME/bin:$PATH然后启动cd /usr/local/tomcat/bin ./startup.sh看到Tomcat started字样不代表一切正常关键是去logs/catalina.out翻一下最近的启动片段确认Server startup in [xxx] milliseconds。浏览器访问http://服务器IP:8080能看到默认首页就说明成功了。如果访问不了第一反应不要是“防火墙”先确认 Tomcat 进程还活着然后在本机执行curl localhost:8080再考虑端口是否被服务监听。八成新手的“访问不了”都是进程已经退了或者端口没被正确识别。Windows 环境我多说一句startup.bat启动的时候会打开一个命令行窗口那个窗口别关关了 Tomcat 就关了。想后台跑可以用service.bat install注册成系统服务。3. 核心配置server.xml 从看懂到改造3.1 三个端口分别干什么打开conf/server.xml新手常被一大堆标签搞晕其实最关键的是三个端口配置端口项默认值作用Server port8005用于接收关闭指令监听SHUTDOWN命令Connector port8080HTTP 访问端口浏览器请求就从这个端口进来AJP port8009用于和其他 Web 服务器如 Nginx、Apache通过 AJP 协议通信我第一次看这个文件的时候也奇怪为什么会有个 8005 专门接收关闭命令因为 Tomcat 的shutdown.sh其实是向这个端口发送指定字符串来触发关闭流程的所以如果你改了默认 shutdown 口令还沿用默认的SHUTDOWN那就等于给本机能连这个端口的任何人都留了个后门。生产环境即使本机部署要果断把8005端口的对外访问收紧或者干脆把shutdown指令改成随机字符串。Connector 端口是最常改的比如 8080 被其他程序占了改一个 8081 即可。改了之后别忘记防火墙放行不然配置再对也访问不了。3.2 Connector 参数别急着照抄server.xml里默认的 HTTP Connector 大概长这个样Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 /参数很少但真正上线之前需要补充的远不止这些。我之前压测的时候总结过一套比较合用的配置Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxThreads400 acceptCount200 maxConnections10000 minSpareThreads50 URIEncodingUTF-8 compressionon compressionMinSize2048 compressibleMimeTypetext/html,text/xml,text/plain,text/css,text/javascript,application/javascript /这里每个参数背后都有原因挑几个重点讲maxThreads能同时处理请求的最大工作线程数。大不代表好线程过多会带来上下文切换开销我一般建议从 200 起步用压测数据反推调整。acceptCount当处理线程全忙时请求可以进入的等待队列长度类似饭馆满了以后让客人在外面排队的位置。如果队列也满了新请求直接被拒客户端会收到Connection refused。maxConnectionsTomcat 同时保持连接的最大数量与线程数不是一个概念TCP 连接可以多但能同时处理的仍受限于线程。connectionTimeout等待客户端发送请求的毫秒数。设太短客户端稍慢就断设太长大量空闲连接占着资源不释放。URIEncoding必须设为UTF-8否则 GET 请求的中文参数大概率乱码。很多人看网上配置直接复制一个高线程数结果压测发现还不如默认的。这类参数本质是“瓶颈转移”线程太多、队列太长CPU 先扛不住调太小并发一上来就拒绝服务。没有压测数据支撑的调优都是赌后面我会讲怎么验证。3.3 Host 和 Context路径映射的底层逻辑Server.xml中还有一个重要结构是Engine下的Host默认namelocalhostappBasewebapps。意思是所有丢在webapps目录下的应用目录或 war 包都会自动挂到这个虚拟主机下。如果想让一个应用根路径访问也就是直接访问http://IP:8080/那么应用目录或 war 名必须叫ROOT。这就是为什么很多人的应用部署在myapp目录下地址得带/myapp才能访问很不习惯。要改根路径要么把应用重命名成ROOT要么用以下方案Host namelocalhost appBasewebapps unpackWARstrue autoDeploytrue Context path docBase/data/apps/order-system reloadabletrue/ /Hostpath表示根路径docBase指向外部某个目录。这样做的好处是应用代码可以放在webapps之外部署、维护、备份都更灵活。autoDeploy和unpackWARs是开发环境比较重要的开关。unpackWARstrue表示 war 包自动解压成目录生产环境一般用它autoDeploytrue表示只要有新的 war 包出现就自动部署开发时方便但生产环境我会关掉它防止往目录里丢个半成品包就把线上服务搞挂了。3.4 改完配置怎么确认改任何配置文件后重启方式要看你的环境。server.xml这类核心文件改了必须重启 Tomcat 才生效靠autoDeploy不会自动加载这是无数人踩过的坑。重启前先保存备份养成习惯cp /usr/local/tomcat/conf/server.xml /usr/local/tomcat/conf/server.xml.bak /usr/local/tomcat/bin/shutdown.sh /usr/local/tomcat/bin/startup.sh tail -f /usr/local/tomcat/logs/catalina.out日志中出现INFO [main] org.apache.coyote.AbstractProtocol.start ...且没有SEVERE级别异常才说明新配置被加载了。如果改动后应用一直报连接池初始化失败先去conf/server.xml里看自己是不是把Context标签改错了位置这类语法错误日志不见得会明确标出来。4. 应用部署war 包、目录部署和更新套路4.1 war 包部署的完整流程war 包是 Web 应用的打包格式本质就是个 zip里面包含 WEB-INF 目录、编译好的 class、静态资源等。打包方式可以用cd 项目根目录 jar -cvf myapp.war *或直接用 Mavenmvn clean package然后把myapp.war复制到webapps下cp myapp.war /usr/local/tomcat/webapps/正常情况下几秒后 Tomcat 会自动解压生成myapp目录并完成部署。如果配置了unpackWARsfalse那它就会直接从 war 包运行不展开。这里有个值得留意的点Tomcat 判断是否重新部署是根据文件时间戳。如果目录里已经存在myapp但 war 包更新了Tomcat 通常也会检测到并重新部署。但有时因为文件权限问题覆盖不成功就会出现“明明放了新包但代码没变”的怪事。稳妥做法是先删掉旧目录和旧 war再放新包。4.2 用目录映射实现开发调试开发环境下频繁重新打包 war 再部署效率太低。更推荐的做法是让 Tomcat 直接指向开发项目的编译输出目录。比如项目在本地编译后输出到/data/projects/order-system/target/order-system就在server.xml的 Host 下加Context path/order docBase/data/projects/order-system/target/order-system reloadabletrue/配合 IDEA 等工具的热部署功能改完 Java 代码后编译Tomcat 检测到 class 更新就会自动重载比手动重启快得多。reloadabletrue开启后Tomcat 会监控WEB-INF/classes和WEB-INF/lib的变化但代价是内存中应用上下文要反复重建生产环境不要开这个开关否则线上每隔几分钟自己重启一次内存不稳定且日志满屏重载信息。4.3 生产环境的更新和回滚上线时最怕的就是“改崩了”。我比较习惯用下面的流程备份当前版本目录和 war 包cd /usr/local/tomcat/webapps mv myapp myapp_$(date %Y%m%d%H%M)_bak mv myapp.war myapp_$(date %Y%m%d%H%M)_bak.war放入新包并清日志cp /tmp/upload/myapp.war . rm -rf /usr/local/tomcat/logs/*重启/usr/local/tomcat/bin/shutdown.sh /usr/local/tomcat/bin/startup.sh tail -f /usr/local/tomcat/logs/catalina.out确认启动成功并做基本功能验证发现有问题直接回滚mv myapp_xxx_bak myapp /usr/local/tomcat/bin/shutdown.sh /usr/local/tomcat/bin/startup.sh这套流程看着简单胜在每一步都能回溯。我见过某些团队直接覆盖 war 包失败了连上一版文件都没了只能现场改代码重新打包那种状态谁碰谁头大。5. 性能与内存让 Tomcat 活得更久、跑得更稳5.1 JVM 参数加到哪怎么加Tomcat 本身跑在 JVM 上所以内存调优等于调整 JVM 启动参数。Linux 下的启动入口是bin/catalina.sh顶部会有JAVA_OPTS变量可以用下面方式设置export JAVA_OPTS-server -Xms1024m -Xmx1024m -XX:MaxMetaspaceSize256m -XX:UseG1GC -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/tomcat_heapdump.hprof这里讲下每个参数的意义-Xms和-Xmx初始堆和最大堆。生产环境我一般直接设为相同值避免 JVM 运行中反复申请内存、收缩带来的性能抖动。-XX:MaxMetaspaceSize元空间上限。默认可能很高在 JDK 8 以后无限使用物理内存但部署很多应用后容易因元空间膨胀吃满内存。根据应用情况设成 256m ~ 512m 较稳妥。-XX:UseG1GCJDK 9 以后默认就是 G1但对老项目可能还在用 Parallel GC开启 G1 对延迟更友好。-XX:HeapDumpOnOutOfMemoryErrorOOM 时自动导出堆快照这个非常重要。没有这个参数遇到内存溢出只能靠猜。-Xlog:gc*JDK 11 写法GC 日志也能记录便于后续分析。老版本 JDK 8 则用-XX:PrintGCDetails -XX:PrintGCDateStamps加文件路径。监控内存用jstat特别顺手jstat -gc pid 1000 10一行就能看到各个区的大小变化、GC 次数和耗时。如果 Full GC 频繁且耗时高多半是堆大小顶不住或者代码里有明显的内存泄漏单纯加堆内存只是延后崩溃。5.2 线程与并发参数之间的平衡关于并发我前面提到过maxThreads、acceptCount、maxConnections三个参数互相制约。举个例子场景是单机 4 核 8G 内存部署一个订单查询服务平均响应时间 200ms想要支撑 2000 QPS。理论需要的线程数大致等于QPS 乘以平均响应时间也就是2000 * 0.2 400。这时候线程池就设 400 比较合适队列不要开太长acceptCount200就差不多如果队列过长请求等太久客户端早就超时了。但实际压测结果往往和理论值有偏差。我调整参数的决策路径是先用默认参数启动。用压测工具简单点用ab复杂些用wrk或JMeter逐步加压。观察线程池活跃数、CPU 使用率、RT 和错误率。如果 CPU 还闲但 RT 变长考虑增加线程数如果 CPU 已经顶满加线程只会更慢就该考虑横向扩容或优化代码。很多人的 Tomcat 卡顿不是参数太小而是第一台机器本来就扛不住需求参数再大也只是让更多请求排队等死。5.3 静态资源压缩和缓存头Tomcat 处理静态资源虽然不是最强项但开了压缩能让带宽占用明显下降。常见配置前面列过配合 HTTPS 部署时要注意compressionon对text/html、application/javascript、text/css这类响应压缩效果明显图片视频通常本身就压缩过没必要再压白白耗 CPU。还想给静态资源加缓存头可以在应用的web.xml里加一个 Filter或者用 Tomcat 的ExpiresFilter。多数时候前边会用 Nginx 接管静态资源Tomcat 这边就不用过度优化了。但如果你只有一台单机不搞 Nginx至少把压缩开起来能省不少流量。5.4 连接池配置这是一块很多人忽视的地基应用每跑一次 SQL 就DriverManager.getConnection()性能肯定很差因为连接创建和销毁太贵。用连接池可以复用连接Tomcat 内置了基于 DBCP 的数据源配置方式在应用的META-INF/context.xml里写Context Resource namejdbc/mysqlDB authContainer typejavax.sql.DataSource driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/orderdb?useUnicodetrueamp;characterEncodingUTF-8 usernamedbuser passwordsecret maxTotal100 maxIdle20 maxWaitMillis10000 validationQuerySELECT 1 / /Context几个关键参数注意下maxTotal连接池中最大连接数。不是越大越好数据库那边也有上限设大了反而容易把库拖垮。maxIdle最大空闲连接数空闲太多浪费资源。maxWaitMillis拿不到连接时最多等待多少毫秒到点就抛异常让请求快速失败而不是无限傻等。validationQuery借出或归还连接时的有效性检查语句MySQL 写SELECT 1就行。然后在代码里用 JNDI 获取Context ctx new InitialContext(); DataSource ds (DataSource) ctx.lookup(java:comp/env/jdbc/mysqlDB); Connection conn ds.getConnection();这种方式的优势是连接复用吞吐量比每次新建连接高一个量级。不过现代项目大多直接上 MyBatis、Hibernate 自带连接池Tomcat 自带的这个反而用得少了但原理一定要懂否则遇到池耗尽、连接超时这类问题还是一脸懵。6. 日志与故障排查别猜去看日志6.1 日志文件怎么读Tomcat 的日志目录logs下常见这些文件文件内容说明catalina.outJVM 标准输出和异常堆栈最重要的排查入口localhost.log应用在 Tomcat 内部的初始化、启动、销毁记录manager.logManager 管理应用的操作日志host-manager.log虚拟主机管理日志localhost_access_log.*.txt访问日志需主动配置才生成很多启动报错在控制台一闪而过但catalina.out里一定有完整堆栈。遇到任何异常第一件事就是tail -200 /usr/local/tomcat/logs/catalina.out如果文件太大用grep -n SEVERE\|Exception\|Error catalina.out先定位异常级别再往前翻上下文。6.2 启动失败排查速查表我把这几年遇到的高频启动问题整理成一张表方便对照排查现象可能原因处理方法java.lang.UnsupportedClassVersionErrorJDK 版本与应用编译版本不匹配升级或降级 JDK或者重新编译应用Port 8080 already in use端口被占用netstat -tlnpjava.net.BindException: Address already in use多个 Tomcat 实例端口冲突改 server.xml 的三个端口注意都别重复ClassNotFoundException: javax.servlet.Filter缺少 Servlet API 依赖确认应用编译期带上容器提供的依赖或正确配置Invalid character found in the request target访问路径含非法字符清理 URL 编码检查带中文或特殊符号的请求SEVERE: Failed to initialize end point连接器初始化失败检查端口权限、防火墙查看具体异常行OOM: Java heap space堆内存不足调大-Xmx或排查内存泄漏这张表不能解决所有问题但能让你在不会看堆栈的时候先排除掉八成的基础问题。6.3 应用卡死、CPU 飙高的解法思路如果 Tomcat 进程还活着但请求全部超时CPU 飙到 100%先拿到线程转储分析jstack pid threaddump.txt连续采样几次通常 5 秒一次共 10 次然后看哪类线程一直处于RUNNABLE状态。如果大量线程在jdbc的 socket read 上可能是数据库慢查询堵住了连接池如果大量线程卡在锁等待需要看代码里的同步块是否有死锁迹象。如果是内存问题用jstat -gc pid看各个内存区域变化加上堆转储jmap -dump:formatb,fileheap.hprof pid这个堆转储文件可以用内存分析工具打开通常能一眼看出哪个 List 有几十万个对象没释放。很多时候排查到最后发现是某个缓存 Map 忘了限制大小只进不出。6.4 访问日志开启方法默认 Tomcat 不记录访问日志这在排查“哪些请求打到了服务”的时候很被动。在server.xml的 Host 下加入Valve classNameorg.apache.catalina.valves.AccessLogValve directorylogs prefixlocalhost_access_log suffix.txt pattern%h %l %u %t quot;%rquot; %s %b %D /%h是客户端 IP%D是请求处理耗时毫秒数。开启后可以对比耗时分布判断接口是否变慢。生产环境建议保留这个日志但注意分割策略日志文件大了以后要及时清理否则磁盘满了也够喝一壶的。7. 安全加固上线前要做的几项基本操作7.1 清理默认应用和默认页面Tomcat 自带的webapps目录里有ROOT、docs、examples、manager、host-manager这些默认应用。生产环境务必删掉或移走这些只保留必须的业务应用。默认页面和示例代码能暴露软件版本和配置信息等于给别人递了张藏宝图。cd /usr/local/tomcat/webapps mkdir ../backup_default mv docs examples manager host-manager ROOT ../backup_default/7.2 管理用户权限最小化manager和host-manager应用如果不删那conf/tomcat-users.xml里的用户密码一定要改成强密码并且角色分配要谨慎。特别提醒网上很多教程写着用一个user配上所有角色比如manager-gui,admin-gui这种做法等于敞开管理后门。我就见过一个生产环境用默认用户名密码被扫出了后台然后被改了个网页。正确做法是只给确有必要的人分配最小权限比如看资源状态只给manager-script角色不给可视化界面的manager-gui。7.3 隐藏版本信息并关掉 AJP对外暴露 Tomcat 版本号不是好事。可以通过给 Connector 加server属性覆盖默认的响应头例如设成一个不具体的名称Connector port8080 protocolHTTP/1.1 serverServer ...AJP 端口 8009 是专门给反向代理内部用的如果前面没有 Nginx 或 Apache 走 AJP直接注释掉!-- Connector port8009 protocolAJP/1.3 redirectPort8443 / --省一个端口的暴露面。这个细节容易被忽略但安全扫描的时候常被拎出来说。7.4 保持升级更新Tomcat 作为使用量很大的开源软件隔段时间就会有一些安全补丁。生产环境尽量选择社区活跃的稳定版本并定期关注官方发布的安全公告遇到高危补丁尽早升级。升级过程也不算复杂下载新版本、解压把conf和业务应用目录整体迁移过去然后跑一遍启动和功能验证即可。千万别图省事直接覆盖安装新旧配置不兼容的概率不低稳妥操作是“并行新目录、迁配置、切流量”。8. 最后再分享几句大实话接触 Tomcat 这些年我最大的体会是很多东西看着是配置问题、启动失败问题归根结底是没搞懂它内部的运行模型。比如你认为的“重启一下就好了”如果不知道重启后发生了什么问题就会出现第二次、第三次。很多团队遇到 Tomcat 卡顿就会想到堆内存不够然后无脑调大-Xmx但其实先压测一把、抓一次堆转储往往效率更高。同样很多“无法访问”的故障排查链路其实很短先确认进程在不在、端口有没有监听、日志有没有异常。按这个顺序走大多数问题在十分钟内就能定位到根因。最后一个小技巧每次改动conf或者部署新版本之前手动给关键文件做个带时间戳的备份。这个动作看着多此一举但真到了线上出问题需要回滚的时候你会庆幸自己做了这个选择。