☰
Linux下Tomcat部署与调优全流程:从JDK匹配到systemd托管
2026/10/11 18:01:12 网站建设 项目流程

搞Linux下部署Java应用,Tomcat基本是绕不开的一环。不管是给老项目做个迁移,还是新架构里暂时需要一个Servlet容器,把Tomcat在Linux上装好、调好,是所有后续工作的地基。这篇文章我直接把我反复部署过多次的完整流程写出来,从JDK版本的匹配、目录结构的规划,到server.xml的核心参数、JVM内存调整,再到用systemd托管、最后出一份排查日志,全程都是可以直接照着抄的实操记录。适合刚接触Linux运维的开发者,也适合被Windows思维困扰、想在Linux上正经跑服务的人。

1. 装之前先把环境盘明白

很多人上手就在/root下解压、启动,结果后面权限、开机自启、日志轮转全踩坑里。这个环节我先说说Linux服务器上装Tomcat之前必须做的几个决定,想清楚再动手,后面能省一整天排查的功夫。

1.1 JDK版本和Tomcat版本怎么匹配

Tomcat本身是用Java写的,运行Web应用也得靠Java运行时,所以JDK是第一依赖。这里先记住一条铁律:JDK大版本和Tomcat主版本不是随意组合的。Tomcat 10之前是javax.*命名空间,Tomcat 10开始迁移到了jakarta.*,如果不匹配,应用启动时直接抛ClassNotFoundException,根本跑不起来。

常见组合我整理一下:

Tomcat版本适配JDK说明
Tomcat 8.5.xJDK 7 以上(推荐JDK 8)老项目的主力选择,官方对JDK 8支持最好
Tomcat 9.0.xJDK 8 以上目前最多生产环境用的版本,稳定、资料多
Tomcat 10.0.xJDK 8 以上jakarta.*命名空间,新项目直接用
Tomcat 11.0.xJDK 11 以上需要更高版本JDK,适合新框架配套

我自己的习惯是:老项目用Tomcat 9 + JDK 8,新项目用Tomcat 10.1 + JDK 11(或17)。JDK 8虽然老,但很多存量业务的字节码、第三方Jar都还停留在那个时代,升JDK这事牵扯面太大,没有必要求新。选版本之前一定先确认项目的javax还是jakarta。

1.2 Linux下的运行用户和目录规划

在Windows上装Tomcat很多人图方便,直接用管理员身份双击startup.bat,但Linux是多人多权限系统,用root跑Tomcat有真实的风险:一旦Web应用被攻破,进程权限就是root,攻击者可以直接读取服务器上的所有文件,甚至替换系统命令。所以生产服务器上一定要单独建一个专用低权限用户,比如tomcat,只给这个用户部署目录的读写权限。

目录规划也建议一步到位,不然后面日志、备份、升级都难受。我习惯在/opt下统一管:

/opt/tomcat # 软链接,指向具体版本目录 /opt/tomcat/apache-tomcat-10.1.23 # 实际解压目录 /opt/apps/webapps # 存放war包源码包,与Tomcat解压目录分离 /home/tomcat/logs # 日志统一外置(可选)

解压目录和webapps分开的意义在于:升级Tomcat时只需要改软链接,业务war包不被动;备份时只打包一个目录,不用把Tomcat自带文件全搞一遍。有强迫症的同仁可以再建一个/opt/tomcat-backup,大版本升级前把conf、webapps、logs三个目录整体备份过去就行。

1.3 创建专用用户和基础目录

创建用户的标准姿势如下:

useradd -r -s /sbin/nologin tomcat

-r表示创建系统用户,-s /sbin/nologin是禁止登录shell,既满足运行需要,又减少被爆破的风险面。这个用户不需要密码,也不能用于SSH登录。

然后建目录并授权:

mkdir -p /opt/apps/webapps chown -R tomcat:tomcat /opt/tomcat /opt/apps

注意授权范围要具体:给/opt/tomcat授权没毛病,但千万别顺手chmod -R 777,给足业务目录的属主权限就够了。要是后面部署war包时tomcat用户写入失败,先查目录属主,而不是直接放宽权限,权限太松是Linux运维里最常见的“为了快而加速爆炸”操作。

2. Tomcat下载、解压与启动验证全流程

环境规划完,下面进入实操主线。这里要安装的步骤不复杂,但每一步我都解释一下为什么这么做,顺便把常见误区点出来。

2.1 下载Tomcat和JDK

先说JDK。现在主流发行版源里可能带OpenJDK,比如:

apt install openjdk-8-jdk # Debian/Ubuntu yum install java-1.8.0-openjdk # CentOS/RHEL

但我一般不直接用发行版源的JDK,更喜欢从官方渠道下载对应版本的tgz包,理由有两个:一是发行版源的JDK安装路径太散,找JAVA_HOME有时候要靠which java+ 猜路径;二是项目如果对JDK小版本敏感,统一在/usr/local/java下管理版本切换更灵活。

JDK安装简化为三步:

tar -zxvf jdk-8u202-linux-x64.tar.gz -C /usr/local/java/ ln -s /usr/local/java/jdk1.8.0_202 /usr/local/java/current

随后写进/etc/profile:

export JAVA_HOME=/usr/local/java/current export PATH=$JAVA_HOME/bin:$PATH

source /etc/profile后验证:

java -version

看到版本号输出就说明JDK层通了。

Tomcat下载直接去Apache官方站点或镜像站,找tomcat/10.1.23/bin/apache-tomcat-10.1.23.tar.gz。我提醒一句:尽量下tar.gz包而不是Windows的zip包,Windows包解压到Linux上容易遇到文件权限和换行符问题,虽然能修,但没必要给自己加戏。

下载完校验一下SHA512是负责任的做法:

sha512sum apache-tomcat-10.1.23.tar.gz

把输出结果和官网提供的校验值对比,一致才解压。这在大规模批量部署时不是形式主义,防止下载节点被劫持或文件损坏。

2.2 解压、部署目录与权限赋值

tar -zxvf apache-tomcat-10.1.23.tar.gz -C /opt/tomcat/ ln -s /opt/tomcat/apache-tomcat-10.1.23 /opt/tomcat/current

这里我做了一个软链接current,好处是后续升级时新版本解压到同目录,改一下软链接指向,业务零感知。很多运维事故都是升级时直接覆盖整个目录,改到一半发现配置忘记迁,软链接至少给了自己一个秒级回滚的机会。

接着把属主给tomcat用户:

chown -R tomcat:tomcat /opt/tomcat/apache-tomcat-10.1.23

关于权限,我一直坚持的原则是webapps、logs、temp、work这几个目录必须tomcat用户可写,bin目录可读可执行但不需要写,conf目录可读(如果以后用治理平台在线改配置,再单独放权)。

2.3 首次启动与验证

切到tomcat用户启动:

su -s /bin/bash tomcat -c "/opt/tomcat/current/bin/startup.sh"

注意我用了su -s /bin/bash,因为tomcat用户默认shell是nologin,直接su tomcat会报无权限。之后看一下启动日志:

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

正常出现Server startup in [xxxx] milliseconds就说明起来了。然后验证端口:

ss -lntp | grep 8080 curl -I http://127.0.0.1:8080

看到HTTP响应就说明安装成功。不建议直接暴露公网测试,先在本地回环地址验证才是规范路径。

这里插一个最常见的坑:很多人从Windows那边习惯双击startup.sh后看到窗口开着就以为成功,Linux下窗口关了进程可能还在,也可能启动失败后catalina.out里已经报错,但窗口没有反馈。Linux下一切以日志为准,以端口监听为准,不要凭窗口判断。

3. 核心配置解析,改完这些才算“配置”

Tomcat装完只是起点,真正决定运行品质的是几个核心配置文件的调整。解析完这个部分,你手里的Tomcat才是一个能扛业务的服务,不是玩具。

3.1 server.xml里最重要的监听端口与连接参数

conf/server.xml是Tomcat的骨架配置文件,核心结构我直接拆开讲。

第一块是Server端口,默认是8005,这是关闭Tomcat的监听端口:

<Server port="8005" shutdown="SHUTDOWN">

生产环境强烈建议改掉8005和shutdown字符串。这个端口只在本地监听,但如果服务器有公网IP且防火墙没兜住,外网都可以直接发SHUTDOWN指令把服务关停。改成不常见的端口和随机字符串,比如:

<Server port="9015" shutdown="S3cur3ShutDown2024">

第二块是Connector,这里决定了HTTP服务行为:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxThreads="400" minSpareThreads="50" acceptCount="200" maxConnections="10000" URIEncoding="UTF-8"/>

逐项解释下:

  • port="8080"是标准HTTP端口,如果同时跑多实例,这里必须改为不同端口。
  • maxThreads="400"是最大工作线程数,线程不是越多越好,Linux上每个线程默认栈空间1MB,线程数几百意味着内存消耗不小,要根据机器核数和内存算。
  • minSpareThreads="50"是空闲保留线程,避免流量一来现拉线程。
  • acceptCount="200"是等待队列长度,当线程全部繁忙时,请求先排队,超过队列直接拒绝。
  • URIEncoding="UTF-8"必配,否则GET请求中文参数容易乱码。

小组把maxThreads定多大?我一般先给一个基线:2核4G的机器建议200-300,4核8G建议400-500。再根据压测和真实监控调。不用一上来就堆上千,线程上下文切换的开销会浪费CPU,表现反而不如适当数量。

此外还有线程池标签Executor。如果要多Connector共享同一组线程池(比如HTTP和HTTPS都用)就配置一个Executor,再把Connector里换成executor="tomcatThreadPool"。单实例单Connector场景下不用管它。

3.2 Host配置、appBase和部署目录

server.xml里Host部分默认如下:

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">

appBase是Web应用放置位置,Tomcat默认webapps目录。autoDeploy默认true,意味着你把war扔进webapps目录后,不用重启Tomcat就能自动部署,这在开发环境确实方便,但生产环境我建议改false。见过多次生产事故:运维往webapps目录拖war包想拷贝留档,结果Tomcat自动热部署,版本被覆盖了都不知道。生产环境手动部署、手动重启,操作可控。

如果是多应用多域名,通常在Host下加Context配置。我举一个最典型的场景:某应用映射到根路径,不想要项目名。

<Context path="" docBase="/opt/apps/webapps/mall" reloadable="false"/>

这样访问http://ip:8080/就直接进应用,不用/mall。reloadable="false"也是两个意思:减小开发时热加载的监控开销,生产环境避免类加载器内存泄漏导致的永久代溢出。

3.3 tomcat-users.xml与Manager权限控制

Tomcat自带manager和host-manager两个后台管理应用,权限在conf/tomcat-users.xml里控制。我在生产环境建议两种处理方式:要么删掉webapps下manager应用,要么给强密码并限制来源IP。

如需使用,推荐最小权限配置:

<role rolename="admin-gui"/> <role rolename="manager-gui"/> <user username="opsadmin" password="强密码用随机字符串" roles="admin-gui,manager-gui"/>

这里必须强调:绝对不要配置成roles="manager-gui,admin-gui,manager-script,admin-script"全给。manager-script允许脚本远程部署war包,授权过大时等于把服务器大门钥匙交别人手里。

更稳妥的做法的确是在Host下发一条Valve,限制能访问Manager的IP:

<Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="127.0.0.1|192.168.1.100"/>

这样即使tomcat-users.xml被渗透,至少管理入口被锁在企业网段里。

3.4 JVM内存参数与启动脚本调优

这是生产环境最影响稳定性的环节。Tomcat默认启动脚本给的JVM参数很保守,不调的话并发一上来,GC频率高、内存不够用的问题马上暴露。

在bin/setenv.sh中添加JVM参数(没有这个文件就新建,这是SpringBoot/Tomcat环境常用外置方式,比直接改catalina.sh更清晰,Tomcat会自动加载它):

JAVA_OPTS="-server -Xms1024m -Xmx1024m -XX:MaxMetaspaceSize=512m"

几个核心点:

  • -server告诉JVM使用服务端编译模式,选择C2编译器,长时间运行的Java进程性能明显好于默认混合模式。
  • -Xms和-Xmx设成一样大,避免堆内存动态伸缩引起性能抖动。8G内存的服务器,一般给JVM 4G堆左右。老项目还要留1-2G给操作系统的页缓存和线程栈。
  • MaxMetaspaceSize是JDK8之后替代永久代的元空间上限,不设置的话默认是无限的,这会导致OOM后把宿主机的内存吃干。系统库反射多的应用尤其容易涨,设个上限,配合后台监控,至少能在失控前有报警。

还有盒子高级点:换成G1垃圾收集器。

JAVA_OPTS="-server -Xms4g -Xmx4g -XX:MaxMetaspaceSize=512m -XX:+UseG1GC"

JDK 8只有update 191以上版本支持G1。G1对大堆的停顿控制比CMS好,我现在的项目默认G1,实测下来GC最长停顿比CMS少一半以上。如果进阶跑高并发微服务还是追求开箱即用,也可以尝试ZGC/Shenandoah(JDK 11+),但稳定性优先还是G1。

3.5 日志配置与access log

Tomcat日志有两大类:一是JVM输出到catalina.out,二是访问日志access log。前者默认按天不轮转,会把磁盘撑爆,所以必须配合外部rotate或logrotate;后者是在server.xml里加一个Valve:

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t &quot;%r&quot; %s %b %D"/>

%h是远程IP,%u是用户,%t是时间,%r是请求行,%s是状态码,%b是响应字节数,%D是处理耗时毫秒——最后这个%D非常重要,排查慢接口的时候全靠它。

很多人在线上没开access log,出了请求慢的问题根本没有原始数据可查,纯靠猜。建议一装好就把访问日志能力打开,数据冗余大不了磁盘多花几G,比起性能排查时抓瞎,这点成本太值了。

4. 部署首个应用与systemd服务化

安装和配置已经让你手里有了一个能跑的基础Tomcat,现在把它变成生产级服务:部署war、注册systemd、开机自启、日志轮转,缺一不可。

4.1 部署war包的两种方式

部署war包最传统的方式是把war拷贝到webapps目录,Tomcat自动或手动重启后解压。我推荐生产用以下流程:

cp /opt/apps/builds/mall-admin-v1.2.3.war /opt/tomcat/current/webapps/ chown tomcat:tomcat /opt/tomcat/current/webapps/mall-admin-v1.2.3.war cd /opt/tomcat/current/bin && su -s /bin/bash tomcat -c "./shutdown.sh && ./startup.sh"

看到这里你可能会问:为什么拷贝war包之前不先停Tomcat?admin-gui一键部署不行吗?对于生产环境,我更推荐“先传包、再停服务、删除旧解压目录、启动服务”这样的顺序,避免边运行边改目录造成不可预期的半交互状态。

对于多个应用共用一个Tomcat的情况,强烈建议给每个应用独立的解压目录外置,然后在server.xml里用形式Context指定。否则默认webapps下管理目录太多,一眼看不清生产环境真正跑着什么,这对排障是个隐性障碍。

4.2 用systemd管理Tomcat生命周期

传统方式用startup.sh/shutdown.sh在Tomcat生命周期管理上很弱。现在的标准做法是写一个systemd service单元,交给systemd托管,开机自启、崩溃拉起、日志统一到journald都有了。

在/etc/systemd/system/tomcat.service写入:

[Unit] Description=Apache Tomcat Web Application Container After=network.target [Service] Type=forking Environment="JAVA_HOME=/usr/local/java/current" Environment="CATALINA_PID=/opt/tomcat/current/temp/tomcat.pid" Environment="CATALINA_HOME=/opt/tomcat/current" Environment="CATALINA_BASE=/opt/tomcat/current" ExecStart=/usr/local/java/current/bin/java $JAVA_OPTS User=tomcat Group=tomcat LimitNOFILE=65536 Restart=always RestartSec=10 [Install] WantedBy=multi-user.target

其中几个细节:

  • Type=forking是因为catalina.sh.启动/关闭时调用Java fork进程,不是前台驻留型。配合CATALINA_PID,systemd可以追踪到真正的Tomcat主进程。
  • LimitNOFILE=65536把文件描述符上限提到65536,高并发长连接场景下默认1024用几个小时就会满,表现为Too many open files。
  • Restart=always在进程崩溃时自动拉起,但要区分:如果服务异常退出,自动拉起大概率是好的;如果进程无法启动,多次重启也会在状态里暴露,配合监控报警就好。

配置后重载并启动:

systemctl daemon-reload systemctl enable --now tomcat systemctl status tomcat

之后不再手动调用startup.sh,否则systemd会失去对进程的控制,日志和状态都会对不上。这是操作纪律问题,养成统一走systemd的习惯,生产环境才不会混乱。

4.3 关闭Tomcat的正确姿势与PID细节

手动关停Tomcat有个细节容易被忽略:shutdown.sh默认是向8005端口发送shutdown字符串,如果8005端口不可达或进程卡死,shutdown.sh会一直等,看起来像挂住了。

此时更直接的方式是:

kill -9 $(cat /opt/tomcat/current/temp/tomcat.pid)

-9是最后手段,正常情况下先执行:

kill $(cat /opt/tomcat/current/temp/tomcat.pid)

给JVM一个优雅下线时间。用systemd托管后其实这些不用你操心,systemctl stop tomcat内部就处理了。如果哪天你手动启动过,又用systemd停了它,会看到进程起来了但status显示dead,就是两套管理方式混用的结果,回头看4.2节那句“统一走systemd”的含金量。

4.4 日志轮转与定时清理

catalina.out如果不管,几个月后轻松上10个G。在Linux上最轻量的方案是logrotate:

在/etc/logrotate.d/tomcat写入:

/opt/tomcat/current/logs/catalina.out { daily rotate 15 missingok copytruncate compress notifempty dateext }

copytruncate很关键:先复制一份日志再截断原文件,这样Tomcat进程的文件描述符还指向原文件,不会造成日志中断。如果用rename方式,Tomcat还得继续往旧文件句柄写,数据就丢到被改名的文件里了。

5. 常见问题与排查实录

这一节我把实操中踩过、也帮别人排查过的坑整理成清单,每一条都是真实经历过的场景,照着顺序查基本能定位80%以上的问题。

5.1 启动失败:端口被占用

典型报错是:

java.net.BindException: Address already in use: bind

排查命令:

ss -lntup | grep 8080 lsof -i:8080

找到占用进程后在确认是旧Tomcat还没退干净的情况下,kill掉旧进程再启动。如果启动脚本使用了8005端口,且上一次异常退出后端口还处于TIME_WAIT,可以临时改配置换个端口,或者用sysctl -w net.ipv4.tcp_tw_reuse=1再观察。

这里有个经验:关闭脚本可能因为PID文件丢失而找不到旧进程,此时不要反复重启,先清理PID文件再启。

5.2 内存溢出实战一例

曾经给某后台系统调优,现象是运行一周后接口全部变慢,然后OOM被杀。当时先看catalina.out:

java.lang.OutOfMemoryError: Java heap space

说明堆内存确实不够,但仅靠调大-Xmx不是根本解。我用jmap -heap 进程号观察堆使用,发现老年代持续增长,回收不掉,出现内存泄漏迹象。进一步dump:

jmap -dump:live,format=b,file=heap.bin 进程号

配合MAT分析,发现是某个定时任务加载了大量对象并持有在静态集合中,任务完成后没释放。定位到代码后修复,同时把堆从2G提到4G,双管齐下才解决。

这个案例想说明:JVM内存参数只能延迟爆炸时间,内存泄漏永远要查代码。Tomcat内存问题的排查思路必须包含:日志确认错误类型,jstack看线程,jmap看堆,不行再dump分析。

5.3 部署war包后404或应用访问不到

部署后404常见原因有两个:一是Context路径弄错了,比如war包部署后是/mall-admin-1.0.0,浏览器却访问/mall-admin;二是端口没开放,浏览器访问超时。

排查步骤:

curl -v http://127.0.0.1:8080/mall-admin/ ss -lntp | grep 8080 tail -100 /opt/tomcat/current/logs/catalina.out

如果本地curl都200说明Tomcat和war包正常,那问题基本在防火墙/安全组。腾讯、阿里的云服务器有安全组策略,Linux本身还有iptables/firewalld,两层都要看:

firewall-cmd --list-all # firewalld iptables -L -n --line-numbers # iptables

开放端口用:

firewall-cmd --permanent --add-port=8080/tcp firewall-cmd --reload

这条只在实际需要外部访问时才做。本地回环和堡垒机跳板架构中,内网应用完全没有必要开放到公网,少开一个端口,少一份风险。

5.4 中文乱码与Cookie问题

应用控制台输出中文乱码几乎都和字符集有关。分三层排查:

通过locale查看当前环境是否为UTF-8,/etc/profile中的JAVA_OPTS有没有加-Dfile.encoding=UTF-8。Tomcat的GET请求参数乱码看Connector的URIEncoding;POST请求乱码看应用自身是否设置CharacterEncodingFilter。这几个全对齐了,乱码问题基本能根治。

Cookie中文乱码则常在共享Session的场景出现,Tomcat 8.5+已经默认支持RFC 6265规范,对Cookie值中特殊字符编码要求更严格。建议在Connector里限制CookieProcessor实现为现代版本,同时应用侧统一URL编码。

5.5 高并发下Too many open files

有一个真实的故障案例:服务平稳运行大半天后,业务反馈大量连接超时,系统日志刷屏Too many open files。当时第一反应查文件描述符:

ulimit -n

结果只有1024。按4.2节在systemd配置文件里设置了LimitNOFILE=65536,然后:

systemctl daemon-reload && systemctl restart tomcat

再查进程的cat /proc/进程号/limits,确认Open files已经是65536。高并发在线业务,文件描述符上限不提前放开,迟早会触发要紧急重启的线上事故,这个配置建议在搭建阶段就写入初始化脚本。

5.6 防火墙、SELinux导致的外部访问失败

很多人在局域网内明明Tomcat本地能通,可另一台机器就是访问不了,此时优先检查两点:一是防火墙,二是在启用SELinux的环境下端口未放行。

查看SELinux状态:

getenforce

如果是Enforcing,可以临时放行8080:

semanage port -a -t http_port_t -p tcp 8080 semanage port -l | grep http

注意,新手建议实战阶段先搞明白SELinux的用意,不要一上来就setenforce 0关闭它。哪怕是腾讯云、阿里云镜像,默认SELinux虽然在Permissive模式,但某些发行版默认是Enforcing,先确认再调整,免得后面怎么死的都不知道。

5.7 快速排查汇总表

现象第一排查目标常用命令
启动失败端口占用ss -lntup / lsof
可以访问但很慢CPU、GC、线程数top / jstack / jstat
应用404Context路径、war状态curl -v / ls webapps
中文乱码文件编码、JVM参数、Connectorlocale / file_cmd / grep set
外部不可达防火墙/SELinux/安全组curl 127.0.0.1 / iptables / getenforce
磁盘写满日志文件大小du -sh logs
OOM堆参数、泄漏代码jstat / jmap / MAT

运维的手册其实就长这样,现象、位置、命令一一对应,碰到问题按表格先定位,大多数情况几分钟能缩小范围。

6. 自己动手之后的一点经验

这轮装下来,我最大的感受是:Tomcat安装本身一点都不难,真正难的是一开始就把环境规划想清楚,以及把常见故障点提前封堵。我习惯在每台新服务部署完,立刻把访问日志打开、日志轮转配置好、JVM参数依据机器内存算好、systemd托管好,看起来多用了几分钟,可后面省下来的排障时间都是论小时计的。

最后再分享一个小技巧:把整个初始化的关键命令整理成脚本,放到部署服务器上,新机器从装JDK到Tomcat跑起来,只需要改版本号和端口两个变量。团队的服务器多了之后,一致性比花哨的操作重要得多。把这篇文章里的步骤沉淀成自己的清单之后,你的Tomcat部署就会越来越顺手,也就不需要再抱着启动脚本看半天了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询