首页 / 资讯中心 / 文章详情

单机多Tomcat部署:环境变量与端口冲突的完整解法

单机多Tomcat部署:环境变量与端口冲突的完整解法 ★ FEATURED ARTICLE
先说个真实场景前两天一个同行问我能不能在一台服务器上同时跑三个Tomcat。我说当然能只要资源够、端口规划好跑十个都行。结果他紧接着甩过来一张截图三个实例只有最后一个能起来而且用shutdown关闭其中一个另外两个进程也跟着没了。他用的还是常见的JDK 1.8搭配Tomcat 8.5部署方式也是网上常见的“复制三份Tomcat再改端口”但就是不行。这类问题在单机多实例部署里非常典型根源基本都集中在环境变量和PID文件上。这篇文章我就按自己实际部署的流程把JDK 1.8安装、三个Tomcat实例搭建、以及“无法同时启动”和“shutdown串杀”这两个坑的完整解法写清楚。如果你正准备在测试服务器上复用Tomcat或者已经被这两个问题折磨过这篇文章可以直接照着抄。1. 整体思路多实例部署为什么容易“互相打架”1.1 单机多Tomcat的需求场景很多人觉得一台服务器跑多个Tomcat没必要但实际运维中这个需求非常普遍。最常见的是公司只有一台性能尚可的测试服务器却有多个项目需要联调或者是同一个应用需要开多个实例做负载模拟再或者是不同团队共用一台机器各自需要独立的Tomcat环境互不影响。这种情况下多实例部署的好处是很明显的端口独立、日志独立、部署目录独立。一个实例挂了或者重启不会影响其他实例某个应用需要单独调内存参数也可以在自己的脚本里配置不需要影响全局。比虚拟机或者容器要轻量得多也比每次只跑一个Tomcat来回切换要高效。1.2 两种部署方式怎么选单机多Tomcat有两种主流做法我先说结论推荐用“完整目录复制”也就是每个实例有自己独立的Tomcat解压目录。另一种做法是“单安装目录多CATALINA_BASE”共享一套二进制文件但每个实例有独立的conf、logs、webapps目录。这两种方式我都用过列个表对比一下对比项完整目录复制单目录多BASE部署难度低适合新手中需要理解CATALINA_BASE概念磁盘占用较高每个实例一套较低共享bin和lib升级维护每个实例单独升级升级一次所有实例都变隔离性好改一个实例不影响其他好配置和日志隔离出错率较低较高概念不熟容易踩坑对于大多数场景我的建议是直接用完整目录复制。理由很简单隔离最彻底思路也最直观。复制三份目录改三个端口每个实例的环境变量都写在自己的启动脚本里互不依赖。虽然磁盘多占几百MB但对于一台服务器来说几乎可以忽略。等你对Tomcat的启动机制足够熟了再尝试单目录多BASE也不迟。1.3 踩坑的根源环境变量污染和端口冲突先说环境变量污染。Tomcat的启动脚本catalina.sh依赖两个关键变量CATALINA_HOME和CATALINA_BASE。很多人喜欢把CATALINA_HOME写进/etc/profile全局环境变量里这本身问题不大但如果你同时开多个Tomcat问题就来了。当你在当前shell里执行了某个Tomcat的startup.sh脚本内部会设置一些变量或者你在全局环境变量里写死了某个目录的CATALINA_HOME那么执行另一个Tomcat的脚本时脚本不会重新解析路径而是沿用当前shell里已有的变量。最后的结果就是你启动了第二个实例实际调用的却可能是第一个实例的catalina.sh互相覆盖、互相干扰自然无法同时启动。再说端口冲突。Tomcat的server.xml里有三个关键端口SHUTDOWN管理端口、HTTP连接器端口、AJP连接器端口。如果你只改了HTTP端口忘记改SHUTDOWN端口和AJP端口那么第二个实例启动时就会提示端口被占用。更恐怖的是如果多个实例的SHUTDOWN端口一样你执行shutdown.sh时系统会向这个端口发送SHUTDOWN指令所有监听这个端口的实例都会被关闭这就是“关一个全没”的经典原因之一。这两个问题叠加在一起就是标题里说的两个现象多个Tomcat无法同时启动、关闭一个Tomcat导致其他Tomcat一同关闭。理解了原理解决起来其实很有套路。2. JDK 1.8安装与环境变量配置要点2.1 为什么还在用JDK 1.8标题里提到version 1.8这里的1.8我理解是指JDK 1.8。虽然JDK 17、21都已经很成熟了但很多老项目、内部系统、以及特定版本的Tomcat运行环境依然依赖JDK 1.8。再加上某些中间件或者老代码用了JDK 1.8才有的内部API升级成本很高所以现在很多服务器上JDK 1.8依然是主力。我用的是jdk-8u391这个版本搭配Tomcat 8.5这个组合在兼容性上非常稳。如果你的项目没有特别需求直接按下面流程装JDK 1.8就行。要是项目必须用更高版本后面的Tomcat部署逻辑是一样的不影响。2.2 JDK 1.8的下载与解压安装安装JDK的第一步是拿到Linux x64版本的tar.gz压缩包。如果服务器能访问外网可以从Oracle官方归档页面下载如果公司内网有软件镜像直接从这里拉更省事速度也快。我这里是先下载到本机再上传到服务器的/root目录下。文件准备好后解压到统一目录。我的习惯是放在/usr/local/java下面这样后续找路径方便mkdir -p /usr/local/java tar -zxvf jdk-8u391-linux-x64.tar.gz -C /usr/local/java cd /usr/local/java ls -l解压完成后目录名一般是jdk1.8.0_391。如果你希望目录短一点也可以用mv命令改个名比如mv /usr/local/java/jdk1.8.0_391 /usr/local/java/jdk8这样JAVA_HOME的路径就是/usr/local/java/jdk8后面配置起来更简洁。当然不改名也行全凭个人习惯。2.3 JAVA_HOME和PATH的配置JDK解压完还不能直接用需要把JAVA_HOME、PATH、CLASSPATH配置到环境变量里。这里有个细节如果你只是临时在当前shell里测试可以先用export命令临时设置但为了持久生效我建议配置到/etc/profile里这样所有用户登录时都会加载。用vim打开/etc/profile在文件末尾追加以下内容export JAVA_HOME/usr/local/java/jdk8 export PATH$JAVA_HOME/bin:$PATH export CLASSPATH.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar保存退出后执行source命令让配置立即生效source /etc/profile然后验证一下Java是否可用java -version如果输出类似下面这样说明JDK安装成功java version 1.8.0_391 Java(TM) SE Runtime Environment (build 1.8.0_391-b13) Java HotSpot(TM) 64-Bit Server VM (build 25.391-b13, mixed mode)2.4 安装JDK时容易忽略的坑第一个坑是系统自带的OpenJDK干扰。有些Linux发行版预装了OpenJDK执行java -version时命中的可能是系统自带的版本而不是你刚装的Oracle JDK。如果你用which java查看看到的是/usr/bin/java说明PATH的顺序有问题。解决方案是把JAVA_HOME/bin放到PATH最前面并确保/etc/profile里的配置在系统默认配置之后加载。我一般还会用update-alternatives来管理但最简单的方式就是在/etc/profile里把$JAVA_HOME/bin放在最前面。第二个坑是root用户和普通用户的环境变量不一致。如果你是在root下配置的/etc/profile切换到普通用户时发现java命令不存在大概率是普通用户的shell没有加载/etc/profile或者加载顺序不对。这种情况可以在普通用户的.bashrc里也加上对应的export或者直接用绝对路径/usr/local/java/jdk8/bin/java去验证。第三个坑是CLASSPATH里的点和冒号。CLASSPATH开头的.表示当前目录中间用冒号分隔多个路径。很多人抄配置的时候把点和冒号搞丢导致编译运行ClassNotFound其实这个是老生常谈但确实容易手滑。3. 多个Tomcat实例的目录规划与端口配置3.1 目录结构如何规划JDK装好了接下来搭建多个Tomcat实例。我建议先把目录结构想清楚不要随便放在奇怪的位置。一个比较清晰的布局是这样的/opt/tomcat/ ├── tomcat-8080 ├── tomcat-8081 └── tomcat-8082每个目录都是一份完整的Tomcat解压包目录名直接体现端口号一看就知道哪个实例对应哪个端口。当然你也可以用tomcat-app1、tomcat-app2这种方式命名但用端口号最直观排查问题时不用再去翻配置文件。具体操作就是先下载一份Tomcat 8.5的tar.gz然后解压三份mkdir -p /opt/tomcat cd /opt/tomcat tar -zxvf apache-tomcat-8.5.100.tar.gz mv apache-tomcat-8.5.100 tomcat-8080 cp -r tomcat-8080 tomcat-8081 cp -r tomcat-8080 tomcat-8082复制完记得检查一下目录权限。如果Tomcat要以某个非root用户运行需要把目录属主改成对应用户如果直接用root跑权限问题不大但为了安全我还是建议创建一个专门的运行用户比如tomcat用户然后把三个目录属主都改给它。3.2 server.xml中的三个端口必须全部改每份Tomcat的conf目录下都有server.xml需要修改三个端口。第一个是Server标签的port也就是SHUTDOWN管理端口第二个是HTTP连接器端口也就是我们访问Web用的端口第三个是AJP连接器端口如果你不用AJP协议这个端口可以不改但建议还是改掉避免和其他实例冲突。以tomcat-8080为例我的规划是这样的实例SHUTDOWN端口HTTP端口AJP端口tomcat-8080800580808009tomcat-8081800680818010tomcat-8082800780828011修改server.xml的时候第一个实例通过编辑器找到这三处把8081、8082对应的实例依次改掉。千万不要只改HTTP端口就启动否则你会在日志里看到大量bind失败报错。Server port8005 shutdownSHUTDOWN ... Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 / ... Connector port8009 protocolAJP/1.3 redirectPort8443 / /Server3.3 端口规划之外还需要注意什么端口改完后很多人以为就大功告成了。实际上还有一个容易忽略的点Tomcat运行时会生成临时文件、日志、PID文件等。如果你每个实例目录独立这些文件天然就是独立的这也是我推荐完整目录复制的原因之一。如果你用的是软链接或者复制的时候保留了一些奇怪的文件可能导致两个实例共享同一份logs目录看起来就像“日志串了”。另外如果你的服务器开启了防火墙还要确保新端口在防火墙白名单里。很多情况下Tomcat能启动、本机curl能通但外部访问不了就是这个原因。CentOS上可以检查firewalldUbuntu上检查ufw把这些端口放行即可。3.4 为每个实例创建独立的setenv.sh这一步是解决多个Tomcat互相干扰的核心。Tomcat启动时catalina.sh脚本会自动加载bin目录下的setenv.sh文件如果存在的话。利用这个机制我们为每个实例创建独立的setenv.sh把JAVA_HOME、CATALINA_HOME、CATALINA_BASE、CATALINA_PID、JAVA_OPTS都明确写死。以tomcat-8080为例在/opt/tomcat/tomcat-8080/bin/目录下创建setenv.sh内容如下export JAVA_HOME/usr/local/java/jdk8 export CATALINA_HOME/opt/tomcat/tomcat-8080 export CATALINA_BASE/opt/tomcat/tomcat-8080 export CATALINA_PID/opt/tomcat/tomcat-8080/bin/tomcat.pid export JAVA_OPTS-Xms512m -Xmx1024m -XX:MetaspaceSize128m -Djava.security.egdfile:/dev/./urandomtomcat-8081和tomcat-8082的setenv.sh内容类似只要把路径和PID文件路径改成各自目录即可。同时给setenv.sh执行权限chmod x /opt/tomcat/tomcat-8080/bin/setenv.sh chmod x /opt/tomcat/tomcat-8081/bin/setenv.sh chmod x /opt/tomcat/tomcat-8082/bin/setenv.sh这里的CATALINA_PID是后面解决shutdown串杀问题的关键先记住这个变量名下一节详细讲。4. 多个Tomcat无法同时启动的根因与解决4.1 为什么启动了一个就起不来第二个如果你已经按前面的步骤改了端口、写了setenv.sh还遇到无法同时启动的问题那大概率是环境变量在作祟。最常见的情况是这样的用户在/etc/profile里配置了CATALINA_HOME指向tomcat-8080然后在当前shell里循环执行三个目录的startup.sh。第一个实例正常启动第二个实例因为CARALINA_HOME已经不是默认路径而是指向tomcat-8080的全局变量导致第二个实例调用了tomcat-8080的catalina.sh启动时又找不到自己的目录配置最后启动失败。还有一种是执行顺序问题。Tomcat的startup.sh脚本在执行时会检测CATALINA_HOME是否存在。如果环境变量里已经有值脚本就不会再根据自身目录去推断而是直接使用现有值。你上一个实例的startup.sh可能已经把CATALINA_HOME导出到了当前shell环境虽然startup.sh本身不会导出一个新的值但它会在调用catalina.sh的时候把变量传递下去如果你的操作方式是source startup.sh而不是执行./startup.sh那当前shell环境就变了后面的实例全部受影响。4.2 核心解法让每个实例完全自包含解决思路很简单就是让每个实例不依赖外部的CATALINA_HOME和CATALINA_BASE环境变量全部由自身目录下的setenv.sh来定义。这样你在任何目录下执行任何实例的startup.sh它加载的都是自己目录下的setenv.sh不会受到全局污染。同时绝对不要在/etc/profile里写CATALINA_HOME和CATALINA_BASE。JDK的JAVA_HOME可以放全局但Tomcat的实例变量必须留在各自的setenv.sh里。这也是很多教程没讲透的地方喜欢把CATALINA_HOME写进/etc/profile用的时候又因为多实例而互相干扰。启动时分别执行三个命令/opt/tomcat/tomcat-8080/bin/startup.sh /opt/tomcat/tomcat-8081/bin/startup.sh /opt/tomcat/tomcat-8082/bin/startup.sh4.3 验证三个实例是否同时存活启动完成后不要急着说成功先看进程和端口状态。用ps命令看Java进程ps -ef | grep java | grep -v grep正常情况下应该看到三个Java进程而且每个进程的启动参数里应该有各自不同的-Dcatalina.base路径。比如tomcat-8080进程有-Dcatalina.base/opt/tomcat/tomcat-8080tomcat-8081进程有-Dcatalina.base/opt/tomcat/tomcat-8081以此类推。如果看到两个进程的catalina.base指向同一个目录说明还是有环境变量问题。再看端口监听lsof -i:8080 lsof -i:8081 lsof -i:8082三个端口分别有Java进程监听就说明启动成功了。最后用curl做一次HTTP验证curl -I http://localhost:8080 curl -I http://localhost:8081 curl -I http://localhost:8082能返回HTTP状态码200说明Web服务也正常。有些Tomcat版本默认页面返回404需要确认首页文件存在但至少端口是通的说明没有端口冲突。4.4 补充单目录多BASE方案下为什么会启动失败如果你用的是单安装目录加多CATALINA_BASE的方案启动失败的原因通常是CATALINA_BASE指向的目录下缺少logs、work、temp等目录。Tomcat会严格按照CATALINA_BASE来寻找配置和生成临时文件如果这些目录不存在它会尝试创建但权限不足时就会失败。我的建议是先用mkdir把目录建齐并赋予正确属主再创建对应目录下的conf文件。不过就像我在前面说的如果没有特别强烈的磁盘空间需求不建议新手走这条路线。完整目录复制是最省心的。5. 关闭单个Tomcat导致其他一并关闭的彻底修复5.1 shutdown串杀的现象和原因三个Tomcat都正常启动了你觉得问题解决了结果执行一下/opt/tomcat/tomcat-8081/bin/shutdown.sh发现8080和8082也一起没了。这个现象我在多个环境里见过原因无非两种。第一种原因是端口没隔离干净尤其是SHUTDOWN管理端口。shutdown.sh的原理是向Tomcat的SHUTDOWN端口发送一个字符串指令默认是SHUTDOWN。如果你三个实例的Server端口都是8005第一个实例占用了8005第二个实例启动时就会报端口占用但如果你是用一些特殊方式强行让多个实例监听同一个端口或者修改了策略那shutdown指令就会把这个端口上所有监听的实例全部关闭。正确的做法就是三个实例的SHUTDOWN端口完全不同就像我在端口规划表里写的那样。第二种原因是环境变量导致执行了错误的catalina.sh。当你执行tomcat-8081/bin/shutdown.sh时如果当前shell环境里的CATALINA_HOME还是tomcat-8080那么这个shutdown.sh实际调用的是tomcat-8080的catalina.sh stop结果就是关闭了tomcat-8080。这个现象表面上看起来像“关闭一个导致其他也关了”实际上是你压根没关对那个实例。还有第三种情况很多人在排查问题时喜欢用pkill -9 java或者pkill -f catalina这样的暴力命令。因为所有Tomcat进程的进程名都是java命令行里都包含catalina字样这样一杀就是全部。如果你看到Tomcat进程一起挂掉回想一下自己是不是用了这类命令。shutdown.sh本身很安全只要环境正确它只会关闭自己对应的实例。5.2 CATALINA_PID的作用与正确配置Tomcat的catalina.sh里有一个CATALINA_PID变量用来指定进程PID文件的路径。如果setenv.sh里配置了CATALINA_PID启动时Tomcat会把当前进程的PID写入这个文件停止时shutdown.sh会先从PID文件读取PID再向对应进程发送关闭信号。这个机制是为了更可靠地关闭Tomcat避免只靠SHUTDOWN端口指令失败。但CATALINA_PID如果配置错了也会引发串杀。比如你把三个实例的CATALINA_PID都指向同一个文件比如/tmp/tomcat.pid那么三个实例启动时都会把各自的PID写到同一个文件后启动的实例覆盖先启动的PID。关闭某个实例时shutdown.sh读到的PID是最后一个实例的PID于是直接把那个实例关了。看起来就像关闭一个Tomcat导致另一个被误杀。我在前面章节强调的setenv.sh配置每个实例都有独立的CATALINA_PID就是这个原因。三个实例的PID文件分别是/opt/tomcat/tomcat-8080/bin/tomcat.pid /opt/tomcat/tomcat-8081/bin/tomcat.pid /opt/tomcat/tomcat-8082/bin/tomcat.pid这样shutdown脚本拿到的PID一定是本实例的不会误伤。5.3 shutdown的完整验证过程配置好之后我们来做一个严格的验证。先确认三个实例都在运行ps -ef | grep java | grep -v grep | wc -l输出3表示三个进程都在。然后只关闭tomcat-8081/opt/tomcat/tomcat-8081/bin/shutdown.sh等几秒后再次查看进程数ps -ef | grep java | grep -v grep | wc -l输出2说明只关闭了一个。再查看8080和8082的端口lsof -i:8080 lsof -i:8082两个端口依然被监听说明其他实例完好无损。如果你执行shutdown.sh后该实例的端口没有被释放查看logs/catalina.out日志多半是JVM关闭时卡住了。这种情况下可以增加shutdown等待时间或者在确认PID正确后手动kill该PIDkill $(cat /opt/tomcat/tomcat-8081/bin/tomcat.pid)用了CATALINA_PID之后这种精准关闭非常方便。5.4 避免再次串杀的几条铁律这里总结几条经验都是我踩过坑之后沉淀下来的第一绝对不要在全局环境变量里设置CATALINA_HOME或CATALINA_BASE。这两个变量是每个Tomcat实例自己的必须放在各自bin/setenv.sh里。全局只放JAVA_HOME。第二三个端口必须全部独立不要以为HTTP端口改了就行。SHUTDOWN端口和AJP端口漏改是最容易引发“shutdown串杀”的原因。第三关闭实例一律用自己目录下的shutdown.sh不要用pkill、不要用killall java更不要用jps然后按PID批量kill。这些都是新手容易踩的雷。第四重启Tomcat前确认没有残留的tomcat.pid文件。如果上次强制杀进程导致PID文件残留启动时可能读取到旧PID导致判断异常。稳妥的做法是启动前检查并删除rm -rf /opt/tomcat/tomcat-8080/bin/tomcat.pid6. 常见问题与排查技巧实录6.1 高频报错速查表下面这些报错我在实际部署和帮人排查时经常遇到整理成一个速查表报错信息原因解决方法java.net.BindException: Address already in use端口被占用或多个实例端口重复用lsof -i:端口确认占用情况修改重复端口The CATALINA_HOME environment variable is not defined correctly全局环境变量错误或setenv.sh路径错误删除全局CATALINA_HOME确认setenv.sh内路径正确Cannot find /opt/tomcat/xxx/bin/catalina.shsetenv.sh里CATALINA_HOME写错检查路径确保目录存在SEVERE: Failed to initialize end pointTomcat连接器端口绑定失败检查HTTP和AJP端口是否重复Server.xml configuration errorSHUTDOWN端口被占用或配置格式错误检查Server标签的port属性和shutdown字符串java.lang.OutOfMemoryError: Metaspace内存参数太小调整setenv.sh中的JAVA_OPTS6.2 定位问题的高效命令组合遇到Tomcat多实例问题我建议按下面的顺序排查。先看进程状态区分是“没启动”还是“启动后崩溃”ps -ef | grep java | grep -v grep接着看端口确认哪个实例占了哪些端口netstat -tlnp | grep java这个命令能输出PID和端口对应关系非常直观。如果你的netstat看不到PID可能是权限问题加sudo重试。再看Tomcat自己的日志。每个实例的logs目录下都有catalina.out这个是主日志。排查启动问题时直接查看尾部tail -100 /opt/tomcat/tomcat-8081/logs/catalina.out如果有报错再结合localhost.log一起看。这里要注意Tomcat的日志文件是按天切换的如果当天没启动成功日志可能写在catalina.out里但详细信息在localhost的当前日志中。6.3 最后一个实用技巧写一个统一启停脚本虽然我们已经让每个实例自包含但每次启动三个实例要敲三条命令时间长了还是烦。我习惯在主目录下写一个简单的启停脚本比如/opt/tomcat/tomcat-manager.sh内容如下#!/bin/bash case $1 in start) /opt/tomcat/tomcat-8080/bin/startup.sh /opt/tomcat/tomcat-8081/bin/startup.sh /opt/tomcat/tomcat-8082/bin/startup.sh ;; stop) /opt/tomcat/tomcat-8080/bin/shutdown.sh /opt/tomcat/tomcat-8081/bin/shutdown.sh /opt/tomcat/tomcat-8082/bin/shutdown.sh ;; status) ps -ef | grep java | grep -v grep ;; *) echo 用法: $0 {start|stop|status} ;; esac给脚本加执行权限后日常维护就简单了chmod x /opt/tomcat/tomcat-manager.sh /opt/tomcat/tomcat-manager.sh start不过使用这种统一脚本时要小心stop的时候是逐个执行shutdown.sh如果某个实例停得慢脚本不会等它完全退出就继续关下一个。如果三个实例都是独立环境变量互相不影响这个顺序执行没什么问题但如果某个实例因为内存不足卡住了可能迟迟关不掉。所以我实际使用时更倾向于单独关闭某个实例而不是用stop一把梭。我个人在实际操作中最深的体会是单机多Tomcat这种问题九成以上不是Tomcat本身的问题而是环境变量和端口规划没做干净。只要你坚持“每个实例自包含”的原则所有变量都写在各自的setenv.sh里三个端口各用各的启动和关闭用各自目录下的官方脚本那这三个问题基本不会出现。最后再分享一个小技巧每次启动前用rm -rf把对应实例的tomcat.pid清一下这个习惯能避免很多诡异现象信我你试一次就知道它的价值了。
阅读完成 · 觉得有帮助?
咨询建站