部署Tomcat这事儿,说难不难,说简单也有一堆坑等着你。从下载解压到跑起来一个能用的Web服务,中间隔着JDK版本匹配、端口配置、JVM参数、自启动脚本、日志切割、前后端分离路由……任何一个环节偷懒,后面上线都可能给你颜色看。这篇东西不是照抄官方文档的翻译稿,而是把我在Linux上从零部署Tomcat、给生产环境做配置调优、踩完各种坑之后沉淀下来的完整流程和心得写出来,从安装到运维再到排错,一条线捋清楚。适合刚接触Java Web部署的开发者、需要自己维护服务器的后端工程师,以及被分配了“把项目跑起来”任务的运维新人——看完至少能少走三天弯路。
1. 内容整体设计与思路拆解
1.1 Tomcat到底是什么,为什么我们需要它
Tomcat本质上是Apache Software Foundation维护的一个开源Servlet容器,负责运行Java Web应用,把HTTP请求转换成Java对象交给你的业务代码处理,再把结果包装成HTTP响应返回给客户端。说得生活化一点:Tomcat像一家餐厅的服务员团队,客人的点餐(HTTP请求)由服务员接单,转交给后厨(你的Servlet/Spring MVC代码),后厨做好菜(业务数据),服务员再端上桌(HTTP响应)。没有这个服务员团队,你写的菜品再好也送不到客人面前。
很多初学者容易混淆的概念是:Tomcat既是Web服务器,也是Servlet容器。Web服务器意味着它能处理静态资源(HTML、CSS、JS、图片),Servlet容器意味着它能运行Java Servlet/JSP。它不像Nginx那样以高性能处理静态文件和反向代理见长,但胜在Java生态的原生支持,Spring Boot内置的Web容器默认就包含Tomcat,这也是为什么前后端分离架构里,Nginx负责静态资源并反向代理到Tomcat后端,两者分工配合的搭配非常常见。
1.2 部署方案选型:为什么选择“传统Tomcat”而不是“全家桶”
现在新项目大多直接Spring Boot内置Tomcat,一个java -jar就完事了,那为什么还要专门学传统Tomcat部署?因为生产环境里还有大量传统Java Web应用,或者说你工作中必然要接手老项目——Struts、Spring MVC、SSH/SSM打成的WAR包,这些都是部署在独立Tomcat上的。另外,独立部署Tomcat也方便做多应用隔离、独立JVM参数调优、与Nginx配合做负载均衡。
我这篇文章采用“Linux服务器 + 官方tar.gz包 + systemd自启动 + 独立JVM调优 + 日志切割”这套组合,也是生产环境最通用的方案。不推荐直接用apt install tomcat9这类包管理器安装,因为仓库里的版本通常滞后,而且目录结构、权限、版本都有系统包管理器的痕迹,不好控制。官方tar.gz包虽然是绿色解压版,但配合自建systemd服务,可控性是最高的。
1.3 受众定位与预期收获
这篇文章的设计思路是先建环境、再配应用、后做优化、最后排坑。读完你应当能独立完成:
- 在Linux上安装指定版本的JDK和Tomcat并正确配置环境变量
- 理解server.xml的核心配置节点并完成端口、线程池、虚拟主机调整
- 部署WAR包、实现Tomcat与Nginx的前后端分离架构
- 配置systemd自启动和JVM参数
- 面对端口占用、乱码、404、内存溢出等问题时有明确的排查路径
2. 安装部署实操要点:从JDK到第一个Hello World
2.1 JDK版本选择与安装
Tomcat本质上是Java程序,先装JDK是硬前提。版本对应关系得先理清楚,否则Tomcat可能起不来。
| Tomcat版本 | 最低Java版本 | 主要特性 |
|---|---|---|
| Tomcat 8.5 | Java 7+ | 老项目主流,兼容性好 |
| Tomcat 9.0 | Java 8+ | 支持Servlet 4.0,老项目新项目都能用 |
| Tomcat 10.1 | Java 11+ | Jakarta EE 9+,包名改为jakarta.* |
| Tomcat 11 | Java 17+ | 最新版本,新项目可选 |
注意:Tomcat 10及以后版本把
javax.servlet迁移成了jakarta.servlet,如果你的项目代码还在用javax包名,直接跑在Tomcat 10上会报ClassNotFoundException,这是升级时最常踩的坑。
JDK安装我用的是OpenJDK。官方tar.gz解压到/usr/local/java,然后配置环境变量:
# 下载对应版本,以JDK 8为例 tar -zxf jdk-8u421-linux-x64.tar.gz -C /usr/local/java # 编辑 /etc/profile,追加 export JAVA_HOME=/usr/local/java/jdk1.8.0_421 export PATH=$JAVA_HOME/bin:$PATH export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar # 使配置生效 source /etc/profile java -version生产环境里我习惯用一个专门账号跑Tomcat,不推荐直接用root。一是安全,二是防止某个Web应用被攻破后拿到root权限,那就全盘失守了。建账号的命令非常简单:
useradd -r -s /bin/false tomcat-s /bin/false限制该用户不能登录Shell,纯粹作为服务运行账号。
2.2 Tomcat下载与解压安装
代码包建议从Apache官网或国内镜像站下载。官网地址是tomcat.apache.org,下载页里选对应版本的tar.gz包,不要下zip包,Linux上tar.gz更干净。
mkdir -p /opt/tomcat tar -zxf apache-tomcat-9.0.98.tar.gz -C /opt/tomcat ln -s /opt/tomcat/apache-tomcat-9.0.98 /opt/tomcat/current做软链接current的好处是以后升级版本时,只需要解压新包然后改一下软链接,配置文件路径不变,脚本引用路径不变,平滑升级。
再把目录归属给tomcat用户:
chown -R tomcat:tomcat /opt/tomcat2.3 环境变量CATALINA_HOME与首次启动验证
Tomcat启动依赖两个环境变量:CATALINA_HOME(安装路径)和CATALINA_BASE(实例路径)。单实例部署时两者可以指向同一路径,多实例部署时base指向各自实例目录。
在/etc/profile里追加:
export CATALINA_HOME=/opt/tomcat/current export PATH=$CATALINA_HOME/bin:$PATH然后启动验证:
/opt/tomcat/current/bin/startup.sh看到Tomcat started只是JVM起来了,不代表一切正常。立刻检查两件事:第一,logs/catalina.out里有没有异常堆栈;第二,浏览器访问http://服务器IP:8080,确认能打开那只经典的Tomcat猫页。
提示:如果8080端口访问不通,优先检查云安全组入方向和服务器本地防火墙。很多人部署没问题,最后是防火墙策略挡在外面。
启动方式需要区分一下:startup.sh是后台启动,会放一个daemon进程;catalina.sh run是前台运行模式,日志直接打印到控制台,适合调试时临时用。生产环境用systemd托管,就不用手动执行startup.sh了。
3. 核心配置文件server.xml深度解析与调优
3.1 三个关键端口:8005、8080、8009
打开conf/server.xml,配置主体分三层:Server、Service、Connector和Engine及Host。三个端口各司其职:
| 端口 | 默认值 | 作用 | 注意事项 |
|---|---|---|---|
| Shutdown端口 | 8005 | 接收关闭命令 | 生产环境建议改掉默认值,防止外部直接关闭服务 |
| HTTP连接器 | 8080 | 对外提供HTTP服务 | 主要调优对象 |
| AJP连接器 | 8009 | 与Apache HTTP Server集成用 | 不用Apache时建议注释掉 |
AJP端口很多场景下是多余的,现在普遍用Nginx反代HTTP请求,根本用不到AJP。留着它反而多一个暴露面,所以我的建议是直接注释掉。
3.2 Connector参数调优:线程池、超时与编码
HTTP Connector是整个Tomcat性能的门户。先看一组生产环境常用配置:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxThreads="400" minSpareThreads="50" acceptCount="200" maxPostSize="10485760" URIEncoding="UTF-8" enableLookups="false" compression="on" compressionMinSize="2048" compressibleMimeType="text/html,text/xml,text/plain,text/css,application/json"/>每个参数背后都有讲究:
maxThreads:最大工作线程数,默认200。不是越大越好,每个线程都占JVM栈空间(默认1MB),400个线程就占了400MB栈内存,还要考虑CPU核心数和数据库连接池上限。一般按每秒请求量 × 平均响应时间估算,比如QPS预期500、平均响应200ms,那么活跃线程约100个,留点余量设200即可。minSpareThreads:启动时预创建的空闲线程,减少请求到来时的线程创建开销。acceptCount:请求队列长度。当线程全忙时新请求进入队列等待,队列满后请求会被拒绝。值太大导致请求排队时间过长,用户体验差;值太小又容易直接拒绝,建议200~500之间。maxPostSize:表单POST提交的最大字节数,默认2MB,很多文件上传功能就是被这里卡住的,需要时改大。URIEncoding:统一设置UTF-8,否则带中文参数的GET请求会出现乱码。enableLookups="false":关闭DNS反查,避免每次请求做域名解析拖慢响应。compression:压缩静态文本资源,减少传输体积,对带宽敏感场景提升明显。
那么Tomcat会如何处理一个连接:接收请求后分配给一个空闲线程执行,线程不够就放到accept队列里排队,再不够就直接拒绝。maxThreads、acceptCount和操作系统TCP backlog共同决定了系统的抗压上限,参数不是越大越好,要和JVM堆内存、CPU负载配套调整。
3.3 Host虚拟主机配置
Engine下可以挂多个Host,实现一台机器上多个域名对应多个应用目录的虚拟主机效果。
<Host name="www.example.com" appBase="webapps" autoDeploy="true" unpackWARs="true"> <Context path="" docBase="/data/apps/example" reloadable="false"/> </Host>几个属性要看清:
appBase:Web应用存放的基本目录,默认是webapps。autoDeploy:是否自动部署新放入的WAR包,开发环境开着方便,生产环境建议关掉,防止误放文件触发应用重载。unpackWARs:是否自动解压WAR包,开发环境开,生产环境可以关,但关掉后访问解压路径有差异,一般保持默认true。<Context>的path:访问路径,空字符串代表域名根路径直接访问应用。
生产环境我更习惯把应用目录放到webapps之外,比如/data/apps/,用docBase指定。这样升级应用时不会误删Tomcat自己的目录结构,也方便给应用独立的管理区域。
3.4 多实例部署的思路
一台机器上跑多个Tomcat实例的需求很常见——不同项目需要隔离的JVM和不同端口。传统做法是复制整套Tomcat目录,占磁盘空间又乱;推荐做法是共享同一份二进制目录,为每个实例单独建conf、logs、webapps、work、temp目录,然后设置CATALINA_BASE指向实例目录。
export CATALINA_HOME=/opt/tomcat/current export CATALINA_BASE=/data/tomcat-instances/app1 /opt/tomcat/current/bin/startup.sh修改实例目录下的conf/server.xml里的端口号,避免和别的实例冲突。这样做的好处是二进制升级一次,所有实例同时受益,维护成本直降。
4. 应用部署实操:WAR包、前后端分离与IDE开发环境
4.1 WAR包部署的几种玩法
传统Java Web应用打成WAR包后,部署方式有下面几种:
| 方式 | 操作 | 适用场景 |
|---|---|---|
| 丢进webapps目录 | cp app.war /opt/tomcat/current/webapps/ | 最简单,但生产环境不推荐 |
| Context描述符 | 在conf/Catalina/localhost/下写一个app.xml,指定docBase | 应用可放任意目录,推荐 |
| Manager管理界面 | 浏览器登录manager页面远程部署 | 管理多个应用时方便 |
| IDE部署 | IDEA里配置Tomcat Server直接热部署 | 开发调试阶段 |
上面表格之外的现代化做法是走CI/CD流水线,Maven或Jenkins打包后直接scp到服务器再调部署脚本,结合autoDeploy=false可以做到受控部署。
4.2 前后端分离项目部署架构:Nginx + Tomcat
前后端分离是当前主流,典型架构是:
- 前端:Vue/React打包成静态资源(HTML、CSS、JS),放在Nginx
- 后端:Spring Boot或者Spring MVC项目打成WAR包部署在Tomcat
- 动态请求:Nginx把
/api/前缀的请求反向代理到Tomcat的8080端口
Nginx配置关键部分:
server { listen 80; server_name example.com; root /data/frontend/dist; index index.html; location /api/ { 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; } location / { try_files $uri $uri/ /index.html; } }两个容易出错的地方提醒一下:
第一,proxy_pass的路径拼接问题。location /api/配proxy_pass http://127.0.0.1:8080;(不带斜杠)时,请求/api/user/list会原样转发给后端;如果proxy_pass http://127.0.0.1:8080/;(带斜杠),会把/api/前缀去掉再转发,变成/user/list。后端接口没写/api前缀就用第一种,写了就一句带过。这个细节能排查掉一半的502/404问题。
第二,try_files $uri $uri/ /index.html;是为了解决Vue Router的history模式刷新404问题。用户直接访问/about时,Nginx找不到对应文件,回退到index.html交给前端路由处理。如果用hash模式(URL带#),不需要这段配置。
4.3 IDEA中配置Tomcat进行本地开发调试
开发环境配Tomcat,很多人卡在IDEA的配置入口。简单捋一遍:
- 打开
Run/Debug Configurations - 点
+号,选择Tomcat Server下的Local Application Server右侧点Configure...,选择Tomcat安装目录Deployment页签点+,Artifact选你Web项目的war exploded(推荐,支持热部署)Application context填你希望的访问路径,比如/demoServer页签里On 'Update' action选Update classes and resources
需要注意的是:IDEA里配置的Tomcat一定要和项目要求的JDK、Tomcat版本匹配,否则启动时会报ClassNotFoundException或者各种版本冲突。另外war exploded比war更适合开发调试,改动代码能自动热更新,不用反复重启。
4.4 部署脚本示例
生产环境手动部署WAR包时,我习惯写一个简单的部署脚本,包含备份和回滚能力:
#!/bin/bash APP_NAME=example APP_WAR=/data/uploads/example.war WEBAPPS=/opt/tomcat/current/webapps BACKUP_DIR=/data/backups/$(date +%Y%m%d%H%M%S) mkdir -p $BACKUP_DIR if [ -f $WEBAPPS/${APP_NAME}.war ]; then cp $WEBAPPS/${APP_NAME}.war $BACKUP_DIR/ fi if [ -d $WEBAPPS/${APP_NAME} ]; then mv $WEBAPPS/${APP_NAME} $BACKUP_DIR/ fi cp $APP_WAR $WEBAPPS/ echo "部署完成,当前版本备份在: $BACKUP_DIR"这个脚本虽然简略,但“先备份再覆盖”这个思路很重要。出了问题能快速回滚,比临时找旧包去下载快了不止一个数量级。
5. 运维实战:自启动、JVM调优、日志与容器化
5.1 Linux下Tomcat自启动:systemd配置
手动startup.sh启动的服务,服务器一重启就没了。生产环境必须做成systemd服务。
在/etc/systemd/system/tomcat.service写入以下内容:
[Unit] Description=Apache Tomcat Web Server After=network.target [Service] Type=forking User=tomcat Group=tomcat Environment=JAVA_HOME=/usr/local/java/jdk1.8.0_421 Environment=CATALINA_HOME=/opt/tomcat/current Environment=CATALINA_BASE=/opt/tomcat/current ExecStart=/opt/tomcat/current/bin/startup.sh ExecStop=/opt/tomcat/current/bin/shutdown.sh Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target然后执行:
systemctl daemon-reload systemctl enable tomcat systemctl start tomcat systemctl status tomcat提示:
Type=forking很重要。Tomcat的startup.sh会fork一个子进程后父进程退出,systemd需要知道怎么判断服务启动是否完成。forking模式会检查PID文件来判断启动状态,Tomcat默认把PID写入catalina.pid?实际上Tomcat并不会自己生成PID文件,需要在catalina.sh里设置,或者用Type = oneshot让ExecStart执行完后服务即处于启动状态。更稳妥的做法是在服务文件里加Environment=CATALINA_PID=/opt/tomcat/current/temp/catalina.pid,这样shutdown.sh / systemctl stop才能正确找到主进程PID,否则停止服务时可能出现“kill不到进程”的现象。
我自己实际测试下来,Type=forking+CATALINA_PID是兼容性最好的组合。如果你发现systemctl stop后8080端口还活着,九成是PID文件没配好。
5.2 JVM参数调优:setenv.sh的正确姿势
Tomcat的JVM参数不建议直接改catalina.sh,因为升级版本时这个文件会被覆盖。推荐做法是在bin/下新建一个setenv.sh,Tomcat启动时会自动加载它。
# 文件: /opt/tomcat/current/bin/setenv.sh CATALINA_OPTS="-server -Xms2g -Xmx2g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -Xss512k -XX:+UseConcMarkSweepGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/heapdump.hprof"参数怎么定?核心原则是先看物理内存和业务量级,再反推参数。一台4核8G的机器,系统预留2G,Tomcat堆给2G比较合理。metaspace里放的是类元数据,默认无上限,那反而危险——如果不设MaxMetaspaceSize,出现类加载器泄漏时Metaspace会一路涨到挤爆物理内存。Xss512k是每个线程的栈大小,线程数多了对内存节约明显。
GC策略的选择现在普遍推荐用G1(JDK 9+默认),命令是-XX:+UseG1GC,而不是传统的老式CMS(JDK 14已经移除了CMS)。我上面示例写的是CMS,是因为JDK 8习惯沿用,如果你用JDK 11+,直接用G1就好:
CATALINA_OPTS="-server -Xms2g -Xmx2g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -Xss512k -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/heapdump.hprof"HeapDumpOnOutOfMemoryError这行强烈建议所有生产环境都加上,否则内存溢出后只有一行报错日志,根本没法分析是哪里泄漏。有了heapdump文件,用MAT打开就能看到对象引用链。
5.3 日志体系与日志分析
Tomcat的日志分布在logs/目录下,但很多人分不清每个文件的作用:
| 文件 | 内容 |
|---|---|
| catalina.out | 标准输出和错误输出,最重要的日志主文件,启动异常、System.out.println(不建议生产用)、未捕获异常都会到这里 |
| localhost.log | Host应用的日志,应用启动失败时的堆栈会记在这里 |
| localhost_access_log.*.txt | 访问日志,记录所有HTTP请求,做分析报表、审计的原始数据 |
| manager.log / host-manager.log | Tomcat管理后台的日志 |
默认情况下访问日志是关闭的,需要在server.xml的Host节点里打开。访问日志对排查线上问题非常重要——有没有人扫你的接口、某个异常流量从哪来、某个慢请求耗了多少时间,都能从这里看出来。配置如下:
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t "%r" %s %b %D" resolveHosts="false"/>%D是处理请求耗时(毫秒),排查慢接口非常好用。日志格式中的%D在同级别配置中容易被忽略,强烈建议加上。
日志切割用logrotate,避免catalina.out无限膨胀:
# /etc/logrotate.d/tomcat /opt/tomcat/current/logs/catalina.out { daily rotate 7 copytruncate compress missingok }copytruncate很关键,它先复制日志再清空原文件,不需要重启Tomcat进程,不影响业务。rotate 7保留一周的日志,够用又不占太多磁盘。
5.4 Docker方式部署Tomcat
容器化场景下,Tomcat部署反而更简单。官方镜像tomcat:9.0-jdk8直接可用:
docker run -d \ --name tomcat-app \ -p 8080:8080 \ -v /data/apps:/usr/local/tomcat/webapps \ -e CATALINA_OPTS="-Xms1g -Xmx1g" \ tomcat:9.0-jdk8需要说明几个镜像内的路径变化:官方Tomcat镜像是基于Debian/Ubuntu的,Tomcat主目录是/usr/local/tomcat,不是常见的/opt/tomcat。如果需要自定义配置文件,可以把server.xml挂进去:
docker run -d \ --name tomcat-app \ -p 8080:8080 \ -v /data/conf/server.xml:/usr/local/tomcat/conf/server.xml \ -v /data/apps:/usr/local/tomcat/webapps \ tomcat:9.0-jdk8生产环境用Docker部署时,建议把日志目录也挂到宿主机,否则容器一删日志就没了:
-v /data/logs/tomcat:/usr/local/tomcat/logs镜像版本注意加tag,不要用tomcat:latest,因为latest会随时间漂移——上周的latest是10.1这周就变成11了,应用可能直接起不来。reproducibility是生产环境第一原则。
6. 常见问题与排查技巧实录
6.1 端口占用:明明能启动,但访问就是不通过
症状:startup.sh提示启动成功,但浏览器访问8080超时或显示拒绝连接。常见原因两条:防火墙拦截和端口被其他进程占用。
排查手段:
# 查端口监听状态 netstat -tlnp | grep 8080 # 或 ss -tlnp | grep 8080 # 找到占用进程后 lsof -i :8080如果发现8080被别的服务占用,要么改Tomcat的conf/server.xml端口号,要么处理占用进程。还有一种隐蔽情况——Tomcat启了两次,第二次启动报端口占用但记录在catalina.out里,你以为没起来,其实第一个进程还在正常跑。这时候ps -ef | grep java看一下有几个Java进程。
防火墙这边,CentOS上用firewall-cmd --permanent --add-port=8080/tcp && firewall-cmd --reload放通端口,Ubuntu上检查ufw status。互联网云服务器别忘安全组,这是最容易忽略的一环。
6.2 乱码问题:中文变问号,到底哪里起了作用
Tomcat乱码是个经典问题,但乱码的具体场景不同,解法也不同:
| 现象 | 原因 | 解决方案 |
|---|---|---|
| catalina.out里的日志中文乱码 | 系统默认字符集不是UTF-8 | 在setenv.sh里加JAVA_OPTS="-Dfile.encoding=UTF-8" |
| 网页显示中文乱码 | JSP页面编码或响应头编码不对 | 确保<% page pageEncoding="UTF-8" %>,统一用UTF-8 |
| GET请求参数中文乱码 | 连接器URIEncoding未设置 | server.xml里URIEncoding="UTF-8" |
| POST请求表单乱码 | 请求体编码不对 | 加字符编码过滤器(Spring的CharacterEncodingFilter或手写Filter) |
catalina.out乱码的根源在JVM读取系统默认字符集,Linux下如果locale是POSIX,默认可能是ANSI_X3.4-1968,Java按这个编码输出中文自然变乱码。加-Dfile.encoding=UTF-8是治本。
还有一个容易被忽略的地方:conf/logging.properties里配置的编码。Tomcat 9的logging.properties默认是UTF-8,但如果你在Windows上跳过tar包直接从Windows复制配置,文件本身可能被改成GBK编码,日志就集体乱码。用file命令看一下配置文件的编码,必要时转成UTF-8。
6.3 404错误:“源服务器未能找到目标资源的表示”
开头那句搜索引擎里高频出现的话——源服务器未能找到目标资源的表示或者是不愿公开一个已经存在的——其实就是Tomcat返回的404错误页面。虽然这个描述写得很委婉,但本质是资源不存在。
排查路径按顺序走:
- 部署路径是否正确:访问根路径还是
/项目名/xxx?webapps/下的文件名决定访问路径。把example.war放进webapps后,Tomcat会自动解压成example/目录,访问入口是http://IP:8080/example/,直接访问http://IP:8080/只会看到Tomcat首页。 - 项目的web.xml匹配是否覆盖了你的URL:Servlet的
url-pattern如果只写了/api/*,访问/index.html自然404。 - Spring MVC的路径映射:前后端分离时,后端Controller有没有写
@RequestMapping?路径大小写是否一致?Linux文件系统区分大小写,/Demo和/demo是两个完全不同的路径。
热词里“idea tomcat 描述 源服务器未能找到目标资源的表示”对应的场景——IDEA里启动Tomcat后,浏览器里项目页面显示404,十有八九是IDEA的Application context配错了,看下部署页签里应用的访问根路径和实际请求URL是否匹配。
6.4 内存溢出:OutOfMemoryError怎么定位
生产环境最怕的就是java.lang.OutOfMemoryError。常见形态有两种:
Java heap space:堆内存不够,多发于业务高峰期、缓存未清理、循环创建大对象。Metaspace:类元数据占满,多发于频繁热部署、自定义类加载器没有回收。
盯着日志里的关键字还不够,内存分析得抓现场。在setenv.sh里加了-XX:+HeapDumpOnOutOfMemoryError以后,溢出时自动生成hprof文件,用Eclipse MAT或者JProfiler打开,看支配树(Dominator Tree)里最大的对象是什么,就能快速定位到具体的业务代码。
如果不想等溢出,主动用jmap -dump:live,format=b,file=/tmp/img.hprof PID抓现场,配合jstat -gcutil PID 1000看GC曲线,大概率能找到元凶。
6.5 开发环境里一个容易混淆的“GET链接不能用”
热词里有一条“tomcat get链接不能用|”。展开来讲,这往往是符号链接问题——WAR包部署到webapps目录后,Tomcat自动解压的目录可能是软链接指向外部目录,如果配置了Context docBase指向软链接路径,或者直接把外部目录软链接进webapps,Tomcat默认allowSymlinks是false(JDK 8u191后),访问链接就报404或禁止访问。解决方案有两条路:一是把docBase直接指到真实物理路径,不要软链接;二是确实需要软链接时,在conf/context.xml里给Context加allowLinking="true"。不过从安全角度还是建议用物理路径更稳妥。
7. 安全加固的一些实用建议
Tomcat部署上线前,下面的安全项值得过一遍:
- 修改默认端口:把8080改成不常见端口,虽然防不了真正的扫描器,但能减少大量基础扫描流量。
- 关闭Manager、Host-Manager管理界面:生产环境用不到就删掉
webapps/manager和webapps/host-manager,或者用tomcat-users.xml强制强密码并限制网段访问。 - 隐藏版本信息:404和500错误页面默认会显示Tomcat版本号,攻击者可以据此查找对应漏洞。在
conf/web.xml里加自定义错误页面覆盖掉。 - 尽量减少使用root运行:本文前面建的tomcat账号就是为了这个目的。
安全是个大话题,但最基本的三四条做扎实,能挡掉大部分低水平扫描,把精力集中在真正的攻防对抗上。
8. 一些想说的扩展方向和心得
Tomcat部署这个主题,表面上看是“装个包跑起来”,但实际延伸到工程层面可以串起一整条链路:版本选型、目录规划、参数调优、日志采集、容器化、与Nginx配合做负载均衡、配合Prometheus + Grafana做监控……任何一个环节都值得单独深入。
最后分享一下我个人的体会:部署这类事情,最重要的不是背命令,而是理解“为什么”。为什么清缓存要删work/目录而不是webapps/?为什么改了server.xml要重启才生效?为什么WAR包解压目录不能随便删?想明白这些底层机制,遇到问题就不会慌,因为你已经有了从原因推出结果的能力。以后我自己接手一台陌生服务器,第一件事永远是看版本、看日志路径、看启动脚本——把“现状是什么”摸清楚,再谈“怎么改”。这也是部署老手和新手之间最本质的区别。希望这篇东西能帮你少走几个弯路,跑出自己的第一个稳定服务。