搞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.x | JDK 7 以上(推荐JDK 8) | 老项目的主力选择,官方对JDK 8支持最好 |
| Tomcat 9.0.x | JDK 8 以上 | 目前最多生产环境用的版本,稳定、资料多 |
| Tomcat 10.0.x | JDK 8 以上 | jakarta.*命名空间,新项目直接用 |
| Tomcat 11.0.x | JDK 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:$PATHsource /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 "%r" %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 |
| 应用404 | Context路径、war状态 | curl -v / ls webapps |
| 中文乱码 | 文件编码、JVM参数、Connector | locale / 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部署就会越来越顺手,也就不需要再抱着启动脚本看半天了。