news 2026/11/2 10:22:53

Tomcat安装配置与上线排障实战:JDK、server.xml、多实例、systemd

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Tomcat安装配置与上线排障实战:JDK、server.xml、多实例、systemd

很多人对 Tomcat 的"学会"标准,一直停留在下载压缩包、双击 startup、浏览器敲出 localhost:8080 看到那只猫这一步。在本地跑一个 demo,这套流程确实够用;但只要把一个应用真正放到服务器上给别人访问,问题就会成串冒出来:端口被占、日志目录没权限、war 包扔进去访问 404、换台机器启动脚本报 JAVA_HOME 找不到、同一台机器想跑两个应用却互相掐架。这些都不是"猫没出来"级别的故障,而是安装和配置阶段埋下的伏笔。

这一节只讲一件事:Tomcat 安装和配置中那些文档不会重点写、但上线一定会碰到的部分。内容包括版本怎么选、目录怎么规划、server.xml 到底该动哪几行、Linux 和 Windows 两条线怎么启动、起不来的时候按什么顺序排查、war 包应该放在哪里。适合刚接触 Java Web 部署的同学,也适合手里管着几台机器、需要在一台服务器上稳定跑多个应用的运维和开发。整套流程我会给到可直接复制的命令和配置片段,也会说清楚每一步为什么这么做——毕竟照抄步骤只能解决一次问题,知道原因才能解决下一次。

1. 安装之前先定三件事,比急着解压重要得多

我在带新人的时候发现,Tomcat 装崩的案例里,超过一半的根因不在安装动作本身,而在装之前没想清楚版本、路径和端口。这三件事定错了,后面所有配置都是在给错误打补丁。

1.1 JDK 版本决定了你能装哪个 Tomcat

最容易踩的不是 JDK 装没装,而是装了个版本对不上的组合。Tomcat 各个大版本对 JDK 的最低要求和包名空间差异很大,尤其是从 9 跨到 10 那一步,包名从javax.servlet.*直接换成了jakarta.servlet.*,这是硬切换,没有兼容层。

Tomcat 版本Servlet 规范最低 JDK包名空间适合的场景
8.5.xServlet 3.1JDK 7javax维护中的老系统
9.0.xServlet 4.0JDK 8javax存量项目主力选择
10.0.xServlet 5.0JDK 8jakarta过渡版本,一般不推荐新上
10.1.xServlet 6.0JDK 11jakarta新项目常用
11.0.xServlet 6.1JDK 17jakarta新项目、需要新语言特性

选版本的逻辑很直接:手里是跑了多年的老系统,依赖里到处是javax.servlet,那就老老实实用 Tomcat 9,别为了"用新版"去改一整个项目的 import。反过来,如果是新起项目,或者已经升级到 Spring Boot 3 那一代依赖,就必须上 10.1 或 11,因为框架层面已经全面切到 jakarta。

判断自己项目属于哪一类,有个很省事的办法:把要部署的 war 包解开,看WEB-INF/lib下有没有 servlet-api 相关的 jar,或者直接在源码里搜import javax.servlet。搜得到,锁 9;搜不到、看到的是jakarta.servlet,那就往上走。这一步花两分钟,能省掉后面半天对着NoClassDefFoundError: javax/servlet/Servlet发愣的时间。

还有一种情况要提一句:项目自带 servlet-api 的 jar 包被打进了WEB-INF/lib。这在容器环境下是隐患,容器自己的实现和包里的类打架,表现为各种诡异的方法找不到。正确做法是把这类依赖的 scope 设成 provided,让容器提供。

1.2 安装路径、运行用户和磁盘布局

路径这件事,看着无所谓,实际上直接影响权限、备份和后续扩容。我见过把 Tomcat 解压在/root下、然后用 root 启动的部署,短期能用,但一旦要接监控、要给别人排查日志,处处受限。

我的习惯是固定在/opt/tomcat或者/usr/local/tomcat,然后单独建一个只用于跑服务的系统用户。理由有三个:一是权限边界清晰,这个用户只对 Tomcat 目录有读写权限,出问题影响面可控;二是审计方便,ps一看就知道是哪个用户在跑;三是避免用 root 启动带来的一堆麻烦,比如 JVM 生成的临时文件属主混乱。

这里还有个概念值得提前建立起来:CATALINA_HOME和CATALINA_BASE是两回事。前者是 Tomcat 的安装目录,放 bin、lib 这些只读的东西;后者是实例的工作目录,放 conf、logs、webapps、work、temp。把两者分开,就能用一份安装、起多个实例——每个实例有自己的 conf 和 webapps,共享同一套程序文件。单实例部署时两者指向同一个目录也没问题,但心里要清楚这个区分,等到哪天需要在同一台机器上跑多个互不干扰的应用,就知道这个设计的价值了。

磁盘方面,logs和work是两个会长大的目录。work是 JSP 编译后的产物,应用多的时候能到几百兆;logs更要注意,catalina.out默认是追加写、不切分的,跑上几个月能到几个 G。所以磁盘规划上,我一般会把/opt/tomcat/logs单独挂一块盘,或者至少保证根分区有足够余量,同时配好日志轮转。这一点在安装阶段顺手做了,比事后清理要轻松得多。

1.3 端口规划:8080 只是个默认值

一台机器上一个 Tomcat,8080 冲突的概率不高;但只要涉及到多实例或者机器上还跑了别的东西,端口就必须提前规划。Tomcat 默认会占用三个端口,而且三个都要管:

  • 8080:HTTP Connector,对外服务的端口,最常被改的就是它。
  • 8005:Server 的 shutdown 端口,收到SHUTDOWN字符串就关掉整个实例。这个端口默认只监听本机,但它的存在本身是个安全隐患,后面会讲怎么处理。
  • 8009:AJP Connector,给前置的 Web 服务器转发用。新版本的默认配置里它是注释掉的,如果你不用 Apache httpd 做前置,保持注释就好。

多实例部署时,这三个端口都要错开,比如第一个实例 8080/8005/8009,第二个用 8081/8006/8010。光改 HTTP 端口不改 shutdown 端口是个经典疏漏,结果就是两个实例的关停互相干扰,停其中一个把另一个也带走了。

选端口的时候还有个实操建议:先在机器上跑一遍ss -lntp看当前占用情况,别用 8080 试运气。有些机器的 8080 早就被别的服务占着,装完启动直接 BindException,白折腾一轮。

2. Linux 上的安装:解压只是第一步

Linux 是最常见的部署环境,整个流程不复杂,但每个动作背后都有讲究。下面按顺序走一遍,命令都是可以直接粘的。

2.1 建用户、解压、改属主

先建用户,注意-r和-s /sbin/nologin,让它成为一个不能登录的服务账户:

useradd -r -m -s /sbin/nologin tomcat

然后把安装包解压到目标目录,并修正属主和权限:

tar -zxvf apache-tomcat-9.0.xx.tar.gz -C /opt/ mv /opt/apache-tomcat-9.0.xx /opt/tomcat chown -R tomcat:tomcat /opt/tomcat chmod -R u+rwX /opt/tomcat

这里有个细节:从官网下载的二进制包,bin下的*.sh本来就有执行权限,一般不用额外处理。但如果压缩包是从某些渠道转手来的,权限可能丢了,表现就是./startup.sh: Permission denied。真遇到了,chmod +x bin/*.sh补上就行。

为什么不建议用 root 跑?最直接的原因是,一旦应用存在文件上传、模板注入这类问题,攻击者拿到的就是 root 权限,影响面直接拉满。用普通用户跑,最坏情况被限制在这个用户的家目录和 Tomcat 目录里。这条不是洁癖,是最基本的隔离。

2.2 setenv.sh:把环境变量从命令行搬进文件

很多教程教你在命令行里export JAVA_HOME=...,然后执行启动脚本。这么做当时能起来,一关终端、一换会话就失效,用 systemd 托管时更是完全找不到环境变量。正确的位置是bin/setenv.sh,catalina.sh会自动加载它。

#!/bin/bash export JAVA_HOME=/usr/local/jdk-17 export CATALINA_HOME=/opt/tomcat export CATALINA_BASE=/opt/tomcat export CATALINA_PID=/opt/tomcat/temp/tomcat.pid export CATALINA_OPTS="-server \ -Xms2g -Xmx2g \ -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \ -Xlog:gc*:file=/opt/tomcat/logs/gc.log:time,uptime:filecount=10,filesize=20M \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/opt/tomcat/logs \ -Dfile.encoding=UTF-8 \ -Duser.timezone=Asia/Shanghai"

这里必须说清楚JAVA_OPTS和CATALINA_OPTS的区别,这是面试常问、实际也真会踩的点:CATALINA_OPTS只在启动时传给 JVM,JAVA_OPTS在启动和停止时都会传。所以那些只和运行期相关的参数——堆大小、GC 日志、堆转储——放进CATALINA_OPTS;如果有些参数停止时也需要(极少),才放JAVA_OPTS。混用的后果是停止时又按启动参数起了一个 JVM,白白消耗内存甚至卡住。

CATALINA_PID这个变量容易被忽略,但它决定了shutdown.sh能不能准确找到进程。不设置的时候,Tomcat 靠去ps里匹配命令行来定位,一旦命令行里有别的相似进程,就可能杀错或者杀不掉。设上之后,关停就精准了,用 systemd 托管时也更可靠。

至于堆大小怎么定,-Xms和-Xmx设成一样是常规操作,避免运行期反复扩缩容带来的停顿。具体数值看机器内存和应用特性,一般不超过物理内存的一半,给操作系统和其他进程留出空间。元空间用MaxMetaspaceSize卡一个上限,防止动态生成大量类导致无限增长——这个在用到某些字节码框架的项目里不是理论风险,是真会发生。

2.3 启动与验证的五个信号

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

启动之后,别急着开浏览器,按下面五个信号依次确认,能快速区分"真起来了"和"看起来起来了":

  1. 进程在不在:ps -ef | grep tomcat能看到一个 java 进程,且用户是 tomcat。
  2. 启动完成日志:tail -f logs/catalina.out,看到Server startup in [xxxx] milliseconds这一行。没有这一行,说明还在启动或者启动失败。
  3. 端口在听:ss -lntp | grep 8080,能看到 LISTEN 状态。
  4. 接口能通:curl -I http://127.0.0.1:8080/,返回 200。
  5. 访问日志有记录:logs/localhost_access_log.<日期>.txt里出现了刚才那次请求。

这里要纠正一个认知:startup.sh并不是真正意义上的后台启动,它内部调的是catalina.sh start,把标准输出重定向到catalina.out后放后台。所以启动失败的痕迹基本都在catalina.out里,出问题时第一件事就是看它,而不是反复执行startup.sh试运气。

另外,catalina.sh run是前台运行,日志直接打到终端,调试阶段特别好用——启动过程的每一条输出都能实时看到,不用去翻文件。正式环境就更推荐这种方式托管,下一节讲 systemd 时会说。

3. server.xml 真正需要动的地方就那么几处

conf/server.xml是 Tomcat 最核心的配置文件,但它同时也是最容易被过度修改的文件。网上流传的各种"优化模板"塞满了看不懂的参数,改完之后连原来的默认行为都丢了。我的建议是:默认配置已经足够合理,只动真正需要的几处。

3.1 Connector:端口、线程与超时要一起看

Connector 是 Tomcat 对外的接口,HTTP 请求都从这里进来。默认模板大概长这样:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

protocol="HTTP/1.1"在较新版本里会自动选用 NIO 实现,一般不用改成具体的类名。需要关注的是下面几个参数:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" maxThreads="400" minSpareThreads="50" acceptCount="200" maxConnections="10000" URIEncoding="UTF-8" compression="on" compressionMinSize="2048" compressibleMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/json" />

maxThreads是同时处理请求的最大线程数,默认 200。线程数怎么估?一个粗糙但好用的公式是:峰值 QPS × 平均响应时间(秒)= 需要的并发线程数,再留 20% 到 30% 的余量。比如峰值 200 QPS、平均响应 200 毫秒,算下来是 40 个线程,即使留足余量,200 的默认值也够。反过来,如果是响应慢的报表类接口,默认 200 可能就不够。

acceptCount是等待队列长度,线程全忙时新来的请求排队,队列满了就直接拒绝。这个值配合maxThreads一起调,光加线程不调队列,高峰期的表现就是间歇性连接被拒。

connectionTimeout有个容易混淆的点:默认模板里写的是 20000 毫秒,而 Connector 组件的默认值其实是 60000。这两个数字不一样的原因纯粹是模板这么写,不是配置错了。要不要改,取决于你的客户端场景——长连接、慢上传的业务,20 秒可能会掐断正常请求。

compression开压缩对文本类响应效果明显,JSON 接口通常能压到原来的三分之一。但别忘了加compressibleMimeType,否则默认只对少数几种类型生效,你以为开了压缩其实没开。图片、视频这类本来就压过的内容别往里加,纯属浪费 CPU。

还有一个细节:想让响应头里不暴露服务器版本号,部分版本可以在 Connector 上设server属性,但不同版本支持情况不一致。更稳妥的做法是在Host里配置 ErrorReportValve:

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Valve className="org.apache.catalina.valves.ErrorReportValve" showReport="false" showServerInfo="false" /> </Host>

这样出错页面上就不会带出 Tomcat 的版本信息,减少了被针对性扫描的机会。

3.2 一个 Tomcat 跑多个应用:Host 与 Context 的关系

理解Host和Context的层级,是搞懂多应用部署的关键。Host代表一个虚拟主机,Context代表一个具体的 Web 应用。默认配置里Host的appBase="webapps",意思是把 war 包丢进webapps目录,Tomcat 就自动把它部署成一个 Context。

规则很朴素,但很多人第一次接触会绕晕:

war 包文件名访问路径说明
ROOT.war/根应用,一个 Host 只能有一个
app.war/app最常见的情况
app#v2.war/app/v2用#表示多级路径
app##001.war/app带版本号但路径仍是/app,用于并行部署

如果应用不在webapps下,比如部署到/data/apps/myapp,有两种做法。一种是在server.xml里加 Context,但改这个文件需要重启整个实例,影响所有应用。更好的是用独立配置文件:在conf/Catalina/localhost/下建一个myapp.xml,内容只有一行:

<Context docBase="/data/apps/myapp" reloadable="false" />

文件名就是访问路径,myapp.xml对应/myapp。这个方式的好处是不用重启主配置,改完 Tomcat 会自动感知。生产环境我基本都用这种,应用目录和 Tomcat 安装目录彻底分开,升级 Tomcat 的时候直接换目录,应用一行不用动。

3.3 把默认开着但用不上的东西关掉

默认配置里有些东西是给开发和学习用的,生产环境该关就关。

shutdown 端口。<Server port="8005" shutdown="SHUTDOWN">这行的本意是让shutdown.sh通过发送字符串来关停实例。问题是,任何能连上这个端口并发送SHUTDOWN的人都能关掉你的服务。如果这个端口暴露在外,等于把开关交出去了。处理方式有两种:改成port="-1"直接禁用,用CATALINA_PID配合信号来关停;或者改成一个随机的高位端口,确保只监听在 127.0.0.1。我一般选前者,进程管理交给 systemd,干净利落。

AJP Connector。默认是注释掉的,如果你的架构里没有前置的 Web 服务器做转发,就别打开。历史上 AJP 协议出过几次比较严重的问题,不开就不会有这方面的担忧。

示例应用。webapps目录下的docs、examples纯粹是学习用的,manager和host-manager是管理端。这几个在生产环境都应该删掉。examples里带着不少演示页面,被扫到就是信息泄露;管理端更危险,弱口令进去可以直接部署任意 war 包,等同于拿到了服务器。真要远程管理,也应该限制来源 IP 并且用强口令,但最省事的做法就是直接删。

4. Windows 与服务化:别拿 startup.bat 当生产启动方式

Windows 上跑 Tomcat 的场合比想象中多,尤其是内部系统。这里的坑和 Linux 不太一样,主要集中在外壳脚本和服务化上。

4.1 Windows 上的几个差异点

最典型的问题是路径带空格。把 Tomcat 装在C:\Program Files\tomcat下面,然后启动脚本各种报错,原因就是脚本里对路径的处理没有全部加引号。解决办法很简单:换一个不带空格的路径,比如D:\server\tomcat。这个问题不值得去改脚本解决,换个目录就完事。

第二个问题是直接用startup.bat启动。双击出来的那个黑窗口,一旦被误关,java 进程就跟着结束了——这根本不是服务,是前台进程。生产环境必须注册成 Windows 服务:

cd D:\server\tomcat\bin service.bat install Tomcat9

注册之后在服务管理器里能看到,可以设成自动启动、可以配置失败重启。JVM 参数不写在setenv.bat里也能生效,但更推荐统一在bin\setenv.bat里维护,避免以后换机器时漏配:

set "JAVA_HOME=D:\server\jdk-17" set "CATALINA_OPTS=-Xms1g -Xmx1g -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"

注意这里是setenv.bat,Linux 下是setenv.sh,别搞混。改完之后要重启服务才生效。

第三个问题是权限。注册服务、改服务配置都需要管理员权限的终端,普通 cmd 里执行service.bat install会失败但报错信息不一定直白。养成用管理员终端操作的习惯,能少绕很多弯。

4.2 用 systemd 管住启停与开机自启

回到 Linux,托管方式我强烈建议用 systemd,不要依赖startup.sh加crontab那种老办法。写一个 unit 文件放到/etc/systemd/system/tomcat.service:

[Unit] Description=Tomcat Application Server After=network.target [Service] Type=simple User=tomcat Group=tomcat Environment="JAVA_HOME=/usr/local/jdk-17" ExecStart=/opt/tomcat/bin/catalina.sh run ExecStop=/bin/kill -TERM $MAINPID SuccessExitStatus=143 Restart=on-failure RestartSec=5 LimitNOFILE=65536 [Install] WantedBy=multi-user.target

几个地方值得解释。Type=simple配合catalina.sh run,而不是Type=forking配合startup.sh。前者让 systemd 直接管住主进程,进程退出它能第一时间知道;后者要靠 PIDFile 去追踪,一旦 PID 文件写失败或者残留,服务状态就会失真,出现"服务显示 active 但实际已经挂了"的情况。

SuccessExitStatus=143是必须的。143 是 128 加 15,代表进程被 SIGTERM 正常终止,但在 systemd 眼里非零退出码就是失败,不加这行,每次正常停止都会标红,配合Restart=on-failure还会误触发重启。

LimitNOFILE=65536也别省。默认的文件句柄上限往往只有 1024,高并发场景下大量 TIME_WAIT 连接会把这个数字吃满,表现为间歇性的连接失败,日志里可能只看到一些莫名其妙的 IO 异常。这个问题排查起来非常费劲,提前设好能省掉一场噩梦。

最后systemctl daemon-reload && systemctl enable --now tomcat启动并设好开机自启。以后看日志直接journalctl -u tomcat -f,比翻catalina.out方便。

5. 起不来的时候,按这个顺序看

排查这件事最怕的是乱试。我习惯先分清故障落在哪一层,再往下钻,效率差好几倍。

5.1 先分清"没起来"和"起来了但访问不到"

这三层要依次确认,跳步容易误判:

第一层,进程层。ps -ef | grep java有没有?没有,说明 JVM 根本没启动,去看catalina.out的头部报错。有,进入第二层。

第二层,监听层。ss -lntp | grep 8080有没有?进程在但端口没监听,常见于启动过程中卡住、或者 Connector 初始化失败。这时候catalina.out里通常会有具体异常。

第三层,应用层。端口在听,但访问返回 404、403、500。这就和 Tomcat 本身关系不大了,问题在应用部署或者应用代码上。404 多半是 context 路径不对或者 war 没解开,403 看权限和访问控制,500 去看应用日志。

把这三层分清楚,能避免"访问不了就去改 server.xml"这种盲目的操作。

5.2 几个高频错误的识别特征

端口被占用。日志里是java.net.BindException: Address already in use。别急着换端口,先ss -lntp | grep 8080看看是谁占的。有时候是上一次没关干净的 Tomcat 进程,杀进程比改端口更合理。顺便检查一下 shutdown 端口是不是也被占了,两个实例的 8005 撞车会表现为"启动看似成功但关不掉"。

JAVA_HOME 没设置。报错很直白:Neither the JAVA_HOME nor the JRE_HOME environment variable is defined。原因通常是setenv.sh没建、建了但路径写错、或者用su切用户时没带上环境。注意su - tomcat和su tomcat加载的环境不一样,前者会读登录 shell 的配置,后者继承当前环境。用 systemd 时,环境变量要在 unit 里声明或者靠setenv.sh提供。

目录权限不足。表现是启动日志里一串Permission denied,或者应用上传文件时报错。检查logs、temp、work三个目录的属主是不是运行用户。有个隐蔽的情况:曾经用 root 启动过一次,work目录下生成了一堆 root 属主的文件,换成 tomcat 用户之后写不进去。这种情况chown -R一遍就好。

中文乱码。老项目里很常见。要同时看三处:Connector 的URIEncoding(新版本默认 UTF-8,老版本默认 ISO-8859-1)、JVM 的-Dfile.encoding、以及响应头的Content-Type。三处里有一处不对,中文就会花。

类找不到。看到NoClassDefFoundError: javax/servlet/Servlet或者ClassNotFoundException: jakarta.servlet.Servlet,基本可以断定是 Tomcat 大版本和应用的包名空间不匹配,回去对一下第 1.1 节的表格就行,不用在依赖里翻。

元空间撑爆。日志里是java.lang.OutOfMemoryError: Metaspace。这通常不是 Tomcat 的问题,而是应用在运行期动态生成大量类(热部署频繁、某些字节码框架用法不当)。先把MaxMetaspaceSize调大观察,同时排查是不是开了reloadable导致类不断堆积。

6. 部署应用:war 包丢哪、路径怎么变

部署这件事,看着就是把 war 扔进 webapps,但实际用起来,路径规则和热更新的行为值得单独说清楚。

6.1 webapps 下的部署规则与 unpackWARs

Host的unpackWARs="true"表示 Tomcat 会把 war 包解压成目录再运行,默认就是开的。解压的好处是启动快、JSP 编译产物有地方放;不开启的话直接以 war 包形式运行,省磁盘但每次都要从压缩包里读。

放 war 的时机也有讲究。Tomcat 在运行中往webapps里放 war,autoDeploy会检测到并自动部署,路径就是 war 文件名去掉后缀。想换版本,直接替换 war 文件,Tomcat 会先卸载旧的再部署新的。听起来很方便,但这套机制在生产环境并不总是可靠——大 war 包解压需要时间,这期间请求会失败;替换瞬间如果有请求正在处理,可能被打断。

所以我的实际做法是:生产环境不用热部署,部署走蓝绿或者至少挑低峰期。要"零停机"的话,正确姿势是新版本部署到另一个 context 路径,验证通过后改前置转发的指向,而不是指望 Tomcat 自己热替换。

6.2 autoDeploy 和 reloadable 的真实代价

这两个参数经常被当成"开发便利性开关"随手打开,但它们在生产环境是有成本的。

autoDeploy="true"让 Tomcat 周期性扫描appBase目录,发现变化就部署。扫描本身开销不大,但对大目录(几百个 war)会有影响,更重要的是它给了"意外部署"的可能——不小心往 webapps 扔了个文件,服务就被改动了。

reloadable="true"是更贵的那一个。它会让 Tomcat 监控WEB-INF/classes和WEB-INF/lib的变化,检测到就重载整个应用。原理是销毁当前的类加载器、新建一个,这也意味着旧类加载器加载的类要等 GC 才能释放。开发时反复改代码反复重载,元空间就会一路涨,最终 OOM。所以这个参数只适合开发环境,生产环境必须关掉。

我见过一个案例:生产环境的 Context 里带着reloadable="true",跑了两个月之后元空间慢慢涨到上限,服务开始间歇性报错。排查时看 GC 日志,发现 Full GC 之后元空间释放得很少,最后定位到这个参数。改成false重启后,问题消失。这类问题不会立刻暴露,属于典型的"埋雷"。

6.3 外部目录加 context 片段:我更推荐的部署方式

综合下来,生产环境我最常用的组合是这样:

应用统一放在/data/apps/下,比如/data/apps/order、/data/apps/user。然后在conf/Catalina/localhost/下建对应的order.xml、user.xml:

<Context docBase="/data/apps/order" reloadable="false" />

这样做的好处很实在:Tomcat 的webapps目录保持干净,升级 Tomcat 时直接换安装目录,应用文件完全不受影响;应用目录可以单独做备份、单独挂盘;部署新版本时先解压到/data/apps/order-new,改一行docBase指向它,Tomcat 自动重新加载,出问题改回来就行。

需要注意的是,这种方式的"自动重新加载"也不是原子的,正在处理的请求会受影响。真正的无缝切换还是得靠前置转发或者双实例。

7. 上线前值得花二十分钟做的加固

配置跑通和能对外提供服务,中间还差一段安全加固。这几件事花不了多少时间,但省掉的麻烦是数量级的。

7.1 管理端、示例应用和默认账号

前面提过,webapps下的manager、host-manager、docs、examples在生产环境应该删掉。删之前确认一下有没有人在用——极少数团队会依赖 manager 的远程部署接口,虽然我不推荐这种方式。

conf/tomcat-users.xml里如果配置了账号,检查角色分配。manager-gui能通过网页部署应用,权限非常大。更安全的做法是根本不给任何人manager-gui,需要部署就走文件系统。新版 Tomcat 的 manager 默认只允许本机访问,这个限制别去放开。

顺带说一个经常被忘记的点:conf/目录下的配置文件里可能存着数据库密码、密钥之类的敏感信息。这个目录的权限要收紧,至少不能是所有人可读。

7.2 JVM 参数、日志和堆转储

堆大小、GC 日志、OOM 时自动转储这组参数,在第 2.2 节已经给过一份。这里强调三个容易漏的:

一是GC 日志一定要开。服务出问题的时候,GC 日志是判断"是不是内存问题"的第一手材料。等出事再开就晚了,历史数据没了。

二是堆转储要指定路径并且确保可写。-XX:+HeapDumpOnOutOfMemoryError只在 OOM 那一刻生效,路径不可写就白配了。转储文件可能和堆一样大,磁盘要留够空间。

三是时区要显式指定。容器镜像、云主机默认 UTC 的情况很常见,日志时间差 8 小时,排查时对时间线能对到崩溃。-Duser.timezone=Asia/Shanghai加上,省心。

日志轮转方面,catalina.out是最需要处理的。可以用系统自带的日志轮转工具,配一个按天切分、保留 14 天、切完压缩的规则;也可以直接改用catalina.sh run加 systemd,让 journald 管日志,天然带轮转和查询。我倾向后者,少维护一个组件。

7.3 一份可以照着打勾的自检清单

装完配完,我习惯按下面这张表过一遍,全部打勾才算部署完成:

检查项通过标准
JDK 匹配版本符合第 1.1 节表格,java -version正常
运行用户非 root,对logs、temp、work有写权限
环境变量setenv.sh存在且被正确加载
端口占用端口已规划,shutdown 端口已禁用或限制本机
启动验证五个信号全部确认过
默认应用docs、examples、manager、host-manager已清理
版本隐藏错误页不暴露版本号
JVM 参数堆、元空间、GC 日志、时区、堆转储均已配置
服务托管systemd 或 Windows 服务已注册,开机自启已验证
重启演练真的重启过一次,确认能自动恢复

最后一项最容易被跳过,也是最能暴露问题的。很多配置在冷启动时正常、热重启时出错,比如 PID 文件残留、临时目录权限、端口没释放干净。装完当天就演练一次重启,比上线后半夜被叫起来排查强太多。

再分享一个小习惯:我会在 Tomcat 目录下放一个DEPLOY.md,把版本、JDK、端口、应用列表、启动方式、日志位置、上次改动写清楚。听起来有点土,但换人接手或者自己半年后再看,这几行字能省掉大量回忆成本。部署文档的价值不在于写得多规范,而在于它就在旁边,随手能翻。

这一节涉及的配置项不多,但每一个都和后面会遇到的故障对得上。真正把这套东西跑顺之后你会发现,Tomcat 的安装和配置本身没什么难度,难的是提前想到那些还没发生的问题。我自己的经验是,凡是能在安装阶段花十分钟确认的,绝不留到出故障时再查——线上排查的时间成本,是准备阶段的十倍不止。

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

桌面语音工作台如何重塑声音创作:从录音降噪到AI配音的全流程指南

身边做播客、做短视频、做有声书的朋友越来越多&#xff0c;大家几乎是同一个节奏&#xff1a;手里已经有了不错的麦克风和声卡&#xff0c;兴冲冲打开录音软件&#xff0c;录完一听&#xff0c;还是“手机录音”的感觉。问题基本不出在设备上&#xff0c;而是卡在一条干净的声…

作者头像 李华
网站建设 2026/11/2 10:19:18

SSA-VMD融合算法在信号处理中的优化与应用

1. 麻雀优化算法与变分模态分解的跨界融合 在信号处理领域&#xff0c;我们常常面临这样的困境&#xff1a;传统分解方法对噪声敏感、模态混叠严重&#xff0c;而参数选择又极度依赖经验。三年前我在处理一组工业振动信号时&#xff0c;曾花费两周时间手动调整VMD的惩罚因子α和…

作者头像 李华
网站建设 2026/11/2 10:19:03

C语言原地实现康威生命游戏:二维数组状态编码与边界安全

1. 项目概述&#xff1a;一道C语言题目的真实面目与教学价值“【C语言】每日一题afeng111 and dead cells”——这个标题乍看像极了某位博主在社交平台随手发的打卡式笔记&#xff0c;但如果你真点进去&#xff0c;大概率会发现它既不是某个知名OJ平台的官方题号&#xff0c;也…

作者头像 李华
网站建设 2026/11/2 10:17:43

RabbitMQ延迟消息实现方案与生产实践

1. 为什么我们需要延迟消息&#xff1f; 在分布式系统中&#xff0c;延迟消息是一种常见且强大的设计模式。想象一下电商系统中的订单超时未支付自动取消场景&#xff1a;用户下单后30分钟内未完成支付&#xff0c;系统需要自动取消订单并释放库存。这种场景如果使用传统的轮询…

作者头像 李华
网站建设 2026/11/2 10:17:32

Linux内核演进:从C到Rust的内存安全与并发革命

1. 从C到Rust&#xff1a;Linux内核的语言演进史 在操作系统开发领域&#xff0c;Linux内核长期被视为C语言的最后堡垒。这个由2500万行C代码构建的庞然大物&#xff0c;支撑着全球80%的服务器和98%的超算系统。然而2026年Linux基金会发布的年度报告显示&#xff0c;内核中新提…

作者头像 李华
网站建设 2026/11/2 10:17:18

差分算法原理与高效区间更新实践

1. 差分算法基础概念差分算法是一种高效处理区间更新操作的数据处理技术&#xff0c;核心思想是通过构建差分数组来记录变化量&#xff0c;从而将区间操作的时间复杂度从O(n)降低到O(1)。我第一次接触这个概念是在处理大规模数据批量更新时&#xff0c;传统方法导致性能瓶颈&am…

作者头像 李华