ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Tomcat单实例多端口配置:共享线程池与HTTP/HTTPS双端口实践

Tomcat单实例多端口配置:共享线程池与HTTP/HTTPS双端口实践

1. 为什么一个 Tomcat 要监听两个端口

第一次碰到“Tomcat 开启两个端口”这个需求,是在一个前后端分离还没完全普及的项目里。当时后端接口和一套老的管理后台都跑在同一个 Tomcat 上,前端同学希望接口走 8080,管理后台走 9090,理由是“方便做网关分流和日志区分”。听起来有点多此一举,但实际排查问题时确实省事——哪个端口出问题,日志一眼就能定位。

Tomcat 默认只监听一个 HTTP 端口,也就是server.xml里那个<Connector port="8080" ... />。所谓“开启两个端口”,本质上就是让 Tomcat 同时绑定两个不同的端口,对外提供两套访问入口。它可以是两个 HTTP 端口,也可以是一个 HTTP 加一个 HTTPS,甚至可以是 HTTP 加 AJP。核心动作只有一个:在conf/server.xml里增加一个<Connector>元素。

这件事解决的核心问题是单实例多入口。你不需要部署两个 Tomcat 实例,不需要复制两份webapps,也不用担心两个实例抢内存、抢 JVM 参数。一个 JVM 进程,两个监听端口,共享同一套应用和线程池配置,资源利用率更高,运维也更简单。

适合谁来参考?如果你正在做下面这些事,这篇内容基本能直接抄作业:

  • 想让同一套应用通过不同端口区分访问来源,比如内网 8080、外网映射 9090;
  • 想在不额外起实例的前提下,同时提供 HTTP 和 HTTPS 服务;
  • 本地开发时想模拟多端口环境,验证网关或反向代理的转发规则;
  • 接手了一个老项目,发现server.xml里已经有两个 Connector,但不知道各自作用。

需要提前说清楚:Tomcat 开两个端口,不是开两个独立的 Web 容器。两个端口背后是同一个Engine、同一个Host、同一份webapps。你在 8080 上部署的应用,9090 上同样能访问,路径完全一致。这一点如果理解错了,后面配置很容易踩坑。

2. 核心原理:Connector 到底在做什么

2.1 一个 Connector 就是一条监听通道

Tomcat 的架构里,Server是最外层容器,里面包着Service,Service里再包Connector和Engine。Connector负责的是网络通信层:它监听某个端口,接收 TCP 连接,按照协议解析请求,然后把请求交给Engine处理,处理完再把响应写回去。

你可以把Connector理解成公司前台。前台可以有好几个,一个负责接待普通访客(HTTP),一个负责接待 VIP(HTTPS),但他们把访客领进去之后,见到的都是同一套内部团队(Engine 和具体的 Web 应用)。前台数量不影响内部团队的数量。

所以增加一个端口,就是在Service里多放一个Connector。多个Connector共享同一个Engine,这是 Tomcat 原生支持的,不需要任何插件。

2.2 两个端口共享线程池的真相

很多人担心“开两个端口会不会线程数翻倍”。默认情况下,每个Connector有自己的执行器配置,如果不显式指定共享,Tomcat 会为每个 Connector 维护各自的线程池。但你可以通过executor属性让多个 Connector 共用一个线程池。

具体做法是在Service里先定义一个<Executor>,然后两个Connector都通过executor="tomcatThreadPool"引用它。这样无论请求从哪个端口进来,用的都是同一批工作线程,总线程数可控,不会因为多开一个端口就把内存吃满。

这个细节在官方文档里写得很清楚,但实际配置时经常被忽略。我见过一个项目,两个 Connector 各自默认 200 个线程,结果高峰期线程数直接飙到 400,GC 压力明显上升。后来改成共享 Executor,问题立刻缓解。

2.3 端口冲突与绑定顺序

Tomcat 启动时,会按server.xml中Connector出现的顺序依次绑定端口。如果第二个端口已经被别的进程占用,启动日志里会出现Address already in use,但第一个端口通常已经绑定成功。这时候 Tomcat 可能整体启动失败,也可能只报错不退出,取决于版本和配置。

所以配置两个端口之前,务必先用系统命令确认端口空闲。Linux 下用ss -lntp | grep 端口号,Windows 下用netstat -ano | findstr 端口号。这一步花十秒钟,能省掉后面半小时的排查。

3. 动手配置:从单端口到双端口

3.1 找到并备份 server.xml

配置文件位置在 Tomcat 安装目录的conf/server.xml。改之前先复制一份,命名成server.xml.bak。这不是形式主义,我确实遇到过改错一个标签导致 Tomcat 起不来、又忘了原始内容的情况,最后只能重新解压一份 Tomcat 对比。

用文本编辑器打开server.xml,找到类似下面这段:

<Service name="Catalina"> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> <Engine name="Catalina" defaultHost="localhost"> ... </Engine> </Service>

这就是默认的单端口配置。Connector只有一个,端口 8080。

3.2 新增第二个 Connector

在原有Connector后面,紧挨着再加一个:

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

保存,重启 Tomcat。访问http://localhost:8080/你的应用和http://localhost:9090/你的应用,应该都能打开同一个页面。

这里有几个参数值得说明:

  • port:监听端口,两个 Connector 必须不同。
  • protocol:HTTP/1.1 是默认的 NIO 实现,Tomcat 8 以后推荐保持默认,不要手动写org.apache.coyote.http11.Http11NioProtocol,除非有特殊需求。
  • connectionTimeout:连接超时,单位毫秒。两个 Connector 可以设不同值,比如内网端口设短一点,外网端口设长一点。
  • redirectPort:当收到需要 SSL 的请求时,重定向到的端口。如果两个 Connector 都走 HTTP,这个值保持一致即可。

3.3 共享线程池的正确写法

如果希望两个端口共用线程池,把配置改成这样:

<Service name="Catalina"> <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="300" minSpareThreads="30" maxIdleTime="60000" prestartminSpareThreads="true"/> <Connector port="8080" protocol="HTTP/1.1" executor="tomcatThreadPool" connectionTimeout="20000" redirectPort="8443" /> <Connector port="9090" protocol="HTTP/1.1" executor="tomcatThreadPool" connectionTimeout="20000" redirectPort="8443" /> <Engine name="Catalina" defaultHost="localhost"> ... </Engine> </Service>

maxThreads="300"表示整个 Service 最多 300 个工作线程,两个端口共享。minSpareThreads="30"是保持的最小空闲线程数。prestartminSpareThreads="true"让 Tomcat 启动时就把最小空闲线程创建好,避免第一波请求来时临时创建线程造成延迟。

注意:Executor必须定义在Connector之前,否则 Tomcat 启动时会报找不到 executor 的错。XML 解析是按顺序来的,这个顺序不能乱。

3.4 验证配置是否生效

重启后,查看 Tomcat 启动日志,通常会在logs/catalina.out或控制台输出里看到类似内容:

信息: Starting ProtocolHandler ["http-nio-8080"] 信息: Starting ProtocolHandler ["http-nio-9090"]

两行都出现,说明两个端口都绑定成功。如果只有一行,或者出现Address already in use,就要去检查端口占用。

另一个验证方式是看 Tomcat 管理页面(如果启用了 manager 应用),在Server Status里能看到当前 Service 下的 Connector 列表,两个端口都会列出来。

4. 进阶场景:HTTP 与 HTTPS 双端口共存

4.1 为什么需要 HTTP 和 HTTPS 同时开

有些场景下,内部服务调用走 HTTP 图个快,对外用户访问必须走 HTTPS 保证安全。这时候一个 Tomcat 同时开两个端口就很合适:8080 走 HTTP 给内网,8443 走 HTTPS 给外部。

HTTPS 的 Connector 配置比 HTTP 多几个参数:

<Connector port="8443" protocol="HTTP/1.1" executor="tomcatThreadPool" maxThreads="200" scheme="https" secure="true" SSLEnabled="true" keystoreFile="conf/your_keystore.jks" keystorePass="你的密码" clientAuth="false" sslProtocol="TLS" connectionTimeout="20000" />

关键参数解释:

  • SSLEnabled="true":开启 SSL。
  • scheme="https":告诉 Tomcat 这个连接是安全的,应用里request.getScheme()会返回 https。
  • secure="true":标记该连接为安全连接,request.isSecure()返回 true。
  • keystoreFile:证书库文件路径,可以是绝对路径或相对于 Tomcat 根目录的路径。
  • keystorePass:证书库密码。
  • sslProtocol:建议用 TLS,不要用 SSLv3,后者已经不安全。

4.2 证书准备的实操细节

证书库可以用keytool生成自签名证书做测试:

keytool -genkeypair -alias tomcat -keyalg RSA -keysize 2048 \ -keystore conf/your_keystore.jks -validity 365 \ -storepass 你的密码 -keypass 你的密码 \ -dname "CN=localhost, OU=Dev, O=Demo, L=City, ST=State, C=CN"

生产环境当然要用正式证书。拿到证书后,通常需要把 PEM 格式转成 JKS 或 PKCS12 格式。PKCS12 现在是更推荐的选择,Tomcat 9 以后对 PKCS12 支持很好,配置里把keystoreType="PKCS12"加上即可。

提示:keystorePass直接写在server.xml里是明文,有安全风险。更稳妥的做法是用 Tomcat 的property替换或者外部化配置。本地测试无所谓,生产环境一定要处理。

4.3 两个端口的请求分流验证

配置完成后,分别访问:

  • http://localhost:8080/应用路径:正常返回,浏览器地址栏显示 http。
  • https://localhost:8443/应用路径:浏览器可能提示证书不受信任(自签名证书的正常现象),继续访问后能看到同样内容。

在应用代码里打印request.getScheme()和request.isSecure(),可以确认两个端口进来的请求属性确实不同。这一点在做重定向或生成绝对 URL 时很重要,如果搞混了,可能生成 http 链接给 https 页面,浏览器会报混合内容警告。

5. 常见问题与排查速查

5.1 启动报端口被占用

现象:日志出现java.net.BindException: Address already in use。

排查步骤:

  1. 确认端口是否被其他进程占用。Linux 用ss -lntp | grep 9090,Windows 用netstat -ano | findstr 9090。
  2. 如果是被别的 Tomcat 占用,改端口或停掉那个实例。
  3. 如果是被自己上一次没关干净的 Tomcat 占用,用ps -ef | grep tomcat找到进程号,kill -9掉再重启。
  4. 检查server.xml里是不是不小心写了两个相同的port值。

5.2 第二个端口访问 404

现象:8080 能访问应用,9090 返回 404。

原因通常不是 Connector 的问题,而是应用部署路径的问题。两个 Connector 共享同一个 Host,理论上访问路径应该完全一致。如果 9090 返回 404,先确认访问的 URL 路径是否和 8080 完全一样,包括应用名和大小写。Tomcat 对 URL 路径大小写敏感,/MyApp和/myapp是两个不同的路径。

另一个可能是 9090 的 Connector 配置里加了URIEncoding或别的参数导致解析差异,但这种概率很低。最直接的排查方式是看 9090 请求进来时localhost_access_log里记录的路径是什么。

5.3 两个端口性能表现不一致

现象:同样压力下,一个端口响应快,另一个慢。

如果两个 Connector 没有共享 Executor,各自线程池独立,可能出现一个端口线程打满、另一个端口线程空闲的情况。解决办法就是前面说的,配置共享 Executor。如果已经共享了,检查两个 Connector 的acceptorThreadCount、pollerThreadCount等参数是否一致。这些参数控制的是网络层接收线程,不共享,但通常不需要改,默认值够用。

5.4 修改后 Tomcat 启动变慢

增加 Connector 本身不会明显拖慢启动。如果感觉启动变慢,检查是不是prestartminSpareThreads="true"配合了很大的minSpareThreads。启动时预创建大量线程确实会花时间。把minSpareThreads调小,或者关掉预创建,让线程按需增长。

5.5 常见问题速查表

问题现象可能原因解决方向
启动报 Address already in use端口被占用换端口或停占用进程
第二个端口 404访问路径不一致核对 URL 大小写和应用名
一个端口快一个端口慢线程池未共享配置共享 Executor
启动变慢预创建线程过多调小 minSpareThreads
HTTPS 端口浏览器警告自签名证书测试环境忽略,生产换正式证书
应用内生成链接协议错误scheme/secure 未配检查 Connector 的 scheme 和 secure

6. 几个容易被忽略的实操心得

第一个心得:改 server.xml 之前一定先停 Tomcat。热加载server.xml在部分版本里支持,但 Connector 的增删通常需要重启才能生效。边跑边改,容易出现配置读了一半、状态不一致的情况。

第二个心得:两个端口的日志最好分开。Tomcat 的 access log 默认按 Host 或 Engine 记录,不区分 Connector。如果确实需要按端口区分访问日志,可以在 Connector 上配置accessLogPrefix或者在应用层用 Filter 根据request.getLocalPort()打不同标记。这个需求不常见,但一旦有,提前规划比事后补要省事。

第三个心得:防火墙和安全组别忘了放行新端口。本地测试通了,部署到服务器上访问不了,十有八九是防火墙没开。Linux 的firewalld或iptables,云服务器的安全组规则,都要检查。这个坑我踩过不止一次,每次都是先怀疑 Tomcat,最后发现是网络层。

第四个心得:端口号不要选太随意的值。9090、8081、8443 这些都是常见选择,但也要注意别和系统里其他服务撞车。比如 9090 有时被 Prometheus 占用,8443 有时被其他 Web 服务占用。选之前扫一眼当前监听列表,心里有数。

第五个心得:配置改完后用catalina.sh configtest做语法检查。这个命令会解析server.xml并报告语法错误,不用真正启动 Tomcat。在正式重启前跑一下,能提前发现标签闭合错误、属性拼写错误这类低级问题。Windows 下对应的是catalina.bat configtest。

7. 配置模板与参数速查

把前面零散的内容整理成一份可以直接复制的模板,方便对照修改:

<Service name="Catalina"> <Executor name="sharedThreadPool" namePrefix="catalina-exec-" maxThreads="300" minSpareThreads="30" maxIdleTime="60000" prestartminSpareThreads="false"/> <!-- 端口一:HTTP 内网访问 --> <Connector port="8080" protocol="HTTP/1.1" executor="sharedThreadPool" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" compression="on" compressionMinSize="2048" compressableMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/json"/> <!-- 端口二:HTTP 管理后台或外网映射 --> <Connector port="9090" protocol="HTTP/1.1" executor="sharedThreadPool" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/> <Engine name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <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 %{X-Forwarded-For}i"/> </Host> </Engine> </Service>

参数速查表:

参数作用建议值
port监听端口两个 Connector 必须不同
protocol协议实现HTTP/1.1 默认即可
executor共享线程池引用两个 Connector 填同一个名字
connectionTimeout连接超时毫秒20000 左右
redirectPortSSL 重定向端口与 HTTPS 端口一致
URIEncodingURL 编码UTF-8
compression是否压缩响应on,节省带宽
maxThreads最大工作线程根据并发量,300 起步
minSpareThreads最小空闲线程30 左右

提示:compression开启后,对文本类响应压缩效果明显,但图片、视频这类已经压缩过的内容不要重复压缩,所以compressableMimeType里只列文本类型。

8. 端口数量还能继续加吗

理论上,一个Service下可以放任意多个Connector,只要端口不冲突。我见过一个内部系统开了四个端口:8080 给普通用户、8081 给移动端、8082 给内部服务调用、8443 给 HTTPS。四个 Connector 共享一个 Executor,跑在一台 4 核 8G 的机器上,日常压力完全扛得住。

但端口不是越多越好。每多一个 Connector,就多一份网络监听资源,多一份配置维护成本。如果两个端口的用途没有本质区别,只是访问路径不同,那完全可以用一个端口加不同的 Context 路径来解决,没必要开两个端口。

判断标准很简单:两个端口的请求是否需要不同的网络层行为。比如一个要 HTTPS 一个不要,一个要限制来源 IP 一个不要,一个要不同的超时策略一个不要——这些才是开两个端口的合理理由。如果只是“想区分一下”,那用路径区分更合适。

另外,端口开多了之后,监控和日志的复杂度会上升。每个端口的连接数、吞吐量、错误率都要单独看,运维成本是实打实增加的。我个人的经验是,单实例超过三个 Connector 就要停下来想想,是不是该拆成两个实例或者上反向代理了。

9. 和反向代理配合时的注意事项

很多生产环境里,Tomcat 前面还有一层反向代理。这时候 Tomcat 开两个端口,反向代理的配置也要对应调整。

假设反向代理监听 80 和 443,分别转发到 Tomcat 的 8080 和 8443,那么反向代理配置里要有两条转发规则。关键是转发时要带上正确的头信息:

proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;

X-Forwarded-Proto这个头特别重要。如果 Tomcat 的 HTTPS Connector 没有配scheme="https",应用里生成绝对 URL 时可能生成 http 链接,导致浏览器混合内容警告。加上这个头,再配合 Tomcat 的RemoteIpValve,就能让应用正确识别原始请求协议。

Tomcat 端的RemoteIpValve配置放在Engine或Host里:

<Valve className="org.apache.catalina.valves.RemoteIpValve" remoteIpHeader="X-Forwarded-For" protocolHeader="X-Forwarded-Proto" protocolHeaderHttpsValue="https"/>

这样应用里request.getScheme()就会根据X-Forwarded-Proto返回正确的值,而不是永远返回 Connector 上配的那个。

10. 我个人的使用体会

Tomcat 开两个端口这件事,配置本身五分钟就能搞定,真正花时间的是想清楚“为什么要开”和“开了之后怎么维护”。我自己的习惯是,每加一个 Connector,就在server.xml里用注释写清楚这个端口的用途、负责人、开通日期。过半年再回来看,不用去翻文档就能明白每个端口是干嘛的。

还有一点,改完配置后不要只测“能不能访问”,要测“两个端口的请求是否真的走了同一套应用逻辑”。最简单的办法是在应用里加一个接口,返回request.getLocalPort(),然后分别从两个端口调一下,看返回值是否和预期一致。这个验证花不了一分钟,但能避免“以为配好了其实没生效”的尴尬。

最后,如果只是本地开发临时用,改完记得改回去。我见过有人把测试用的双端口配置直接提交到生产分支,结果上线后多开了一个不该开的端口,虽然没出大事,但被安全扫描报了出来,解释起来挺麻烦的。配置文件的版本管理,和代码一样重要。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进