Linux下Tomcat部署与调优全流程:从JDK匹配到systemd托管
搞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/profileexport 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 port8005 shutdownSHUTDOWN生产环境强烈建议改掉8005和shutdown字符串。这个端口只在本地监听但如果服务器有公网IP且防火墙没兜住外网都可以直接发SHUTDOWN指令把服务关停。改成不常见的端口和随机字符串比如Server port9015 shutdownS3cur3ShutDown2024第二块是Connector这里决定了HTTP服务行为Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxThreads400 minSpareThreads50 acceptCount200 maxConnections10000 URIEncodingUTF-8/逐项解释下port8080是标准HTTP端口如果同时跑多实例这里必须改为不同端口。maxThreads400是最大工作线程数线程不是越多越好Linux上每个线程默认栈空间1MB线程数几百意味着内存消耗不小要根据机器核数和内存算。minSpareThreads50是空闲保留线程避免流量一来现拉线程。acceptCount200是等待队列长度当线程全部繁忙时请求先排队超过队列直接拒绝。URIEncodingUTF-8必配否则GET请求中文参数容易乱码。小组把maxThreads定多大我一般先给一个基线2核4G的机器建议200-3004核8G建议400-500。再根据压测和真实监控调。不用一上来就堆上千线程上下文切换的开销会浪费CPU表现反而不如适当数量。此外还有线程池标签Executor。如果要多Connector共享同一组线程池比如HTTP和HTTPS都用就配置一个Executor再把Connector里换成executortomcatThreadPool。单实例单Connector场景下不用管它。3.2 Host配置、appBase和部署目录server.xml里Host部分默认如下Host namelocalhost appBasewebapps unpackWARstrue autoDeploytrueappBase是Web应用放置位置Tomcat默认webapps目录。autoDeploy默认true意味着你把war扔进webapps目录后不用重启Tomcat就能自动部署这在开发环境确实方便但生产环境我建议改false。见过多次生产事故运维往webapps目录拖war包想拷贝留档结果Tomcat自动热部署版本被覆盖了都不知道。生产环境手动部署、手动重启操作可控。如果是多应用多域名通常在Host下加Context配置。我举一个最典型的场景某应用映射到根路径不想要项目名。Context path docBase/opt/apps/webapps/mall reloadablefalse/这样访问http://ip:8080/就直接进应用不用/mall。reloadablefalse也是两个意思减小开发时热加载的监控开销生产环境避免类加载器内存泄漏导致的永久代溢出。3.3 tomcat-users.xml与Manager权限控制Tomcat自带manager和host-manager两个后台管理应用权限在conf/tomcat-users.xml里控制。我在生产环境建议两种处理方式要么删掉webapps下manager应用要么给强密码并限制来源IP。如需使用推荐最小权限配置role rolenameadmin-gui/ role rolenamemanager-gui/ user usernameopsadmin password强密码用随机字符串 rolesadmin-gui,manager-gui/这里必须强调绝对不要配置成rolesmanager-gui,admin-gui,manager-script,admin-script全给。manager-script允许脚本远程部署war包授权过大时等于把服务器大门钥匙交别人手里。更稳妥的做法的确是在Host下发一条Valve限制能访问Manager的IPValve classNameorg.apache.catalina.valves.RemoteAddrValve allow127.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:MaxMetaspaceSize512m几个核心点-server告诉JVM使用服务端编译模式选择C2编译器长时间运行的Java进程性能明显好于默认混合模式。-Xms和-Xmx设成一样大避免堆内存动态伸缩引起性能抖动。8G内存的服务器一般给JVM 4G堆左右。老项目还要留1-2G给操作系统的页缓存和线程栈。MaxMetaspaceSize是JDK8之后替代永久代的元空间上限不设置的话默认是无限的这会导致OOM后把宿主机的内存吃干。系统库反射多的应用尤其容易涨设个上限配合后台监控至少能在失控前有报警。还有盒子高级点换成G1垃圾收集器。JAVA_OPTS-server -Xms4g -Xmx4g -XX:MaxMetaspaceSize512m -XX:UseG1GCJDK 8只有update 191以上版本支持G1。G1对大堆的停顿控制比CMS好我现在的项目默认G1实测下来GC最长停顿比CMS少一半以上。如果进阶跑高并发微服务还是追求开箱即用也可以尝试ZGC/ShenandoahJDK 11但稳定性优先还是G1。3.5 日志配置与access logTomcat日志有两大类一是JVM输出到catalina.out二是访问日志access log。前者默认按天不轮转会把磁盘撑爆所以必须配合外部rotate或logrotate后者是在server.xml里加一个ValveValve classNameorg.apache.catalina.valves.AccessLogValve directorylogs prefixlocalhost_access_log suffix.txt pattern%h %l %u %t quot;%rquot; %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包之前不先停Tomcatadmin-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] DescriptionApache Tomcat Web Application Container Afternetwork.target [Service] Typeforking EnvironmentJAVA_HOME/usr/local/java/current EnvironmentCATALINA_PID/opt/tomcat/current/temp/tomcat.pid EnvironmentCATALINA_HOME/opt/tomcat/current EnvironmentCATALINA_BASE/opt/tomcat/current ExecStart/usr/local/java/current/bin/java $JAVA_OPTS Usertomcat Grouptomcat LimitNOFILE65536 Restartalways RestartSec10 [Install] WantedBymulti-user.target其中几个细节Typeforking是因为catalina.sh.启动/关闭时调用Java fork进程不是前台驻留型。配合CATALINA_PIDsystemd可以追踪到真正的Tomcat主进程。LimitNOFILE65536把文件描述符上限提到65536高并发长连接场景下默认1024用几个小时就会满表现为Too many open files。Restartalways在进程崩溃时自动拉起但要区分如果服务异常退出自动拉起大概率是好的如果进程无法启动多次重启也会在状态里暴露配合监控报警就好。配置后重载并启动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_reuse1再观察。这里有个经验关闭脚本可能因为PID文件丢失而找不到旧进程此时不要反复重启先清理PID文件再启。5.2 内存溢出实战一例曾经给某后台系统调优现象是运行一周后接口全部变慢然后OOM被杀。当时先看catalina.outjava.lang.OutOfMemoryError: Java heap space说明堆内存确实不够但仅靠调大-Xmx不是根本解。我用jmap -heap 进程号观察堆使用发现老年代持续增长回收不掉出现内存泄漏迹象。进一步dumpjmap -dump:live,formatb,fileheap.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-port8080/tcp firewall-cmd --reload这条只在实际需要外部访问时才做。本地回环和堡垒机跳板架构中内网应用完全没有必要开放到公网少开一个端口少一份风险。5.4 中文乱码与Cookie问题应用控制台输出中文乱码几乎都和字符集有关。分三层排查通过locale查看当前环境是否为UTF-8/etc/profile中的JAVA_OPTS有没有加-Dfile.encodingUTF-8。Tomcat的GET请求参数乱码看Connector的URIEncodingPOST请求乱码看应用自身是否设置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配置文件里设置了LimitNOFILE65536然后systemctl daemon-reload systemctl restart tomcat再查进程的cat /proc/进程号/limits确认Open files已经是65536。高并发在线业务文件描述符上限不提前放开迟早会触发要紧急重启的线上事故这个配置建议在搭建阶段就写入初始化脚本。5.6 防火墙、SELinux导致的外部访问失败很多人在局域网内明明Tomcat本地能通可另一台机器就是访问不了此时优先检查两点一是防火墙二是在启用SELinux的环境下端口未放行。查看SELinux状态getenforce如果是Enforcing可以临时放行8080semanage 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 logsOOM堆参数、泄漏代码jstat / jmap / MAT运维的手册其实就长这样现象、位置、命令一一对应碰到问题按表格先定位大多数情况几分钟能缩小范围。6. 自己动手之后的一点经验这轮装下来我最大的感受是Tomcat安装本身一点都不难真正难的是一开始就把环境规划想清楚以及把常见故障点提前封堵。我习惯在每台新服务部署完立刻把访问日志打开、日志轮转配置好、JVM参数依据机器内存算好、systemd托管好看起来多用了几分钟可后面省下来的排障时间都是论小时计的。最后再分享一个小技巧把整个初始化的关键命令整理成脚本放到部署服务器上新机器从装JDK到Tomcat跑起来只需要改版本号和端口两个变量。团队的服务器多了之后一致性比花哨的操作重要得多。把这篇文章里的步骤沉淀成自己的清单之后你的Tomcat部署就会越来越顺手也就不需要再抱着启动脚本看半天了。