1. 安装前必须想清楚的几件事:版本、JDK 和下载渠道
很多朋友拿到 Tomcat 后第一件事就是解压、双击 startup.bat,结果要么闪退,要么起来之后项目部署一堆莫名其妙的问题。其实 Tomcat 的“配置”从下载之前就开始了,版本选错,后面全是坑。
先看 Tomcat 和 JDK 的版本对应关系。Tomcat 9.x 要求 JDK 8 及以上,Tomcat 10.0.x 是个分水岭,它基于 Jakarta EE 9,把原来的 javax.servlet 包全部改成了 jakarta.servlet,如果你的项目是老代码,用的还是 javax,直接扔进 Tomcat 10 里面跑,启动就报 ClassNotFoundException,而且是在你完全看不懂的位置报错。Tomcat 8.5.x 是目前兼容性最稳的选择,Spring Boot 内嵌的默认 Tomcat 也长期停留在 8.5/9.x 这个路线,公司生产环境里大量存量项目跑在 8.5 上,这个事实说明它很能打。
这里给一个比较省心的选择逻辑:
| 项目类型 | 推荐 Tomcat 版本 | 推荐 JDK 版本 |
|---|---|---|
| 老项目(javax 包) | 8.5.x 或 9.0.x | JDK 8 或 11 |
| 新项目(jakarta 包) | 10.1.x 或 11.x | JDK 11 或 17 |
| Spring Boot 内嵌场景 | 随 Boot 版本走的默认版本 | 随 Boot 要求走 |
| 只是想学习 Servlet/JSP | 9.0.x | JDK 8 |
下载渠道这里也提一句:不要随便找个第三方网站下所谓的“优化版”“绿色版”,直接去 Apache Tomcat 官网,如果要下旧版本,去 archive.apache.org/dist/tomcat/ 翻历史版本。之前见过有人在非官方渠道下载的安装包,解压出来确实能运行,但里面被塞了挖矿程序,服务器的 CPU 莫名其妙飙到 100%,这种低级亏不能再吃。
还有个容易忽视的点是 ARM 架构。如果你用的是云服务器 ARM 实例或者 Mac 的 M 系列芯片,Tomcat 本身是纯 Java 写的,理论上不区分 CPU 架构,真正区分架构的是 JDK。也就是说,在 ARM 机器上装 Tomcat,只要你的 JDK 是用 ARM 版安装的,Tomcat 直接解压就能跑,不用刻意找“Tomcat ARM 版”,网上搜这个词基本都是误导。但要注意一点:如果 JDK 是从官网下的 x86 版,装在 ARM 系统上会直接报 Exec format error,这个报错藏在 startup 日志里,很容易漏掉。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 第一次启动 Tomcat:闪退、乱码和端口占用到底怎么查
2.1 startup.bat 一闪而过,日志去哪看
Windows 下双击 startup.bat,黑窗口闪一下就没了,这是新手遇到最多的场景。很多人第一反应是“重装”,实际上 Tomcat 的所有启动报错都会写进 logs/catalina.out(Linux)或 logs/catalina.<日期>.log(Windows 下也有),闪退只是窗口关闭了,不代表日志没写。
startup.bat 本质上调用了 catalina.bat start,而 catalina.bat 又会去解析环境变量。闪退最常见的原因是 JAVA_HOME 或 JRE_HOME 没有设置,或者设置路径不对。Tomcat 启动脚本的判断逻辑是:先找 JAVA_HOME,找不到再找 JRE_HOME,两个都没有就直接退出。注意一个细节,如果 Tomcat 是 9.0 的安装版,它还会检查 JRE_HOME 是否存在,但很多时候你只配了 JAVA_HOME,也够用,因为脚本会从 JAVA_HOME 推导出 JRE 路径。
排查闪退的正确姿势是:不要在 Windows 下双击 startup.bat,而是打开 cmd 终端,cd 到 Tomcat 的 bin 目录,手动执行 catalina.bat run。这个命令会让 Tomcat 在前台运行,所有日志直接打到当前控制台,报错信息一目了然。等你能正常运行后,再用 startup.bat 后台启动。
2.2 控制台乱码的根源不是编码,是 Windows 代码页
startup.bat 启动后控制台打印的中文日志全是乱码,这个问题的本质是 Tomcat 的日志输出编码用的是 UTF-8,而 Windows 控制台默认代码页是 GBK(cmd 里 chcp 查看,936 就是 GBK)。两边对不上,中文自然就花了。
解决办法有两层。第一层是修改 conf/logging.properties 文件,把 java.util.logging.ConsoleHandler.encoding 的值从 UTF-8 改成 GBK。做了这一步,控制台乱码就解决了。但要注意,这个修改只影响控制台输出,logs 目录下按日期生成的文件日志仍然是 UTF-8 编码,用文本编辑器打开是正确的。
第二层是如果你用的是 IDEA 或其他 IDE 内置终端,乱码可能来自 IDE 终端本身的编码设置。IDEA 里 Help -> Edit Custom VM Options 加上 -Dfile.encoding=UTF-8,然后重启 IDE,终端编码切成 UTF-8,基本就通了。这个问题在 Windows 服务器上特别常见,很多时候不是 Tomcat 配置错,而是终端环境搞鬼,所以在服务器上我一般直接用 catalina.out 看日志,不在控制台纠缠。
2.3 端口占用:8080 被别的程序抢了,别急着改端口
启动时如果报:
code复制SEVERE: Failed to initialize end point associated with ProtocolHandler ["http-bio-8080"]
java.net.BindException: Address already in use: JVM_Bind <null>:8080
说明 8080 端口被占用。很多人第一反应是把 Tomcat 端口改成 8081 或者 9090,这当然能解决,但要想清楚:如果换端口是为了绕开问题,那下次部署其他服务可能还会撞上。更稳妥的做法是先找出占用 8080 的进程。
Windows 下用 netstat -ano | findstr 8080 拿到 PID,再去任务管理器里找到对应进程,确认它是什么。常见的情况是之前启动过的一个 Tomcat 实例没关干净,或者本机装了别的 Web 服务(比如某些开发工具自带的 HTTP Server)。如果是残留的 Java 进程,直接 taskkill /PID <pid> /F 杀掉再启动。
Linux 下就是 netstat -tlnp | grep 8080 或者 ss -tlnp | grep 8080,看 PID 后 kill -9 之前,最好用 ps -ef | grep <pid> 看一眼进程全路径,避免误杀重要服务。Tomcat 端口的修改位置在 conf/server.xml 的 <Connector port="8080" ... /> 这一行,注意文件里有好几个 Connector,有 8080(HTTP)、8009(AJP)、8005(shutdown),改的时候别看错行。
3. Web 应用部署的完整链路:从 war 包到 idea 可视化配置
3.1 三种部署方式,按场景选
Tomcat 部署 Web 应用,常见的有三种方式。
第一种最直接:把打好的 war 包丢进 Tomcat 的 webapps 目录,启动时 Tomcat 会自动解压部署。这种方式的优点是简单、无需额外配置,缺点是每次部署都要手动拷贝文件,而且重启才能生效,适合测试环境或者一次性部署。
第二种是配置虚拟目录映射,不改 webapps,而是在 conf/server.xml 的 <Host> 标签里加 <Context> 元素,把外部目录映射到访问路径。比如你想把项目放在 /data/myapp,访问路径是 /app,就这么写:
xml复制<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
<Context path="/app" docBase="/data/myapp" reloadable="true" />
</Host>
这种方式的优势是应用代码可以放在任意位置,不污染 Tomcat 安装目录,适合把应用目录和 Tomcat 目录分开管理的场景。但坑也很明显:appBase 和 docBase 如果配得不对,会出现“部署了但访问 404”的情况,后面细说。
第三种是基于 IDE 的开发模式,IDEA 或 Eclipse 里配置本地 Tomcat,每次改完代码自动部署。开发模式用的是 Tomcat 的 CATALINA_BASE 机制,IDE 会复制一份 Tomcat 的配置到临时目录,然后指向你本地的 Tomcat 安装目录运行,所以控制台里看到的 Tomcat 路径往往不是你真正解压的那个路径,这是正常的。
3.2 IDEA 里 use custom location 到底是什么意思
热搜词里有个“eclipse 中 use custom location(does not modify tomcat installation) 如何配置”,很多人在配置 IDE 集成 Tomcat 时被这个选项卡住。
这个选项的意思是:是否使用自定义的 Tomcat 配置目录(CATALINA_BASE),如果你勾选了,IDE 会在工作空间的 .metadata 下生成一份 Tomcat 配置副本,你在 IDE 里改的端口、部署路径,都是改在副本上,不会动你磁盘上那个 Tomcat 原始安装目录(CATALINA_HOME)。不勾选的话,IDE 直接复用 Tomcat 安装目录下的配置。
我的建议是勾选。原因有三点:
- 多个项目共用同一个 Tomcat 安装包时,每个项目有自己的配置副本,互不干扰
- IDE 里改崩了配置,直接删掉副本恢复默认,不影响原始安装
- 部署时会用到 IDE 生成的
conf/Catalina/localhost/*.xml这种动态 Context 文件,它在副本目录里,Tomcat 原始安装目录里不会出现一堆垃圾配置
但注意勾选后有一个经典问题:你在 IDEA 的 Deployment 里添加了 war 包,运行时发现访问 404,因为 IDEA 的部署机制是生成一个 context 文件指向 target 目录,如果 context 文件的 docBase 指向的路径不存在,或者不是你预期的工作目录,部署就不生效。解决办法是在 Deployment 标签里把 Application context 和 Output directory 对清楚,然后把 build 输出路径改成 target,确保 IDEA 把编译产物输出到正确位置。
3.3 部署后 404 的排查顺序
部署完应用,浏览器访问 http://localhost:8080/应用名/ 返回 404,很多人一头雾水。按照这个顺序排查,基本能定位 90% 的问题:
webapps下是否有对应的文件夹或 war 包logs/localhost.<日期>.log里有没有报错,特别是ClassNotFoundException和NoClassDefFoundErrorconf/server.xml的<Host>里有没有多余的<Context>配置覆盖了默认部署路径- 访问路径和
path属性是否一致 - 应用内的
WEB-INF/web.xml配置的 servlet-mapping 是否正确
有次我在测试环境部署一个老项目,war 包解压成功,目录结构也正常,但访问首页一直 500。最后查 localhost.log 才发现是 web.xml 里引用了 web-app_3_0.xsd,而 Tomcat 8.5 的默认 JSP 引擎版本对 3.0 的 schema 检查严格,少了个命名空间声明就直接抛异常。这类问题只看浏览器报错是看不出来的,必须养成看日志的习惯。
4. server.xml 里那些“改一下就好”的配置,背后的原理是什么
conf/server.xml 是 Tomcat 最核心的配置文件,很多人对它又爱又恨。爱是因为大部分运行参数都在这改,恨是因为结构看起来复杂,Server、Service、Connector、Engine、Host、Context 层层嵌套,不知道哪个改哪个。
4.1 Connector 的线程池和常用优化参数
HTTP Connector 是外部请求进入 Tomcat 的入口,最常用的配置项有 port、protocol、connectionTimeout、maxThreads、minSpareThreads、acceptCount 等。
xml复制<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxThreads="200"
minSpareThreads="20"
acceptCount="100"
maxConnections="8192"
compression="on"
compressionMinSize="2048"
compressibleMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/javascript" />
解释几个关键参数的含义:
maxThreads:Tomcat 处理请求的最大线程数。默认 200,这个值不是越大越好。每个线程都要占用 JVM 栈内存(默认 1MB),200 个线程就是 200MB,如果 JVM 堆只给了 512MB,线程数开到 1000,内存直接爆。实际生产中,要根据业务耗时和机器核数来定,一般 4C8G 的机器,maxThreads设 400-600 是一个比较合理的区间。acceptCount:请求队列的长度。当所有线程都忙时,新进来的请求会被放入队列等待。队列满之后,来的请求直接拒绝。这个值设得太小,高峰期会大量丢请求;设得太大,请求排队时间过长,用户感受到的响应时间急剧上升。connectionTimeout:建立连接后的超时时间,单位毫秒。20 秒是默认值,如果应用响应本身就慢,可以适当调高,但要注意同时配合socketTimeout使用。compression:开启内容压缩。对于页面有大量文本和 JSON 接口的服务,开启压缩可以显著降低带宽占用,但会消耗一点 CPU。实测一个 2MB 的 JSON 返回体,开启 gzip 压缩后能压到 200-300KB,传输时间少一个数量级。
4.2 Host 与虚拟主机:一台 Tomcat 跑多个域名
Engine 下面可以配置多个 Host,每个 Host 代表一个虚拟主机,通过域名区分。比如同一台服务器上要同时跑 a.example.com 和 b.example.com 两个站点,就配两个 Host:
xml复制<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
<Context path="" docBase="app-a" reloadable="false" />
</Host>
<Host name="a.example.com" appBase="webapps-a" unpackWARs="true" autoDeploy="true">
</Host>
<Host name="b.example.com" appBase="webapps-b" unpackWARs="true" autoDeploy="true">
</Host>
</Engine>
配了多个 Host 之后,要区分 defaultHost 的作用:访问 IP 时,请求会落到 defaultHost 上;访问具体域名时,按域名匹配。如果域名没有对应的 Host,也会走 defaultHost。这个逻辑很多人搞混,以为设置了多个 Host 就能自动按域名分发,其实 defaultHost 是最终兜底。
appBase 是 Host 的应用基路径,每个 Host 可以指向不同的目录,这样 A 站点的部署不会影响到 B 站点。这个目录可以是绝对路径,也可以是相对路径(相对 CATALINA_HOME)。
4.3 Context 的 path 和 docBase 的配合逻辑
<Context> 元素是 Host 下的子元素,表示一个 Web 应用。path 决定访问路径,docBase 决定应用文件在哪。
这里有三种典型配法:
- 直接丢 war 包到
webapps下,不写 Context 标签,访问路径就是 war 包文件名(不带 .war 后缀) - 在 Host 下写
<Context path="/myapp" docBase="/opt/myapp" />,访问路径是/myapp,应用文件在/opt/myapp - 不加 path 属性,写
<Context docBase="/opt/myapp" />,这个 Context 会成为 Host 的默认应用,访问根路径/就能直接进
第三个配法用的场景是:同一台机器部署多个 Web 应用,其中一个是主应用,希望用户输入域名直接打开,不用带上下文路径。
这里有一个容易踩的坑:如果你在 server.xml 里写 <Context path="/myapp" docBase="/opt/myapp" reloadable="true" />,那这个 /opt/myapp/WEB-INF/web.xml 只要被修改,Tomcat 就会自动重新加载。开发时这个功能很爽,但生产环境一定要关掉,否则偶尔一次线上误改配置文件,整个应用自动重启,用户请求大量报错。reloadable="false" 是生产环境的基本操作。
5. 内存配置和 systemctl 启动失败的隐藏逻辑
5.1 setenv.sh:Tomcat 内存配置的标准姿势
Tomcat 的启动脚本在 bin/catalina.sh 里会默认读取 bin/setenv.sh(如果存在),这个文件是配置 JVM 参数的推荐位置。默认情况下这个文件不存在,需要自己创建。为什么不用直接改 catalina.sh?因为 catalina.sh 是 Tomcat 自带的脚本,升级 Tomcat 版本时会被覆盖,而 setenv.sh 是专门留给用户扩展的,升级不会动它。
创建 bin/setenv.sh,内容大致如下:
bash复制#!/bin/sh
CATALINA_OPTS="-Xms1024m -Xmx2048m -XX:MaxMetaspaceSize=256m -Djava.awt.headless=true"
# 如果需要指定 JVM 路径,可以加一行
# JAVA_HOME=/usr/local/java/jdk1.8.0_202
-Xms 是 JVM 初始堆大小,-Xmx 是最大堆大小。不要想当然地以为 -Xmx 越大越好,堆大小超过物理内存的一半,GC 时间会变得很长,整个应用响应速度反而下降。一个常见的分配原则是:物理内存 8GB,Tomcat 堆给 2-3GB,留一部分给操作系统、线程栈和元空间。
-XX:MaxMetaspaceSize 是元空间大小,对应的是类元数据的存储空间。如果你的应用用了大量第三方库(Spring、MyBatis 等),元空间默认不受限(只受物理内存限制),但为了防止类加载器泄漏导致的元空间膨胀,生产环境建议显式指定一个上限。Spring Boot 应用经常报 java.lang.OutOfMemoryError: Metaspace,典型的就是因为没设这个参数。
另外,CATALINA_OPTS 和 JAVA_OPTS 的区别值得说清楚:JAVA_OPTS 会影响所有通过 catalina.sh 启动的 Java 进程,包括停止 Tomcat 的 catalina stop 命令;CATALINA_OPTS 只影响 Tomcat 启动,不影响 stop。所以像 -Xms、-Xmx 这种堆参数,放 CATALINA_OPTS 里更合适,避免 stop 操作因为尝试分配大内存而出问题。
5.2 线上真实场景:setenv.sh 配好了,systemctl 启动却失败
这是热搜词里非常典型的一个场景:在 setenv.sh 里配置了 JVM 内存参数,手动执行 catalina.sh start 一切正常,但用 systemctl start tomcat(或类似服务管理命令)启动时直接失败,查看日志什么有效信息都没有。
这个问题的关键在于:setenv.sh 默认没有执行权限。手动 catalina.sh start 时,是在 root 或普通用户下执行的,shell 直接解析脚本,不需要执行权限。但 systemd 在执行时会以服务定义中指定的用户去启动,如果 setenv.sh 的权限是 644(即 -rw-r--r--),且你的 Java 进程是 tomcat 用户,shell 无法执行这个脚本,启动就会失败。
解决方式很简单:
bash复制chmod +x /usr/local/tomcat/bin/setenv.sh
还有一个类似的坑:setenv.sh 里如果有 #!/bin/sh 并带有 CRLF(Windows 换行符),Linux 下会报 $'\r': command not found。如果你在 Windows 里编辑过 setenv.sh 再传到服务器,务必检查一下换行符,转成 LF。这个问题很隐蔽,因为它在脚本第一行就会报错,但很多人直接看 systemctl 状态只显示 failed,不看 journalctl 的详细输出,根本发现不了。
排查 systemctl 启动失败时,一定要用这个命令看完整日志:
bash复制journalctl -u tomcat.service -n 100 --no-pager
它会显示服务启动时的标准输出和错误输出,比 systemctl status 看到的“Active: failed”信息有用得多。
5.3 JVM 内存配置后的验证方式
配置完内存后,不要只凭“启动成功”来判断,要确认 JVM 实际分配了多大内存。最直接的办法是启动后查看 PID,然后:
bash复制jps -l
jinfo -flags <pid>
jinfo -flags 里能看到实际的 -Xms 和 -Xmx 值。如果显示的值和 setenv.sh 里配置的不同,说明配置没被读取到,或者读取的优先级有问题(比如多份配置文件互相覆盖)。
另外,jmap -heap <pid> 可以更详细地看堆内存的分代情况。不过要注意,线上环境不要轻易执行 jmap -dump 这类重量级命令,会触发 Full GC,影响在线服务。jinfo 和 jps 比较轻量,影响小。
6. Tomcat 迁移到新服务器:一份实操清单
6.1 要搬的不是整个目录,而是这几样
把 Tomcat 应用迁移到另一台服务器,最容易犯的错误是“直接打包整个 Tomcat 目录搬过去”。这样做不是不行,但会把一堆无关的日志、临时文件、缓存也搬过去,而且新服务器的 JDK 版本、目录结构、应用依赖可能都不一样,直接搬完大概率起不来。
迁移前先明确要搬什么:
| 迁移项 | 位置 | 说明 |
|---|---|---|
| 应用包 | webapps/*.war 或解压后的目录 |
可以直接搬 war 包,也可以搬解压后的完整目录 |
| 核心配置 | conf/server.xml、conf/web.xml、conf/context.xml |
特别注意 server.xml 里是否修改过端口、Host、线程池 |
| 内存配置 | bin/setenv.sh |
如果有,一起带上 |
| 环境变量配置 | /etc/profile 或 /etc/environment |
JAVA_HOME、CATALINA_HOME 等 |
| 证书文件 | conf/ssl 或自定义路径 |
如果启用了 HTTPS,证书和密钥文件必须带上 |
| 数据库连接配置 | 应用内的配置文件(如 jdbc.properties) | 迁移后数据库地址、账号密码大概率要改 |
6.2 迁移后的启动顺序和验证步骤
新服务器上装好 JDK,解压 Tomcat(最好使用与原环境相同的大版本),把上面的配置文件和 war 包放到位,按以下顺序启动和验证:
- 先检查 JDK 版本:
java -version,确认大版本和原环境一致(比如原来是 JDK 8,新环境装了 JDK 17,代码里用了旧的字节码增强库,很可能跑不起来) - 设置环境变量:
export JAVA_HOME=...、export CATALINA_HOME=... - 如果从旧机器拷贝了 conf 目录,先确认
server.xml里的路径是否在旧环境出现过绝对路径,比如<Context docBase="/opt/old/path/myapp" />,这个路径在新机器上不存在的话,应用部署不上 - 启动前先
catalina.sh configtest,这个命令会检查配置文件的基本合法性,有问题会提示,省去你用catalina.sh run反复看日志的时间 catalina.sh start- 启动后观察
logs/catalina.out是否出现Server startup in ... ms,看到这行才算真正启动成功 - 本地先用
curl http://localhost:8080/应用名/验证
迁移过程中遇到 404 或 500,优先检查路径类的配置。曾经遇到过一次迁移后所有静态资源 404,排查了半天,发现是 webapps/ROOT 下的应用目录结构里有个软链接指向了旧服务器上的绝对路径,这边没有对应的目录,Tomcat 解压的过程中对软链解析失败,导致部分文件丢失。所以迁移前最好先清理应用目录里的软链接、绝对路径引用。
6.3 数据源和外部依赖,迁移时最容易遗漏
Tomcat 应用的数据源配置有两种常见位置:一种是应用内 META-INF/context.xml 配置 JNDI 数据源,另一种是直接写在应用自己的 Spring 或 MyBatis 配置文件里。迁移时,META-INF/context.xml 经常被忽略,因为它在 war 包或者解压目录的 META-INF 下面,不在 conf 目录里。
如果原应用用了 JNDI 数据源,迁移后要检查新 Tomcat 的 conf/context.xml 是否允许应用使用 JNDI(默认允许),以及数据库服务器的地址是否从内网 IP 变成了公网 IP(如果迁移到不同云厂商,内网互通情况不同)。
外部依赖还包括:应用用到的静态资源目录(可能是服务器上的独立目录)、上传文件的存储路径、日志输出的绝对路径。这些路径如果硬编码在应用配置里,迁移后必须逐一改掉。建议在应用内统一用相对路径或者从环境变量读取,而不是写死绝对路径,这样迁移时只改环境变量即可。
7. 几个高频问题的排查链路:CPU 飙升、CORS、nginx 代理
7.1 Tomcat CPU 高的根因到底在哪
“Tomcat CPU 高”这个话题在搜索里常年靠前,但很多人一上来就盯着 Tomcat 本身查,方向就偏了。Tomcat 是 Java 应用服务器,CPU 高的根本原因几乎都在应用代码或 JVM 层。
排查 CPU 高的问题,我的步骤是固定的:
第一步,确认是不是 Tomcat 自身的问题。用 top -Hp <tomcat_pid> 看线程级别的 CPU 占用,找到 CPU 最高的线程号,记下来。如果多个线程都在 100%,且线程名类似于 http-nio-8080-exec-*,说明是请求处理线程忙不过来,问题在上层应用。
第二步,把线程号转成十六进制:printf "%x\n" <线程号>。
第三步,用 jstack <pid> > thread_dump.txt 导出线程快照,在文件里搜那个十六进制线程号。对应的栈信息就是当前 CPU 最高的线程正在执行的代码。如果栈里显示在 java.net.SocketInputStream.socketRead0 这种地方,说明线程在等待 IO,不是真正的 CPU 消耗;如果显示在业务代码的 while 循环或者 JSON 序列化上,那问题就在那。
一个线上实际案例:某服务每个请求的处理时间突然从 50ms 涨到 5 秒,top 看 Tomcat CPU 300%。jstack 后发现大量线程卡在 org.apache.catalina.connector.CoyoteAdapter.service 之后的 com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString 上,进一步分析发现响应对象里有一个巨大的嵌套 List,每次请求都在做全量序列化。优化方案是把列表改为分页查询,CPU 立刻降下来了。这个过程没有改 Tomcat 的任何配置,问题出在应用层。
7.2 跨域问题:官方 CorsFilter 的正确配置方式
Tomcat 8.5 及以上版本自带了一个 CorsFilter,不用自己在应用里写 Filter,也不用引入第三方库。在 web.xml 里配置即可:
xml复制<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
<init-param>
<param-name>cors.allowed.origins</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.headers</param-name>
<param-value>Content-Type,X-Requested-With,Accept,Authorization</param-value>
</init-param>
<init-param>
<param-name>cors.exposed.headers</param-name>
<param-value>Location</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
配置 CorsFilter 有两个注意点:
一是 cors.allowed.origins 如果设置了具体的域名,注意它不支持带路径的写法。曾经有同事配了 https://www.example.com/(带尾部斜杠),导致浏览器跨域请求拿不到 Access-Control-Allow-Origin 响应头,排查了半天才发现是配置值写法的细节问题。
二是 OPTIONS 预检请求的处理。浏览器跨域请求在正式请求前会先发一个 OPTIONS 请求做预检,如果 CorsFilter 没有在 cors.allowed.methods 里包含 OPTIONS,预检就过不了。更常见的坑是另外配置了一个拦截器把 OPTIONS 请求拦掉了,导致预检请求根本到不了 CorsFilter。
7.3 nginx 反向代理 Tomcat 的常见坑
用 nginx 代理 Tomcat 最常见的需求是“域名 80 端口转发到 Tomcat 8080”。基础配置很直观:
nginx复制server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
但有几个问题不配好,线上就会出事故。
第一个是请求大小限制。client_max_body_size 默认是 1MB,如果应用有文件上传功能,超过 1MB 的请求会被 nginx 直接拒绝,返回 413,而且 Tomcat 那边根本看不到请求进来。需要在 server 或 location 块里加上:
nginx复制client_max_body_size 20m;
第二个是超时时间。proxy_read_timeout 默认 60 秒,如果应用有长时间运行的接口(比如报表导出、批量处理),超过 60 秒 Tomcat 还没返回,nginx 就会断开连接,浏览器那边看到的是 502 或 504。配置时要和应用的超时时间配合,不能简单调到很大,否则连接被占住,nginx 的 worker 连接数也会被拖垮。
第三个是 WebSocket 支持。如果你的应用用了 WebSocket(比如消息推送),nginx 需要额外配置:
nginx复制location /ws/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
不加这段,WebSocket 握手会失败,前端控制台会报 WebSocket connection to 'ws://...' failed 的错误。
7.4 其他几个让新手头疼的问题集锦
Tomcat 启动出现... 这类搜索词背后,通常跟着的是各种启动报错。挑几个高频的来说:
报错 The basedir set in the context's docBase is invalid:context.xml 或 server.xml 里的 docBase 指向的目录不存在或者没有权限。检查路径有没有拼错,以及这些目录的用户权限。
报错 Error deploying web application archive:war 包解压失败,可能是 war 包损坏,也可能是 WEB-INF/classes 下存在非法的 class 文件字节码版本(JDK 版本太老,class 编译版本太高)。
报错 java.lang.OutOfMemoryError: PermGen space:这是 JDK 8 之前的典型问题,JDK 8 中 PermGen 被 Metaspace 取代了,所以如果你用 JDK 8 还看到这个报错,说明项目里的某个依赖通过反射或字节码增强生成了大量类,旧代码没适配 Metaspace 的回收机制。这时用 -XX:MaxMetaspaceSize 限制大小,同时关注有没有类加载器泄漏。
Tomcat 的日志文件一直增长不清理:logs/catalina.out 会无限增长,esx上跑几个月就能到几十 GB。建议配置 logrotate 做日志切割,按天保留 7-30 天。启动时也可以把 bin/catalina.sh 里的 CATALINA_OUT 环境变量指到自定义位置,方便统一管理日志。
8. 从实战角度聊聊 Tomcat 的运维习惯
Tomcat 用久了,会发现它本身并不复杂,真正考验人的是对 Java 进程和 JVM 的熟悉程度。这里分享几个我自己长期在用的运维习惯。
第一是启动脚本和管理命令尽量统一。手动部署的机器上,尽量用 catalina.sh run 在前台调试,用 catalina.sh start 后台运行,用 catalina.sh stop 停止。不要频繁去 kill PID,kill -9 虽然快,但会跳过 Tomcat 的优雅停机流程,连接池、线程池来不及释放,下次启动时可能因为端口未释放或锁文件未清理而失败。只有在 kill 正常无法停止时才考虑 kill -9。
第二是每次改配置前先备份。改 server.xml、context.xml 这类文件前,cp server.xml server.xml.bak.<日期>。改完用 catalina.sh configtest 检查,再重启。这看起来是个再简单不过的习惯,但在线上环境能救命,因为 Tomcat 的许多配置错误是在启动时才暴露的。
第三是监控必须做。至少要把 Tomcat 的 JVM 内存、线程数、HTTP 连接数这几个指标采集到监控平台。不需要装多复杂的插件,脚本定时执行 jstat -gcutil <pid> 采集堆内存使用率,再用 curl -o /dev/null -s -w "%{http_code}" http://localhost:8080/ 做存活探测,就能发现绝大多数问题。
我在实际运维中发现,Tomcat 的部署配置内容其实只占日常工作的三成,七成时间都在和 Java 日志、JVM 参数、应用代码打交道。很多人遇到 Tomcat 问题第一时间想的是“Tomcat 配置是不是有问题”,但真正排查下来,大部分问题出在应用内部或者 JVM 层。
所以如果你想学好 Tomcat 的配置与使用,我建议不要只盯着 server.xml 那几行参数,而是花时间把 JDK 环境变量、JVM 内存模型、线程转储分析、nginx 反向代理这些周边技术一并打通。Tomcat 本身是一个很稳定的 Servlet 容器,它不惹事,但你要能看懂它留下的日志和线索。配置只是入口,能定位并解决问题,才是这项技能真正的价值所在。
