尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

云原生时代Tomcat实战指南:从核心部署到容器化调优

云原生时代Tomcat实战指南:从核心部署到容器化调优 云原生时代一说到企业级 WEB 应用服务器很多人第一反应是“这不就是 Tomcat 嘛”但真往深了问Tomcat 在企业级场景里到底承担什么角色、为什么它能活二十年还没被淘汰、云原生架构下它又该怎么调整能讲清楚的人并不多。我这两年前前后后帮团队做过好几轮 Tomcat 标准化迁移和容器化改造从裸机部署到 Kubernetes 里跑实例中间踩过的坑、总结出的经验应该对正在做同样事情的人有点参考价值。这篇文章我会从 Tomcat 在企业级应用里的核心定位讲起把安装配置、多应用部署、性能参数调整、容器化适配这几个关键环节完整过一遍。不管你是刚接触 Java Web 工程的学生还是正在做云原生改造的技术负责人都能在里面找到可以直接抄作业的内容。1. 企业级 WEB 应用服务器为什么绕不开 Tomcat1.1 Tomcat 在企业应用栈中的真实位置先纠正一个常见误解Tomcat 不是 Web 服务器而是 Servlet 容器或者按 Java EE 规范里的说法叫 Web 容器。它负责的事情是加载 Servlet、处理 JSP、管理 Session、处理 HTTP 请求的生命周期而静态资源的处理和负载均衡这些事通常交给前面的 Nginx 或者网关来做。企业级架构里典型的链路是这样的客户端请求先到 NginxNginx 做静态资源响应、反向代理、负载均衡然后把动态请求转发给后面的 Tomcat 实例。Tomcat 再通过 JDBC 连接池访问数据库通过 Redis 做缓存和 Session 共享通过消息队列做异步解耦。所以 Tomcat 处在整个请求链路的中间层它本身不算复杂但它连接的上下游非常多这就导致它的配置和调优涉及面很广。一个小型创业公司的订单系统可能就是一个 Spring Boot 应用打成 war 包丢进 Tomcat前面挂一个 Nginx后面接一个 MySQL整套系统就能跑起来。但到了中大型企业事情就没这么简单了多个应用需要部署在同一批 Tomcat 上、不同环境的配置要隔离、流量高峰时要快速扩容、上线新版本要尽量不停机。这些需求都会直接反映到 Tomcat 的部署方式和参数配置上。1.2 为什么云原生架构下还是要用 Tomcat有人觉得“上了云原生就一切容器化、Serverless 化了Tomcat 是不是该退休了”。这个想法其实是对云原生的一种误解。云原生解决的是基础设施层面的弹性和标准化问题而 Tomcat 解决的是 Java Web 应用运行环境的问题两者所在的层次并不冲突。你看现在主流的 Spring Boot 应用内嵌的 Web 容器默认就是 Tomcat。你写了一个 Spring Boot 应用打成 jar 包跑起来里面其实就内嵌了一个 Tomcat 实例。这种情况下你甚至感觉不到 Tomcat 的存在但它确实在底层工作着。到了某些特殊场景比如企业要求使用国产化中间件替换或者需要更灵活的管理能力时又要把内嵌 Tomcat 换成外置部署方式这也说明 Tomcat 的部署模式是可以根据场景灵活切换的。云原生架构强调应用与基础设施解耦但应用的运行载体始终需要一个容器或运行时。你可以在 Docker 镜像里内置 Tomcat也可以直接构建一个包含应用代码的 Spring Boot 镜像两种方式都绕不开 Tomcat 这个运行时组件。1.3 选 Tomcat 还是选别的容器企业级 Java Web 应用服务器其实有多个选择Tomcat、Jetty、Undertow、WildFly、WebLogic、宝兰德等等。怎么选取决于应用的类型和团队的维护能力。容器规范支持启动速度内存占用管理复杂度典型场景TomcatServlet/JSP中等中等低绝大多数 Java Web 应用JettyServlet快低低嵌入式场景、短生命周期实例UndertowServlet快低低Spring Boot 内嵌、高并发场景WildFly全量 Java EE慢高高需要 EJB、JMS 等完整规范支持WebLogic全量 Java EE慢高高金融、电信等传统大企业核心系统大部分业务应用其实只需要 Servlet 和 JSP 规范这部分 Tomcat 已经完全覆盖了。而且 Tomcat 的资料丰富、社区活跃、排错成本低团队里有经验的人多这些隐性成本在选型时一定要算进去。2. 从下载到跑通Tomcat 安装与核心配置全流程2.1 版本选择与 JDK 版本匹配安装 Tomcat 之前第一件事是确认版本匹配关系很多人踩的坑都出在这里。Tomcat 版本和 JDK 版本有严格的对应关系装错版本可能直接导致应用启动失败。Tomcat 版本支持的 JDK 版本对应的 Servlet 规范Tomcat 10.1.xJDK 11Servlet 6.0Tomcat 10.0.xJDK 8Servlet 5.0Tomcat 9.0.xJDK 8Servlet 4.0Tomcat 8.5.xJDK 7Servlet 3.1这里有一个很多人容易忽略的细节Tomcat 10 开始使用 Jakarta EE 命名空间包名从javax.servlet变成了jakarta.servlet。如果你是从 Tomcat 9 升到 Tomcat 10旧应用里的 import 语句全部要改。这不是配置的问题是代码层面的兼容性改造工作量还不小。所以如果不是有强制要求生产环境用 Tomcat 9 是更稳妥的选择。下载的时候要注意区分 tar.gz 和 zip 包Linux 服务器用 tar.gzWindows 本机调试用 zip。解压之后看一下目录结构bin/ # 启动关闭脚本 conf/ # 核心配置文件 lib/ # Tomcat 自身的 jar 包 logs/ # 日志目录 temp/ # 临时文件 webapps/ # Web 应用部署目录 work/ # JSP 编译后的 class 文件bin 目录里最关键的是catalina.sh它负责 JVM 参数的加载和 Tomcat 的启动。conf目录里最重要的是server.xml、context.xml和tomcat-users.xml三个文件。2.2 环境变量与启动脚本配置安装 Tomcat 前需要先确认 JAVA_HOME 已经正确设置。Tomcat 启动脚本会直接读取这个环境变量来定位 JDK。设置方式export JAVA_HOME/usr/local/jdk-17 export PATH$JAVA_HOME/bin:$PATH生产环境建议把这两个变量写到/etc/profile或者 Tomcat 的bin/setenv.sh里。使用setenv.sh的好处是环境变量配置和 Tomcat 主目录隔离升级 Tomcat 版本时 setenv.sh 可以保留复用。启动之后先验证端口是否正常监听。Tomcat 默认占用的端口号有三个8005关闭 Tomcat 的管理端口只能本机访问8080HTTP 服务端口对外提供服务8009AJP 协议端口用于和 Apache HTTP Server 集成如果 8080 端口被占用可以在server.xml里直接修改。这里有个小经验生产环境最好把 8005 端口改掉默认值太容易被扫描到虽然它只能本机访问但多一层防护总归没坏处。2.3 server.xml 核心参数说明server.xml 是整个 Tomcat 配置的核心文件里面包含了端口、线程池、连接器、虚拟主机等所有关键信息。很多性能问题最后都能在 server.xml 里找到答案。HTTP Connector 部分常见的关键参数Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxThreads200 minSpareThreads10 acceptCount100/maxThreads 控制最大工作线程数这个参数直接决定了 Tomcat 能同时处理多少并发请求。minSpareThreads 是保持空闲的最小线程数等于是常备部队。acceptCount 是当线程池满了之后可以放进等待队列的请求数量超过这个数量的请求会被直接拒绝。这三个参数的配合逻辑是请求来了先由空闲线程处理空闲线程不够了就用 acceptCount 排队队列也满了就拒绝新请求。调优时不是盲目加大 maxThreads而是要根据实际的 QPS 和响应时间曲线来定。一般建议 maxThreads 设置在 200 到 500 之间配合前面 Nginx 的负载均衡做横向扩展单机堆线程数不是好办法线程数过多反而会增加上下文切换的开销。内存参数配置在bin/catalina.sh里通过 JAVA_OPTS 设置。生产环境常用参数JAVA_OPTS-Xms2048m -Xmx2048m -XX:MaxMetaspaceSize512m -XX:UseG1GC-Xms 和 -Xmx 设置为相同的值这样 JVM 启动时就一次性申请完内存运行时不需要再动态扩展性能更稳定。MaxMetaspaceSize 根据应用加载的类数量来定一般 256m 到 512m 足够。注意给 Tomcat 分配的内存不能超过服务器物理内存减去操作系统和其他进程所需的内存。我见过有人把 -Xmx 开到 8g结果服务器总共才 16g 内存数据库和缓存组件全被挤到 swap 里整台机器响应都变慢了。3. 应用部署的三种方式与实际操作3.1 war 包部署的标准流程把应用打包成 war 包丢到 webapps 目录下Tomcat 启动时会自动解压并部署这是最经典的方式。war 包的文件名就是应用的上下文路径比如 AppShop.war 对应的访问路径是http://ip:8080/AppShop。我们原来有一个订单系统用 Maven 打包成 war 包后通过脚本直接拷贝到 webapps 目录下然后调用 Tomcat 的管理接口或者重启 Tomcat 完成部署。这个流程看着简单但生产环境里有一个大坑如果直接将新 war 包覆盖旧的Tomcat 可能不会自动重新加载应用还在跑旧代码。更稳妥的做法是三步走先拷贝新 war 包到某个临时目录然后通过管理脚本把旧的 war 包和对应的解压目录删除再把新 war 包移入 webapps 目录。这样可以避免 Tomcat 文件锁导致的覆盖失败问题。另外要提醒的一个点是解压后的目录名冲突问题。war 包文件名决定了上下文路径所以文件名里不能有空格和特殊字符。我有一次部署一个应用war 包名叫order%finish.war结果 Tomcat 解压后目录名解析异常应用直接 404排查了半天才发现是文件名里的%引起的。3.2 IDE 一键部署与热部署配置开发环境里最常用的方式是在 IDEA 或 Eclipse 里直接配置 Tomcat实现一键部署和热更新。IDEA 里配置 Tomcat 的流程是Run - Edit Configurations - 左上角加号 - Tomcat Server - Local然后在 Application server 里选择 Tomcat 的安装目录在 Deployment 页签里点击加号选择 Artifact把本地 Web 应用的 exploded 结构部署到 Tomcat 上。这里有个关键设置On Update action 选择 Update classes and resourcesOn frame deactivation 选择 Update classes and resources。这样在 IDEA 里按 CtrlF10 重新编译后改动会直接同步到 Tomcat 的运行目录里不需要重启 Tomcat 就能生效。Eclipse 里的操作逻辑是一样的在 Servers 视图里新建 Tomcat 7/8/9 的 Server Runtime然后把动态 Web 项目 add 到服务器列表里。Eclipse 默认会采用.metadata/.plugins/org.eclipse.wst.server.core/tmp0/下的副本目录部署应用修改 JSP 和静态资源时可以自动同步但修改 Java 代码后一般还是要等自动编译完成后让它 reload速度比 IDEA 慢一些。热部署虽然方便但有一个问题要注意频繁的热部署会导致永久代内存泄漏。每次 reload 都会新建类加载器如果旧类加载器没有被正确回收内存会慢慢涨起来。开发环境问题不大但如果你挂着 IDE 连续跑了一周不重启很容易看到 Metaspace OutOfMemoryError。3.3 Nginx 多 Tomcat 实例部署生产环境常见的部署模式是 Nginx 在前面做负载均衡后面挂多个 Tomcat 实例。每个 Tomcat 实例都配置不同的端口和不同的应用。比如在一台 8 核 16G 的服务器上部署两个订单系统实例配置逻辑如下第一个实例端口 8080第二个实例端口 8081。Nginx 配置 upstream 做轮询转发upstream order_backend { server 127.0.0.1:8080 weight5; server 127.0.0.1:8081 weight5; keepalive 32; } server { listen 80; server_name order.example.com; location / { proxy_pass http://order_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }在同一台机器上跑多个 Tomcat需要修改三处端口server.xml 里的 8005 管理端口、8080 HTTP 端口、8009 AJP 端口三者都不能冲突。除了端口还要注意 CPU 亲和性问题大部分情况下不需要刻意绑核但如果你发现两个 Tomcat 实例都跑在同一个 CPU 核心上而其他核心空闲严重可以考虑用 taskset 绑定一下。多实例部署最大的收益不是单机性能翻倍而是故障隔离。当一个实例因为 Full GC 卡住时另一个实例还能正常处理请求Nginx 会自动把流量多分给健康的实例。这一点在高流量场景下非常管用。4. 云原生环境下的 Tomcat 适配与容器化改造4.1 制作精简 Tomcat 镜像的实践容器化的第一步是制作 Tomcat 镜像。网上很多教程直接用基础镜像装 Tomcat到最后镜像体积动辄五六百兆。对于一个云原生架构来说这个体积太大不管是推送到镜像仓库还是拉取到节点都会浪费时间。我常用的方案是在一个编译阶段把依赖准备好然后拷贝到精简的运行时镜像里FROM maven:3.8-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests FROM eclipse-temurin:17-jre RUN mkdir -p /usr/local/tomcat COPY --frombuild /app/target/order-system.war /usr/local/tomcat/webapps/ROOT.war COPY --fromtomcat:9.0-jre17 /usr/local/tomcat/conf /usr/local/tomcat/conf WORKDIR /usr/local/tomcat EXPOSE 8080 CMD [catalina.sh, run]这样做的好处是最终镜像里只有 JRE、Tomcat 运行文件和应用 war 包没有构建工具链。构建完成后镜像体积可以从 500M 压到 200M 左右。注意我在这里把 war 包命名成 ROOT.war这样应用部署后可以直接通过根路径访问不需要带上下文路径。容器里运行 Tomcat 有一个很重要的细节必须以catalina.sh run启动而不能用startup.sh。因为容器要求主进程在前台运行startup.sh会启动一个子进程后自己退出容器直接判定它挂了。4.2 在 Kubernetes 中部署 Tomcat 应用Kubernetes 部署 Tomcat 应用时推荐的模式是把 Tomcat 容器当作无状态组件来管理。也就是说应用产生的 Session 不能保存在本地必须外置到 Redis 或者使用 JWT 这种无状态认证方案。在 Tomcat 里配置 Session 外置需要在 context.xml 里加一个 ManagerContext Manager classNameorg.apache.catalina.session.PersistentManager saveOnRestartfalse Store classNameorg.apache.catalina.session.RedisStore hostredis-service port6379 database0 timeout2000/ /Manager /Context这样配置之后即使某个 Tomcat Pod 被杀死重建用户的 Session 也不会丢失新的 Pod 会从 Redis 里读取 Session 数据。Deployment 的资源配置要特别注意limits 里的内存不能低于 JVM 的 -Xmx。否则可能出现 Pod 被 Limit 限制的内存小于 JVM 已分配内存导致 OOM Kill。resources: requests: memory: 1024Mi cpu: 500m limits: memory: 1536Mi cpu: 1这里有一个真实的教训。我们有一次把 -Xmx 设置成了 2g但 limits 只给了 1536Mi。应用跑到高峰期JVM 堆内存增长超过容器限制Pod 直接 OOMKilled。容器重启了几次流量的错误率一直很高后来把 limits 调到了 2048Mi 才恢复正常。4.3 传统 war 包应用迁移到 Spring Boot 内嵌模式很多存量系统是传统 war 包部署在外置 Tomcat 上的迁移到云原生环境时可以考虑直接改造成 Spring Boot 内嵌模式这样连外部 Tomcat 都不需要了应用就是一个独立的进程生命周期完全由平台管理。Spring Boot 2.7 里如果想用外置 Tomcat 替换成内嵌模式其实改动量很小。只需要做两件事把打包方式从 war 改成 jar然后在启动类上保持 main 函数即可。SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }依赖部分把原来的 spring-boot-starter-web 保留它会自动引入内嵌的 Tomcat。如果你想用 Undertow 替代排除掉 Tomcat 依赖然后引入 Undertow 就行。这种改造成本比很多人想象的低但有一个点要注意原来外置 Tomcat 里通过 context.xml 配置的数据源、JNDI 资源在 Spring Boot 内嵌模式下是失效的。你需要把这些配置全部挪到 application.yml 里用 Spring 的 DataSource 配置重新声明一遍。如果原来的应用大量使用了 JNDI 查找这个迁移工作量会大很多。5. 高频故障排查与性能调优实录5.1 访问 404 的排查步骤Tomcat 启动后访问应用出现 404是所有问题里出现频率最高的。排查顺序建议从外到内先确认 Tomcat 是否启动成功再确认应用部署目录最后确认访问路径。用命令检查启动状态ps -ef | grep tomcat如果进程存在再检查日志目录下的 localhost.logtail -100 /usr/local/tomcat/logs/localhost.$(date %Y-%m-%d).log大部分 404 都出在上下文路径上。Tomcat 的访问路径规则是http://ip:8080/{应用上下文路径}/{应用内部的 URI}。应用上下文路径默认取自 war 包文件名。如果你部署的是 ROOT.war则可以不写上下文路径直接访问根路径。还有一种容易忽略的情况是 webapps 下存在新旧两个版本的应用目录。比如之前部署过 order后来你删掉了 order.war但文件锁导致 order 目录没被删掉Tomcat 又启动了一个新实例部署 application结果应用还是访问旧的目录。手动清空 webapps 目录后重启能解决这类问题。5.2 端口冲突与启动失败启动时报Address already in use是最直观的问题。遇到这种报错先看端口占用情况lsof -i :8080如果是残留的 Java 进程占用了端口直接 kill 掉进程再重启kill -9 $(lsof -t -i:8080)如果是多实例部署出现端口占用就是改漏了端口。记住一行三个端口8005、8080、8009三件套必须全改完再启动。另外有一种情况很隐蔽Tomcat 启动提示端口被占用但实际上占用的进程就是你上一次启动的 Tomcat因为 catalina.sh 里没有正确设置 CATALINA_PID导致旧进程还存在新进程就报端口被占用。解决办法是在 setenv.sh 里加上CATALINA_PID$CATALINA_BASE/tomcat.pid5.3 高并发场景的参数调优并发量上来了Tomcat 的表现和参数设置关系很大。我遇到过很多次应用 CPU 不高、数据库压力也不大但接口超时严重的情况。最后发现是 Tomcat 的线程池设置得过小请求全在 acceptCount 队列里排队等待。对于常规的 HTTP 请求处理调优重点是线程池。推荐的调优方向Connector port8080 protocolHTTP/1.1 maxThreads400 minSpareThreads50 acceptCount200 connectionTimeout10000 enableLookupsfalse compressionon compressionMinSize2048 compressibleMimeTypetext/html,text/xml,text/plain,text/css,application/json/enableLookups 设为 false关闭 DNS 反向解析这个操作可以减少每个请求的解析开销。compression 开启后静态资源体积能压缩 60% 以上对减小带宽压力帮助很大。线程池调优的依据是压测数据。没有压测就不要乱调。你可以用 JMeter 设置半小时的压力测试逐渐增加并发数观察响应时间的拐点这个拐点对应的线程数就是比较合适的值。还有一个 JVM 层面的建议使用 G1 垃圾回收器它可以避免 CMS 时代常见的 Full GC 停顿问题。JDK 11 以上默认就是 G1如果你是 JDK 8需要显式指定JAVA_OPTS-Xms2048m -Xmx2048m -XX:UseG1GC -XX:MaxGCPauseMillis2005.4 安全加固的几个必要操作Tomcat 作为公网暴露的组件安全问题不能忽视。除了前面提到的修改 8005 管理端口还有几个必要的安全操作第一删除 webapps 目录下默认的 ROOT、examples、docs、manager、host-manager 这几个目录。它们自带的内容没有业务价值还可能被利用来探测环境信息。第二tomcat-users.xml 里的管理账号在生产环境不配置或使用强密码。manager 和 host-manager 是管理接口暴露在公网上等于把服务器钥匙递出去了。第三隐藏版本信息。Server 头默认会返回Apache-Coyote/1.1攻击者可以用它判断 Tomcat 版本然后针对已知漏洞进行攻击。隐藏方式是在 Connector 中配置 server 属性为自定义值。Connector serverApache port8080 protocolHTTP/1.1/第四限制 AJP 端口访问。Tomcat 的 AJP 协议如果不对外提供 Apache 集成建议直接注释掉 8009 端口对应的 Connector 配置减少攻击面。历史上出现过 AJP 协议的相关漏洞这是非常值得注意的一个点。6. 不同规模场景下的部署方案选型6.1 单机开发环境个人开发或者小团队联调用最简单的方案一个 Tomcat 实例直接把应用 war 包放到 webapps 下或者走 IDE 内部部署。这种情况下不需要过多调优内存按默认或者调整到 1G 左右就行。6.2 中小型生产集群日活几千到几万的应用推荐 Nginx 双 Tomcat 实例部署。两个 Tomcat 分在不同端口放置在同一台机器或者两台机器上都可以前面用 Nginx 做负载均衡。Session 需要外置到 Redis避免一台 Tomcat 挂掉后用户 Session 全部丢失。6.3 大型云原生集群日活几十万的场景一般不会手动管理 Tomcat 实例了。应用容器化部署在 Kubernetes 集群里通过 HPA 根据 CPU 使用率或请求量自动扩缩容。Tomcat 本身当成无状态运行环境来用JVM 参数放在镜像构建阶段确定运行期通过环境变量动态调整。这种模式下最核心的关注点是启动速度和内存控制。镜像启动时间控制在 30 秒以内内存按照容器 Limit 的 70% 设置 -Xmx预留出 JVM 元空间和非堆内存的余量。如果启动太慢可以考虑后端使用 Spring Boot 编译期 AOT 技术来进行优化。选型没有绝对的正确答案核心是根据团队的技术能力和业务场景做取舍。我的经验是能用 Tomcat 标准部署解决的问题不要引入太复杂的架构云原生改造的目的是降本增效不是为了技术炫技。我在实际运维过程中对 Tomcat 最深的一点体会是它本身不复杂但几乎所有 Java Web 应用的问题最终都会反映到它身上。学会看 Tomcat 的日志、理解线程池的工作方式、搞清楚 JVM 内存模型这三个基本功可以解决 80% 以上的故障。在日常生活中用到的电脑杀毒软件早年很多桌面应用其实就走的是本地 HTTP 服务端模式这和 Tomcat 的原理是相通的理解了 Web 容器你回头看很多东西的思路都会清晰很多。最后分享一个小工具使用经验排查线上问题时用jstack抓线程快照往往比翻日志更快。比如你看到 Tomcat 线程全部处于 BLOCKED 状态说明有锁竞争问题。线程快照输出重定向到文件后分析线程状态分布能快速定位有问题的业务代码。这个技巧在我排查接口超时问题时帮过大忙推荐大家多去试试。
返回列表