news 2026/10/7 3:08:08

Linux部署Tomcat全流程:JDK、systemd与生产优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux部署Tomcat全流程:JDK、systemd与生产优化

1. 部署前的准备与设计思路

1.1 先搞清楚Tomcat在Linux上到底扮演什么角色

很多新手把Tomcat当成一个“装完就能跑”的Java Web容器,但我更愿意把它看作一个Servlet容器 + HTTP服务器的组合体。你在Linux上装Tomcat,本质上是在搭建一个能接收HTTP请求、把请求转给Java业务代码处理、再把响应返回给客户端的运行环境。它自带HTTP服务器,所以不需要额外装Nginx或者Apache就能对外提供服务,只是并发能力远不如Nginx这类专业Web服务器,生产环境通常会用Nginx做前置反向代理,Tomcat只负责跑应用。

先说清楚Tomcat的适用场景:中小型Java Web应用、内部管理系统、开发测试环境、以及不需要超高并发的生产服务。如果你准备跑高并发业务,最好把Tomcat放在Nginx后面,或者干脆考虑Spring Boot内置容器直接打包成Fat Jar运行。但无论怎么选,理解Tomcat在Linux上的部署方式和运行机制,依然是Java后端开发者的基本功,面试也爱问。

这篇文章适合谁?刚接触Linux运维的Java开发、需要自己搭建测试环境的测试工程师、还有准备部署第一个Java Web项目的学生。我会从零开始,把环境准备、JDK版本选择、目录规划、systemd托管、常见报错排查全部过一遍,尽量把每一步“为什么这么做”讲清楚。

1.2 部署方案选型:不要一上来就apt install

网上很多教程会让你直接apt install tomcat9,我劝你谨慎。系统自带的Tomcat版本通常落后官方好几个小版本,而且包管理器会把配置文件和目录结构按照发行版习惯重新布局,比如Debian系的Tomcat会把CATALINA_HOME拆得很散,webapps默认指到/var/lib/tomcat9/webapps,配置文件在/etc/tomcat9/,日志在/var/log/tomcat9/。这样用起来倒也没问题,但你在网上搜到的很多Tomcat优化资料是基于官方标准目录结构(CATALINA_HOME/bin、CATALINA_HOME/conf、CATALINA_HOME/webapps)写的,路径对不上会让你踩坑。

所以我推荐二进制发布包部署:从Apache官网下载tar.gz压缩包,解压到指定目录,自己掌控一切。这种做法有三个好处:版本完全可控(想用哪个版本就用哪个)、目录结构符合官方文档标准(任何教程都能直接参考)、卸载干净(删目录即可)。代价是你得自己写systemd服务文件或者init脚本,但这并不难,我会在后面给出完整的配置。

部署前先规划几个问题:JDK用哪个版本?用哪个系统用户跑Tomcat?安装目录放哪?是否开启AUTO_DEPLOY?日志怎么管理?这些看似小事,实际影响后面的维护体验。我见过太多生产事故源于“部署时图省事用root启动Tomcat”或者“两个应用挤在同一个Tomcat里没做隔离”。先把方案想清楚再动手,比啥都强。

1.3 JDK版本选择:8还是11,OpenJDK还是Oracle

Tomcat是Java写的,所以第一件事是装JDK。Tomcat 8.5和9.0要求Java 7+,但实际生产推荐Java 8;Tomcat 10.0+要求Java 8+,推荐Java 11;Tomcat 10.1彻底转向Jakarta EE 9+,要求Java 11+。

这里有个非常容易踩的坑:Tomcat 10之后包名从javax.*变成了jakarta.*。如果你用的是Tomcat 9或者更早,代码里import javax.servlet.*没问题;但用Tomcat 10.0及以上,旧代码直接无法运行,必须改用jakarta.servlet.*。所以对大多数存量项目,老老实实选Tomcat 9 + JDK 8或者Tomcat 9 + JDK 11,别追新给自己找事。

JDK本身选OpenJDK就行,Oracle JDK在Java 8之后不再免费提供商业授权,没必要折腾。Ubuntu/Debian可以直接apt install openjdk-11-jdk,CentOS/RHEL用yum install java-11-openjdk-devel,云服务器也可以用云厂商的镜像源,装完用java -version验证。

有个细节容易被忽略:编译和运行JDK版本不一致导致的ClassNotFoundException或UnsupportedClassVersionError。本机开发用JDK 17编译,服务器只装了JDK 8,Class文件版本号对不上,启动直接抛异常。所以部署前务必确认应用编译的目标版本,服务器JDK版本不低于编译版本,但如果低于编译的Target版本,会直接报UnsupportedClassVersionError。

2. 在Linux上安装JDK并验证环境

2.1 安装OpenJDK并配置JAVA_HOME

这里以Ubuntu 22.04 LTS、CentOS 7.9两个主流环境为例。Debian/Ubuntu系列用apt,RedHat/CentOS系列用yum或dnf,底层逻辑都一样,先更新软件源再安装。

Debian/Ubuntu:

sudo apt update sudo apt install -y openjdk-11-jdk java -version

RedHat/CentOS:

sudo yum install -y java-11-openjdk-devel java -version

验证出来的输出类似这样:

openjdk version "11.0.21" 2023-10-17 OpenJDK Runtime Environment (build 11.0.21+9-post-Ubuntu-0ubuntu122.04) OpenJDK 64-Bit Server VM (build 11.0.21+9-post-Ubuntu-0ubuntu122.04)

JDK装好后,JAVA_HOME环境变量通常不会自动设置。Tomcat启动脚本会优先找JAVA_HOME,找不到就找JRE_HOME,再找不到就尝试直接用java命令。为了让Tomcat稳定找到JDK,建议手动配置:

# 先找到JDK安装的真实路径 sudo update-alternatives --config java # 或者直接看 readlink -f $(which java)

然后编辑/etc/profile或者当前用户~/.bashrc:

export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 export PATH=$JAVA_HOME/bin:$PATH

执行source /etc/profile使其生效,再用echo $JAVA_HOME验证。

你可能会问:既然Tomcat找不到JAVA_HOME也能用java命令跑,为啥还要配?我的经验是:配好JAVA_HOME能让你在写systemd服务文件时明确指定JDK路径,避免系统有多个JDK版本时Tomcat被意外切换到错误的JDK上。尤其服务器上同时装JDK 8和JDK 17的时候,不显式指定路径,Tomcat可能用错版本,启动时各种诡异报错,排查半天才发现是JDK冲突。

2.2 创建专用系统用户与目录规划

线上环境千万别用root跑Tomcat。Tomcat进程如果有漏洞被利用,拿到的是root权限,整个服务器都危险。主流做法是创建一个无登录权限的系统用户,只给它读写Tomcat目录的权限。

sudo useradd -r -s /sbin/nologin -d /opt/tomcat tomcat sudo mkdir -p /opt/tomcat sudo chown -R tomcat:tomcat /opt/tomcat

-r表示创建系统用户(UID小于1000),-s /sbin/nologin禁止该用户登录Shell,-d /opt/tomcat指定家目录。这样即使Tomcat被入侵,也无法直接拿到Shell权限。

目录我习惯这样规划:

路径用途说明
/opt/tomcat安装主目录(CATALINA_HOME)Tomcat二进制文件、配置、Web应用全在这
/opt/tomcat/logs日志目录默认就有,建议软链接到独立数据盘便于日志清理
/data/backup备份目录定期打包webapps和conf,出问题能快速回滚

生产环境最好把logs目录挂到独立磁盘上,避免日志把系统盘撑爆。云服务器的话,把数据盘挂载到/data,再把logs软链过去:

sudo mkdir -p /data/tomcat_logs sudo chown -R tomcat:tomcat /data/tomcat_logs sudo mv /opt/tomcat/logs /data/tomcat_logs sudo ln -s /data/tomcat_logs /opt/tomcat/logs

这一步我在刚做运维的时候完全没想到,直到一次磁盘告警把我从半夜喊醒——Tomcat的localhost_access_log每天几百MB,三个月没清理,磁盘满了。

3. 下载解压Tomcat与核心配置

3.1 获取Tomcat二进制包并校验完整性

到Apache官网的Tomcat下载页,选择你需要的版本。注意两个镜像地址:一个是Apache官方镜像(https://dlcdn.apache.org/tomcat/),一个是备份镜像(https://archive.apache.org/dist/tomcat/)。dlcdn只有最新版本,历史版本要去archive找。

截至我写这篇文章时,Tomcat 9.0.x仍是Java Web项目最常见的生产版本。下载时选tar.gz而不是zip,然后顺手下载同目录下的sha512校验文件:

cd /tmp wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.85/bin/apache-tomcat-9.0.85.tar.gz wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.85/bin/apache-tomcat-9.0.85.tar.gz.sha512 sha512sum -c apache-tomcat-9.0.85.tar.gz.sha512

输出出现OK再解压。这一步看着多余,但对生产环境太重要了——软件包可能被劫持或下载不完整,解压时出问题会让你误以为自己的操作有错,实际上就是包坏了。我有一次在公司内网下载Tomcat,镜像源同步了一半,校验值对不上,解压后启动直接找不到类,排查了半小时才想到是压缩包损坏。从那以后,校验这步再也没跳过。

解压并移动到目标目录:

sudo tar -zxvf apache-tomcat-9.0.85.tar.gz -C /opt/tomcat --strip-components=1

--strip-components=1会去掉压缩包里的顶层目录名(apache-tomcat-9.0.85),直接把内容解到/opt/tomcat。这样CATALINA_HOME不带有版本号路径,以后升级Tomcat时不用改一堆配置文件里的路径。

3.2 配置Tomcat环境变量与管理脚本

虽然Tomcat启动脚本本身会推测环境变量,但我强烈建议在/etc/tomcat目录下单独建一个环境变量文件,让systemd能读到:

sudo mkdir -p /etc/tomcat

编辑/etc/tomcat/tomcat.conf:

JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 CATALINA_HOME=/opt/tomcat CATALINA_BASE=/opt/tomcat CATALINA_TMPDIR=/tmp JAVA_OPTS="-Djava.awt.headless=true -Dfile.encoding=UTF-8 -server -Xms512m -Xmx1024m" CATALINA_PID=/var/run/tomcat.pid

这里解释几个关键项:

  • CATALINA_HOME是Tomcat二进制安装目录,CATALINA_BASE是实例运行目录。单实例默认两者相同,多实例部署时把BASE指向不同目录,实现一套二进制跑多个应用实例,后面有时间专门写一篇。
  • JAVA_OPTS里-Xms512m -Xmx1024m是Java堆内存初始值和最大值。生产环境建议把Xms和Xmx设为相同值,避免堆动态伸缩带来的GC开销。如果服务器内存8G,Tomcat独占1G~2G比较合理。
  • CATALINA_PID让systemd能精确获取Tomcat主进程PID,停止服务时更可靠。

然后写systemd服务单元文件。使用systemd托管Tomcat是CentOS 7+和Ubuntu 16.04+的标配方式,优点是开机自启、崩溃自动拉起、日志统一交给journald管理。

编辑/etc/systemd/system/tomcat.service:

[Unit] Description=Apache Tomcat Web Application Container After=network.target [Service] Type=forking User=tomcat Group=tomcat EnvironmentFile=/etc/tomcat/tomcat.conf Environment="CATALINA_HOME=/opt/tomcat" Environment="CATALINA_BASE=/opt/tomcat" Environment="CATALINA_TMPDIR=/tmp" ExecStart=/opt/tomcat/bin/startup.sh ExecStop=/opt/tomcat/bin/shutdown.sh SuccessExitStatus=143 Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target

Type=forking的意思是启动脚本会fork出子进程后退出,systemd跟踪那个派生出来的主进程。SuccessExitStatus=143是因为Tomcat默认关闭脚本在停止进程时返回143(128+15,SIGTERM),如果不声明这个退出码,systemd会认为服务停止失败。

配置完成后:

sudo systemctl daemon-reload sudo systemctl enable tomcat sudo systemctl start tomcat sudo systemctl status tomcat

看到active (running)就说明服务起来了。但别急着高兴,先验证端口再部署应用。

3.3 验证启动与访问管理页面

Tomcat默认监听8080端口,启动完成后检查端口:

ss -tlnp | grep 8080

输出应该包含java进程监听0.0.0.0:8080。然后看启动日志:

sudo tail -f /opt/tomcat/logs/catalina.out

正常情况下能看到:

INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/opt/tomcat/webapps/docs] INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/opt/tomcat/webapps/docs] has finished INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [xxx] milliseconds

浏览器访问http://服务器IP:8080/,能看到默认页面就说明部署成功。默认页面右上角有“Server Status”、“Manager App”、“Host Manager”三个入口,点进去会让你输入账号密码。默认状态下Tomcat是没有任何管理账号的,这需要单独配置。

这里说个典型新手困惑:Tomcat默认页面里的管理入口点进去报403。原因是Tomcat 8之后的webapps/manager、webapps/host-manager默认只允许本机IP访问。这是安全设计,不是故障。要解除限制得改conf/manager.xml或者conf/context.xml的Valve配置,下面会专门讲。

4. 核心配置:端口、线程池、内存与管理账号

4.1 server.xml端口详解与调整场景

Tomcat的conf/server.xml是核心配置文件,里面定义了三种通信端口。初学者经常把8080和8005、8009搞混,这里一次讲透:

端口配置项默认端口作用出现场景
<Connector port="8080"8080HTTP服务端口,处理用户浏览器请求对外提供Web服务
<Connector port="8009"8009AJP协议端口,接受Apache通过mod_jk转发的请求Nginx/Apache前置反向代理到Tomcat
<Server port="8005"8005Tomcat管理端口,接收SHUTDOWN指令执行shutdown.sh时连接这个端口

有个安全注意事项:8005端口不要暴露到公网。它只接受一句SHUTDOWN指令,如果被外部访问到,任何人都能远程关闭Tomcat。建议在云安全组里放行格式只开8080(如果直接用Tomcat对外服务),8005和8009只允许内网IP访问,或者干脆改掉默认端口号。

调整端口也很简单,找到server.xml里对应Connector改port属性:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxThreads="200" minSpareThreads="10" maxPostSize="10485760"/>

这里额外挤出一个问题:8080端口被占用怎么办?常见的是服务器上同时跑了别的Java应用也用了8080,或者之前没停干净的Tomcat还占着端口。先ss -tlnp | grep 8080看是哪个进程占用,确认是旧的Tomcat就kill掉;如果是别的程序,就改server.xml里的端口。

4.2 线程池与连接器参数优化

server.xml里的Connector参数直接决定Tomcat能扛多少并发。默认配置比较保守,遇到稍微大一点的流量就会看到请求排队。

生产环境我一般这样配:

<Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="300" minSpareThreads="20"/> <Connector port="8080" protocol="HTTP/1.1" executor="tomcatThreadPool" connectionTimeout="20000" keepAliveTimeout="15000" maxKeepAliveRequests="1" maxPostSize="10485760" enableLookups="false" acceptCount="200" compression="on" compressionMinSize="1024" noCompressionUserAgents="gozilla, traviata" compressibleMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/javascript"/>

逐个解释关键参数的选择逻辑:

  • maxThreads="300":最大处理线程数。不是越大越好,每个线程都占用内存,300对于4C8G的机器已经偏激进,8C16G可以调到500。超过这个数,后面的请求进入acceptCount排队。
  • acceptCount="200":排队等待的请求数量。超过maxThreads+acceptCount之后,新请求会被拒绝,客户端直接收到Connection refused,而不是干等。
  • maxKeepAliveRequests="1":控制长连接最多复用次数。设为1是让每个连接处理完就断开,这对某些浏览器和服务端配合不佳的场景反而有效——避免一个长连接占着线程不释放。但注意,这会增加TCP握手次数,对静态资源少的纯动态接口影响不大。
  • compression="on":开启Gzip压缩,对文本类资源(HTML、CSS、JS)效果明显,压缩率通常70%以上,能大幅减少带宽消耗。但注意,如果前置了Nginx且已经开启压缩,这里建议关闭,否则双重压缩浪费CPU。

这些参数不是随手填的,网上找一份压测报告对拍一下,然后用ab或wrk做一轮压测,观察线程池和响应时间变化,再做微调。没有压测就按默认值先跑,别在生产上一上来就调得太激进。

4.3 配置JVM内存参数避免OOM

Java应用程序最常见的问题之一就是内存溢出。Tomcat默认JVM堆内存比较保守(取决于机器内存,通常1/4左右),很多部署上去没跑几天就OutOfMemoryError,撑爆堆内存。

推荐在/etc/tomcat/tomcat.conf里显式配置:

JAVA_OPTS="-server -Xms1024m -Xmx1024m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/tomcat/logs/heapdump.hprof"

-Xms和-Xmx建议相等,避免JVM频繁扩容和缩容堆内存,降低GC停顿。Metaspace是JDK 8之后替代PermGen的元数据区域,如果不限制,有的应用会产生大量动态类导致元空间无限增长直至物理内存耗尽。-XX:+HeapDumpOnOutOfMemoryError是救命参数——OOM时自动导出堆快照文件,后续用MAT分析定位哪个对象爆了。

还有个容易混淆的参数:CATALINA_OPTS和JAVA_OPTS。Tomcat启动脚本里两者的区别是:JAVA_OPTS会传给所有Java子进程(包括启动脚本本身),CATALINA_OPTS只传给Tomcat主进程。严格来说应该把Tomcat专用的配置放CATALINA_OPTS,把JDK全局属性放JAVA_OPTS。但实际使用中,大多数场景两者混用也不会出问题,因为Tomcat没有别的子进程。我自己的习惯是:内存和GC配置放JAVA_OPTS,这样即使以后脚本调整也不容易被覆盖。

4.4 配置管理账号与Manager应用访问限制

默认Tomcat没有管理账号,改配置文件前先看懂结构。控制管理页面的配置文件是conf/tomcat-users.xml,在<tomcat-users>标签内添加:

<role rolename="manager-gui"/> <role rolename="manager-script"/> <role rolename="admin-gui"/> <user username="admin" password="你的强密码" roles="manager-gui,admin-gui"/>

Tomcat定义了多个角色,常用这几个:

  • manager-gui:允许访问/manager/html(图形化部署管理页面)
  • manager-script:允许访问/manager/text(脚本化部署,支持API方式上传War包)
  • manager-jmx:允许通过JMX管理
  • admin-gui:允许访问/host-manager/html(虚拟主机管理页面)

配置完重启Tomcat,登录Manager页面就能在线部署、卸载、重载Web应用。

但这里就是你们热词里搜到的问题——“tomcat后台页面上传war被限制ip”。Tomcat 8/9的webapps/manager/META-INF/context.xml和webapps/host-manager/META-INF/context.xml里默认写了:

<Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />

这个Valve的意思是只允许本机IP(127.0.0.1和IPv6 ::1)访问Manager页面。所以你从远程浏览器访问Manager时,即使账号密码全对,也会看到403 Access Denied。

解决方案有两种。如果你确定服务器在内网且只给自己用,可以直接注释掉Valve。但如果你有安全洁癖,更好的做法是只放开你办公网的固定IP:

<Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="192\.168\.1\.\d+|127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />

注意IP匹配用的是正则,192.168.1.后面的\\d+表示任意数字。修改后重启Tomcat生效。

5. 部署Web应用与生产环境实践

5.1 War包部署的三种方式

Web应用在Tomcat有三种部署方式:自动部署、指定位置部署、在线部署。

方式一,最简单:把xxx.war放到webapps目录,Tomcat启动(或reload)后自动解压并部署。因为配置里的autoDeploy="true"默认开启,目录发生变更会自动触发部署。但我必须提醒:生产环境慎用自动部署——你往webapps丢一个半成品war包,Tomcat立刻会尝试部署,如果包结构不对,可能污染当前运行的应用状态。我见过有人误传一个损坏的war包,结果Tomcat直接部署失败,还影响到了同目录其他应用。

方式二,改server.xml的Host标签:

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="false"> <Context path="/myapp" docBase="/data/apps/myapp.war" /> </Host>

这种方式的优势是Web应用可以放在Tomcat目录之外,并且指定路径访问。比如把多个应用分散到不同磁盘目录,避免挤在同一个webapps里。autoDeploy="false"意味着只有重启Tomcat或者手动reload才会加载新版本,适合发布流程严格控制的场景。

方式三,通过Manager页面在线部署。登录/manager/html后,在“Deploy”区域选择war包上传,Tomcat会直接部署。这是最方便的方式,但刚才说过,别忘记解决Manager的IP限制问题。还有一种脚本化方式:用curl执行manager-script上传:

curl -u admin:密码 "http://localhost:8080/manager/text/deploy?path=/myapp" \ -F "file=@/tmp/myapp.war"

推荐在CI/CD流水线里用这种方式发布,比你手动上传省事得多,也更容易集成到Jenkins或者GitLab CI里。

5.2 context.xml修改Servlet版本与目录权限

部署过程中还有一个高频坑:应用打包时的Servlet版本高于Tomcat支持的版本。比如你本地用Tomcat 10开发,打出来的war包里web.xml声明了version="5.0"(Servlet 5.0对应Tomcat 10),但服务器是Tomcat 9(只支持Servlet 4.0),部署时日志就会报:

org.apache.catalina.startup.HostConfig deployWAR Error deploying web application archive java.util.concurrent.ExecutionException: org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[...]] Caused by: java.lang.IllegalArgumentException: Servlet [xxx] in web应用程序 [/myapp] 与 JSP 版本不匹配

解决办法:要么把服务器换成Tomcat 10,要么把war包里的web.xml声明改回4.0(或者直接删掉web.xml里的version声明让Tomcat按默认值处理)。更彻底的做法是检查开发环境pom.xml里的Servlet API依赖版本,确保编译目标与运行时容器一致。

还有权限问题。用tomcat用户运行服务时,war包的属主必须是tomcat,否则部署时无法写入解压目录:

sudo chown -R tomcat:tomcat /data/apps/

注意:Tomcat对war包的归属有严格检查,属主不对会报Access is denied异常,表现为部署超时或目录创建失败。别问我怎么知道的,我第一次部署时忘了chown,盯着日志看了一下午。

5.3 多实例部署与session共享的架构考量

如果你的服务器内存充足,一个Tomcat扛不住所有流量,可以考虑多实例部署。比如在/opt/tomcat安装二进制文件,然后建/opt/tomcat-instance1、/opt/tomcat-instance2,每个目录只放conf、logs、webapps、temp、work,通过CATALINA_BASE区分:

mkdir -p /opt/tomcat-instance1/{conf,logs,webapps,temp,work} cp -r /opt/tomcat/conf/* /opt/tomcat-instance1/conf/

然后修改第二个实例的server.xml端口(8081和8006),修改CATALINA_BASE环境变量后启动:

CATALINA_BASE=/opt/tomcat-instance1 /opt/tomcat/bin/startup.sh

多实例部署的好处是应用隔离——一个应用OOM不会连带把另一个拖垮。代价是维护成本变高,每个实例都要单独管理,而且如果多个实例间需要共享Session,还得引入Redis等外部Session存储。

多实例模式下,共享会话一般这样规划:引入Redis Session Manager,把Session信息从Tomcat内存挪到Redis。这样多台Tomcat之间可以共享登录状态,前端负载均衡随便转发。简单说,不用改业务代码,只加依赖和配置:

  • 下载tomcat9-redis-session-manager相关jar包放进lib
  • 在context.xml中添加Valve和Manager配置
  • 重启Tomcat

这个方案我实际部署过,注意Redis的maxIdle连接数和Tomcat线程数要匹配,否则高并发时Redis连接池会成为瓶颈。另外,Session序列化器务必统一用JAVA序列化或者JSON,别一半实例用Java一半用JSON,那样Session无法共享。

5.4 小贴士:日志轮转与监控

Tomcat日志默认不会自动轮转切割,catalina.out会无限增长,时间长了能把磁盘塞满。生产环境要么用Linux自带的logrotate,要么在cron里写脚本定期切割。

简单可靠的方案是配置logrotate。创建/etc/logrotate.d/tomcat:

/data/tomcat_logs/*.out { daily rotate 7 compress delaycompress missingok copytruncate notifempty }

copytruncate是重点——它先拷贝日志再清空原文件,Tomcat进程不重启也能完成切割。rotate 7保留7天日志,compress压缩旧日志。如果没有logrotate,至少写个cron任务,超过500MB就清空一次。

监控方面,至少盯三个指标:8080端口存活检测(curl -I http://localhost:8080/)、JVM堆内存使用率、线程数。轻量方案是zabbix或者Prometheus + node_exporter,再加一个黑盒探针探HTTP状态码。别等用户报障再去查catalina.out,那样太被动。

6. 常见问题与排查技巧实录

6.1 Tomcat启动失败/启动慢的定位思路

Tomcat启动失败,我先看日志,再看端口,最后看权限。基本排查链路是:

  1. 查服务状态:systemctl status tomcat -l
  2. 查日志:tail -100 /opt/tomcat/logs/catalina.out
  3. 查端口:ss -tlnp | grep 8080
  4. 查Java进程:ps -ef | grep java
  5. 查目录权限:ls -ld /opt/tomcat/*

启动慢的常见原因:/dev/random熵池不足导致UUID生成卡顿、DNS反解超时、大量应用部署启动。针对熵不足,可以在catalina.sh的JAVA_OPTS里加:

-Djava.security.egd=file:/dev/urandom

这个参数的原理是让JVM从/dev/urandom而不是/dev/random读取随机数。/dev/random在熵池耗尽时是阻塞的,导致Tomcat生成Session ID时卡住几秒钟甚至几十秒;/dev/urandom则不会阻塞。对非加密应用,安全性几乎没有差异,但启动速度明显提升。

DNS反解问题:Connector默认enableLookups="false",但有的应用日志里还是会看到连接客户端IP反解。检查server.xml里Connector是否加了enableLookups="false",没加就加上。

6.2 war包部署失败的5类高频报错

我整理了这几年遇到最多的部署报错,做成速查表供你对照:

报错特征根因解决方案
ZipException: invalid LOC headerwar包损坏或不完整重新打包上传,校验MD5
UnsupportedClassVersionError编译版本高于JVM版本降低编译target或升级JDK
NoClassDefFoundError依赖jar缺失或冲突检查WEB-INF/lib,用dependency tree排查
java.lang.IllegalArgumentException: Document base ... does not existContext的docBase路径错误检查server.xml/context.xml路径是否正确且属主一致
org.apache.jasper.JasperException: Unable to compile class for JSPJSP编译环境问题确认JAVA_HOME已配置,编译临时目录可写

其中NoClassDefFoundError最让人头疼。如果你在开发环境运行正常,传到Linux服务器上却报类找不到,多半是依赖冲突——多个jar包里有同名类,Tomcat的类加载机制按照“web应用lib优先、父类加载器次之”的顺序,加载到了错误版本。排查办法:找到报错类名,去WEB-INF/lib里sAll一下,看有没有多个jar包含相同包名,用jar tf xx.jar | grep 类名定位。

有时候还得开启Tomcat的类加载器调试日志:

-Dorg.apache.catalina.loader.WebappClassLoaderBase.DEBUG=true

加了之后catalina.out会刷出大量类加载信息,虽然啰嗦,但能直观看到每个类的来源jar包。

6.3 双亲委派机制与Tomcat类加载的相爱相杀

热词里出现“tomcat打破双亲委派机制”,这确实是个经典问题。JVM默认的双亲委派模型(Parent Delegation Model)是:类加载器收到加载请求时,先委托给父加载器,父加载器找不到才自己加载。这样做避免类被重复加载,也保证核心类不被篡改。

但Tomcat为了做到Web应用之间类隔离(你的应用A想用Spring 4,应用B想用Spring 5,互不干扰),打破了双亲委派机制:每个Web应用有一个独立的WebappClassLoader,它遵循“先自己加载,再委托父加载器”的顺序。具体规则是:

  1. 先检查WEB-INF/classes目录的类
  2. 再检查WEB-INF/lib目录的jar包
  3. 找不到才委托给父加载器(CommonClassLoader)加载共享库

这个机制带来的好处是应用隔离,坏处是如果你把一些本该共享的公共jar(比如数据库驱动、日志库)也打进了WEB-INF/lib,每个应用各加载一份,就会出现多个同名实例,静态变量不共享、类比较不相等、ClassCastException莫名其妙地冒出来。

一个典型的ClassCastException场景:应用A和B都包含log4j的jar,你在Tomcat的lib目录放一份,应用里也放一份,然后应用代码里强转Logger类型,就会报类型不匹配。解决办法:公共库放CATALINA_HOME/lib,应用私有库留在WEB-INF/lib,不要重复。

这个机制也解释了为什么修改WEB-INF/lib里的jar后需要重启Tomcat——重新部署不等于重新加载类,旧类还驻留在PermGen/Metaspace里,只有重启才能彻底清理。

6.4 磁盘满与日志清理实战

我前面提过日志撑爆磁盘的事故,再补充一个具体案例:某天晚上收到磁盘告警,df -h一看/dev/vda1使用率99%。排查先挂/opt/tomcat/logs目录,du -sh *发现catalina.out已经7.8G。为什么这么大?因为应用里有一段异常日志打印了堆栈,每次执行SQL语句都往Error里写一次,QPS稍微一上去,日志就疯长。

处理过程:先清理在线日志,然后用logrotate规则限制大小。清理命令:

# 立即截断当前日志文件,不重启Tomcat sudo -u tomcat sh -c 'cat /dev/null > /opt/tomcat/logs/catalina.out' # 删除已轮转的旧日志 sudo find /data/tomcat_logs/ -name "*.out.*" -mtime +7 -exec rm {} \;

用cat /dev/null而不是rm,是因为Tomcat进程还开着这个文件句柄,rm删掉后进程依然往那个inode写数据,磁盘空间根本不会释放,只有重启进程才释放。很多人删除日志后df显示空间没变,就是这个原因。cat /dev/null则保证原文件被清空,进程继续正常写入。

做完后再去代码层面修掉疯狂打日志的问题——否则你再怎么清理,日志还是会涨回来。这也是我坚持在生产环境配置logrotate而不是依赖人肉维护的原因。

6.5 端口冲突和进程残留的终极排查

部署Tomcat最烦的是“明明stop了,端口还是被占用”。Tomcat的shutdown脚本依赖8005端口发指令,如果某些异常情况下进程无响应,shutdown脚本会失败,主进程还在,端口自然还在。这时候得手工处理:

# 查出占用8080端口的PID sudo lsof -i :8080 # 或者 sudo ss -tlnp | grep :8080 # 杀掉该进程 sudo kill -9 <PID> # 清理pid文件 sudo rm -f /var/run/tomcat.pid

还有种情况是shutdown脚本已经成功,但是日志里出现Failed to destroy the connection,实际上进程挂了但端口还没释放,等待几分钟再查。如果多实例部署,注意检查每个实例的CATALINA_BASE/tomcat.pid是否写入了各自的PID,避免停止一个实例把另一个也杀了。

至于Tomcat重启后端口变了?(实际不会,除非你改错了server.xml),检查conf/server.xml是否真的生效,用grep -E "port=|Connector" /opt/tomcat/conf/server.xml确认。

7. 从个人实践角度给新手部署者的几点建议

说句实在话,Tomcat安装部署的难度不算高,真正考验人的是遇到问题时的排查思路。我自己带过不少新人,发现他们在Tomcat部署上的通病是“只知道跟着教程复制粘贴,出了问题就懵了”。所以最后分享几条个人经验:

第一,部署前先把server.xml和catalina.sh这两个文件通读一遍,不求全看懂,但至少知道哪些参数在哪。很多问题其实都不复杂,比如端口被占用、线程池配小、内存参数写错,你如果熟悉配置文件,一眼就能发现问题根源。

第二,所有操作前先备份。改配置前拷贝一份server.xml、context.xml、catalina.sh到/opt/tomcat/backup,改坏了随时还原。别嫌麻烦,这个习惯救过我无数次。

第三,生产环境永远用systemd托管服务,不要在终端里直接跑startup.sh然后关掉SSH窗口就完事。那样Tomcat会随着Shell退出而终止,除非你用了nohup。systemd不仅能开机自启,还能在进程崩溃后自动拉起,这才是线上服务应有的待遇。

第四,日志别省着看。catalina.out是排查问题的第一线索,很多人出问题第一反应是Google,其实答案就在日志里。学会看日志、查端口、看进程,是Linux运维的基本功,比背多少命令都有用。

最后再补一个我常用的小技巧:部署完成后写一个验证脚本,一键检查所有关键项:

#!/bin/bash echo "===== Java版本 =====" java -version 2>&1 echo "" echo "===== Tomcat进程 =====" ps -ef | grep catalina | grep -v grep echo "" echo "===== 端口监听 =====" ss -tlnp | grep -E "8080|8005" echo "" echo "===== 健康检查 =====" curl -s -o /dev/null -w "HTTP状态码: %{http_code}\n" http://localhost:8080/ echo "" echo "===== 磁盘使用 =====" df -h /opt/tomcat | tail -1

这个脚本花不了几分钟,但每次部署完跑一遍,心里有底得多。线上环境出问题时,第一件事永远是先收集信息再动手改配置,而不是一上来就乱改server.xml。希望这篇文章能帮你少踩几个坑,把Tomcat部署这件事从“玄学”变成“常规操作”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 3:08:07

苍穹外卖Day02:本地上传图片完整链路与常见坑解析

苍穹外卖这套项目我前前后后给不少人讲评过&#xff0c;自己也完整复现过不止一遍。到了Day02这个阶段&#xff0c;项目算正式从"登录跑通"进入到"业务铺开"的关键节点&#xff1a;员工管理、分类管理、菜品管理三大模块全部在这天落地&#xff0c;同时会碰…

作者头像 李华
网站建设 2026/10/7 3:06:59

健康管理系统后端架构设计与技术选型实战

1. 项目复盘&#xff1a;健康管理系统后端比想象中复杂1.1 看似简单&#xff0c;实际涉及的功能闭环接手健康管理系统这个后端项目时&#xff0c;产品经理手里只有两张原型图&#xff1a;一张是用户填写健康问卷的页面&#xff0c;一张是展示健康评估报告的页面。说实话&#x…

作者头像 李华
网站建设 2026/10/7 3:06:48

Linux网络基础:协议栈、TCP握手与排障命令详解

有朋友问我&#xff1a;Linux网络到底该怎么学&#xff1f;是不是把常用命令背熟就够了&#xff1f;我说&#xff0c;命令只是手&#xff0c;协议栈才是骨架。今天这篇就把Linux网络基础中最容易绕晕的部分——协议与网络传输基本原理——摊开讲一遍&#xff0c;从数据包怎么封…

作者头像 李华
网站建设 2026/10/7 3:04:07

支付宝沙箱接入全流程:从环境配置到回调处理,避开常见坑

“支付宝沙箱”在我第一次接触时&#xff0c;被它吓到了。总觉得要申请一堆资质&#xff0c;要对接各种证书&#xff0c;代码要写得很复杂&#xff0c;甚至要先跟支付宝的技术支持聊上一轮。但等我真正做完一个项目之后回头再看&#xff0c;整个接入过程其实可以压缩成两件事&a…

作者头像 李华