ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Tomcat 9.0.56部署实战:从环境配置到JVM调优与避坑指南

Tomcat 9.0.56部署实战:从环境配置到JVM调优与避坑指南 简介Apache Tomcat 9.0.56 的 ZIP 包是面向 Java Web 应用开发与部署场景的官方发行版适合需要在 Windows、Linux 或 macOS 上快速搭建 Servlet/JSP 服务器的开发者与运维人员。包内含完整目录结构包括 bin 启动脚本、conf 核心配置、lib 运行库、webapps 默认部署目录等可满足部署应用、调试连接器、配置 SSL 与性能调优等常见需求。资源共 639 个文件类型涵盖 html、class、java、jsp、jar、xml、properties、sh、bat 等既有源码与编译产物也有配置文件和跨平台启动脚本整体压缩包约 11.57MB体积精简便于下载与反复解压使用。目前已有 143 人学习下载。通过解析该压缩包读者可以直观理解 Tomcat 的目录职责、配置文件作用以及应用部署流程可作为入门学习、环境搭建和日常维护的参考资料。1. 为什么偏偏是 apache-tomcat-9.0.56.zip一个版本号背后的生产环境真相拿到apache-tomcat-9.0.56.zip这个压缩包很多人的第一反应是“随便找个 Tomcat 下载不就完了”。但如果你在生产环境里维护过 Java Web 应用就会明白Tomcat 的版本号不是随便挑的尤其是 9.0.x 这条线每个小版本都对应着不同的 Servlet 规范实现、不同的默认行为甚至不同的漏洞修复状态。9.0.56 这个版本刚好卡在一个非常微妙的时间点上——它修复了之前几个版本里被反复讨论的若干问题同时又没有引入后续版本里那些更激进的行为变更。对于很多跑着老项目、不敢随便升级大版本的技术团队来说这个版本往往是“再撑一年”的务实选择。这篇文章要讲的东西很直接这个 zip 包下载下来之后怎么从零开始部署、配置、调优以及最重要的——你会踩到哪些坑。Tomcat 的难点从来不在启动本身而在于 JVM 参数、连接器配置、应用部署方式、日志排查这几件事。我会把每个环节的命令、参数和排错思路都拆开讲清楚保证新手照着做能跑起来老手也能看到一些平时容易忽略的边界情况。2. 从 zip 到可运行实例目录结构、环境变量与首次启动的七个细节2.1 解压后的目录结构哪些文件夹能动哪些别碰拿到 zip 后先解压。Windows 上直接右键解压Linux 上用unzip apache-tomcat-9.0.56.zip。解压完你会看到一个以apache-tomcat-9.0.56命名的目录里面大概有这些子目录bin、conf、lib、logs、temp、webapps、work。很多人第一次接触 Tomcat 时会困惑为什么没有src目录也没有README里说的“源码包”——因为这个 zip 是二进制发行版不是源码包你要部署的是编译好的 Servlet 容器不是自己去编译 Tomcat。这里有一个很重要的认知bin目录下是启动和关闭脚本conf目录下是全部配置文件lib目录下是 Tomcat 自身运行依赖的 jar 包而webapps目录才是你放置 Web 应用的地方。logs和temp是运行时生成的work目录是 JSP 编译后的 class 文件缓存。我的习惯是conf和webapps做备份logs挂到独立磁盘分区因为日志增长会拖垮系统盘。2.2 必须先配好的两个环境变量在启动之前必须确认JAVA_HOME已经指向一个可用的 JDK。Tomcat 9.0.x 要求 Java 8 及以上版本但如果你用的是 Java 11 或更高版本需要注意模块化系统带来的反射访问限制。我一般会在/etc/profile.d/tomcat.sh里写export JAVA_HOME/usr/local/jdk-11.0.21 export CATALINA_HOME/opt/apache-tomcat-9.0.56 export CATALINA_BASE$CATALINA_HOME export PATH$JAVA_HOME/bin:$CATALINA_HOME/bin:$PATH这段配置的关键在于CATALINA_HOME和CATALINA_BASE。绝大多数单实例部署下两者指向同一个目录但如果你在同一台机器上跑多个 Tomcat 实例就必须让CATALINA_BASE指向各自的配置目录否则多个实例会互相覆盖logs和work。CATALINA_HOME是程序文件所在位置CATALINA_BASE是实例运行时使用的配置和临时文件目录这个区别很多人搞混后面排错时会坑到自己。2.3 首次启动startup.sh 与 catalina.sh run 的区别启动方式有两类startup.sh后台启动catalina.sh run前台启动。第一次部署我强烈建议用前台启动因为你能直接看到控制台日志任何报错都会第一时间刷出来。命令如下cd $CATALINA_HOME/bin ./catalina.sh run如果一切正常你会看到类似 “Server startup in [xxxx] milliseconds” 的日志。此时在浏览器访问http://服务器IP:8080能看到默认首页。如果看不到先别急着查防火墙先确认启动日志里有没有报错。常见的问题是端口被占用或者JAVA_HOME没配好导致脚本直接退出。2.4 修改端口server.xml 里的三个连接器Tomcat 默认监听 8080 端口但有三个端口需要关注全在conf/server.xml里。第一个是 HTTP 连接器Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 /第二个是 AJP 连接器默认注释掉了。第三个是8005端口那是 shutdown 监听端口用来接收关闭指令。如果你改了 HTTP 端口通常也要改 shutdown 端口避免同一个机器上多个实例冲突。这个文件改完必须重启才生效而且要注意 XML 标签别写错Tomcat 启动时会做严格解析一个标签没闭合整个实例都起不来。2.5 验证启动成功的三个方法启动成功的标志不是进程存在而是端口真正在监听。我一般按顺序做三件事ps -ef | grep tomcat netstat -tlnp | grep 8080 curl -s http://127.0.0.1:8080 | head -20ps命令看进程是否还活着netstat看端口是否真的在监听curl看 HTTP 响应是否正常。有些时候进程在但端口没起来说明应用启动到一半失败了有些时候端口起来了但curl超时说明应用线程阻塞或连接器出了问题。这三种情况分别对应不同的排查路径不要混为一谈。2.6 关闭方式shutdown.sh 的隐藏风险关闭 Tomcat 的脚本是shutdown.sh它通过向8005端口发送一个特定字符串来触发关闭。这里有一个很隐蔽的坑如果 JVM 卡死或线程死锁shutdown.sh很多时候不生效因为 Tomcat 的关闭钩子需要 JVM 正常运行才能执行。此时只能kill -9但这可能带来数据丢失或应用状态不一致的问题。我的习惯是先用shutdown.sh等 30 秒如果进程还活着再kill -9并且写进运维手册里。2.7 开机自启与以非 root 用户运行生产中绝对不要用 root 跑 Tomcat这是安全红线。常见做法是创建一个专门的系统用户然后通过 systemd 管理useradd -r -s /sbin/nologin tomcat chown -R tomcat:tomcat /opt/apache-tomcat-9.0.56systemd 服务单元文件里的关键参数是Usertomcat和ExecStart/opt/apache-tomcat-9.0.56/bin/catalina.sh run。用run而不用startup是因为 systemd 需要 Tomcat 在前台运行才能正确管理进程生命周期。这里我踩过一次坑用startup.sh启动systemctl status 显示 inactive因为 Tomcat 启动后把进程 fork 出去了systemd 认为服务已经退出。3. 部署一个 Web 应用war 包与目录热部署的完整流程3.1 war 包放哪webapps 下的自动部署机制Tomcat 默认开启自动部署只要把 war 包拷贝到webapps目录下它会自动解压并启动应用。这个过程看着简单但实际上有一个非常容易忽视的点war 包的文件名决定了应用访问路径。比如myapp.war会被部署到/myapp这个上下文路径下访问地址就是http://IP:8080/myapp。如果你希望应用直接跑在根路径上需要把 war 包命名为ROOT.war。这个机制对已有线上环境很不友好。因为自动部署是异步的war 包拷贝进去到应用真正可用可能间隔几秒钟而且如果 war 包已经存在同名目录Tomcat 会先展开新包再覆盖期间如果请求打进来会出现间歇性 404。我一般会先停止 Tomcat清空旧的解压目录和 work 缓存再放新包启动。虽然牺牲了一点自动化但换来的是确定性。3.2 解压目录与可重部署性为什么删 work 缓存能治诡异问题JSP 文件在第一次被访问时会编译成 class 文件缓存在work/Catalina/localhost/下。如果你更新了 JSP 文件但浏览器看到的还是旧页面十有八九是 work 缓存没清理。更诡异的情况是应用里的静态资源改了但页面没变这时候除了清浏览器缓存也要清 Tomcat 的 work 目录。这个目录删掉完全不影响运行Tomcat 会在下次访问时重新编译。3.3 上下文路径配置server.xml 的 Context 标签与 META-INF/context.xml虽然自动部署很方便但有些场景需要手动指定上下文路径或其他参数比如关联到外部目录下的应用文件。常见做法是在conf/Catalina/localhost/下放一个 XML 文件文件名就是上下文路径Context docBase/data/apps/myapp reloadablefalse /这段配置的意思是把/data/apps/myapp这个目录映射到http://IP:8080/myapp这个访问路径。docBase指向真实路径reloadablefalse表示不开启自动重载。这里我要多说一句reloadable这个参数是个双刃剑开发环境设成true很爽改个 class 文件就自动重载生产环境设成true就是灾难因为每次更新文件都会触发一次类加载器重建如果并发高可能引起内存溢出或短暂不可用。3.4 部署时如何看日志catalina.out 与 localhost.log 的分工Tomcat 有多个日志文件新手经常搞混。catalina.out是控制台输出的汇集包括启动日志、JVM 的 System.out 输出localhost.log是 Tomcat 内部组件连接器、容器的日志localhost_access_log.txt是访问日志记录每次 HTTP 请求。还有一个容易被忽略的manager.log只有当你开启 Manager 应用时才会产生。排错的第一原则先看catalina.out但不要只看最后几行要搜索Exception、ERROR、Caused by这些关键词。很多部署失败的真正原因在堆栈的Caused by部分而不是在最开始的Exception行。某次我部署一个应用时catalina.out 只显示LifecycleException看起来毫无头绪搜到Caused by才发现是数据库驱动 jar 没放进lib目录。3.5 依赖 jar 放哪lib 目录与 WEB-INF/lib 的边界应用依赖的 jar 包可以放在两个地方Tomcat 的lib目录或者应用自身的WEB-INF/lib目录。放lib目录的位置是全局共享多个应用可以复用放WEB-INF/lib只对当前应用生效。这个选择有讲究如果两个应用需要不同版本的同一个库就不能放全局lib否则后加载的类会覆盖先加载的导致诡异的 NoSuchMethodError。我的原则是除非确定所有应用都能共用同一个版本否则一律打进 WAR 包里的WEB-INF/lib可以避免太多类加载器层面的玄学问题。3.6 把部署过程写成脚本一个可靠的发布流程手动操作容易出错我通常会用脚本把部署流程固化下来。一个典型的上线脚本逻辑是备份旧包 → 停服务 → 清缓存 → 放新包 → 启服务 → 健康检查。注意脚本里每个步骤之间都要有明确的结果判断不能盲目往下走。4. JVM 参数与连接器调优让 Tomcat 在压力下不拖后腿4.1 内存参数catalina.sh 里的 JAVA_OPTS 设置Tomcat 默认的 JVM 内存很小通常只有 256MB 或 512MB。一旦你的应用稍微有点规模就会遇到java.lang.OutOfMemoryError: Java heap space。这个问题几乎每个做 Java Web 的人都会碰上但千万不要一上来就无脑调大内存。正确的做法是在bin/catalina.sh或bin/setenv.sh里设置JAVA_OPTS-Xms2048m -Xmx2048m -XX:MaxMetaspaceSize512m -XX:UseG1GC-Xms和-Xmx分别表示堆内存的初始值和最大值生产环境我通常会把两者设成一样避免 JVM 在运行中动态调整堆大小带来的性能抖动。-XX:MaxMetaspaceSize是元空间上限很多老项目跑着跑着就报Metaspace溢出就是没设这个值。G1 是 JDK 11 及以后默认的垃圾回收器适合大多数 Web 应用场景。4.2 连接器参数maxThreads、acceptCount 与 connectionTimeout 的三角关系HTTP 连接器有几个关键参数直接影响并发能力。maxThreads是处理请求的最大线程数acceptCount是等待队列的长度connectionTimeout是建立连接后的超时时间。这三者的关系很多人理解有误不是线程数越大越好也不是队列越长越好。一个典型的配置Connector port8080 protocolHTTP/1.1 maxThreads400 acceptCount200 connectionTimeout20000 minSpareThreads50 /这里maxThreads400表示最多同时处理 400 个请求acceptCount200表示当 400 个线程全忙时还能排队的请求数量是 200超过这个数量的请求会被直接拒绝。minSpareThreads50表示即使空闲也保持至少 50 个线程待命避免短时间内大量请求涌入时频繁创建线程。这个参数要怎么设没有标准答案取决于你的应用平均响应时间和目标 QPS。但有一个血泪经验maxThreads不要超过 CPU 核心数的 10 到 20 倍因为线程切换本身的代价会吃掉性能。如果 CPU 只有 4 核设maxThreads1000只会让系统在大量请求时卡死在线程调度上而不是真正在处理业务。4.3 慢请求排查Tomcat 卡住时先看线程转储Tomcat 卡死、请求全部超时的时候最直接的排查手段是抓线程转储。用jstack命令jstack -l tomcat_pid /tmp/thread_dump_$(date %Y%m%d_%H%M%S).txt这个命令会输出 JVM 里所有线程的当前状态和调用栈。抓下来的文件里重点看处于RUNNABLE状态的线程是否大量集中在某个类的方法上或者是否有大量线程在java.net.SocketInputStream.socketRead0这种地方那通常说明线程都阻塞在等待外部资源数据库连接池、外部 HTTP 调用而不是 Tomcat 本身的问题。4.4 线程池参数什么时候改executor 与 Connector 的分离配置Tomcat 支持用一个独立的线程池给多个连接器使用配置在server.xml里Executor nametomcatThreadPool namePrefixcatalina-exec- maxThreads400 minSpareThreads50 maxIdleTime60000 /然后在 Connector 里用executortomcatThreadPool替代直接设置maxThreads。这个做法的价值在于如果你的 Tomcat 同时开启了 HTTP 和 AJP 连接器它们可以共享同一个线程池避免线程资源被某个连接器独占。4.5 一个典型的调优检查清单我的习惯是部署完成后用压测工具常见的就是 Apache Bench 或 wrk打一下观察ab -n 10000 -c 200 http://127.0.0.1:8080/的结果。重点关注Time per request和Failed requests这两个指标。如果失败率不为 0通常是连接器参数不合理或者应用本身有瓶颈。压测一定要在测试环境做不要直接对生产环境发起任何形式的压测这是红线。5. 避坑指南Tomcat 9.0.56 最容易翻车的五个场景5.1 静态资源 404 但应用能正常访问现象访问http://IP:8080/myapp能打开页面但页面里引用的 JS、CSS 文件全部 404。原因这个坑通常出在应用本身的静态资源路径上或者请求被某个过滤器拦截了。90% 的情况是应用的 web.xml 里配置了过滤器映射把静态资源请求也拦下来做了权限校验导致返回 404 而不是正常返回文件。解决在应用的web.xml里为静态资源扩展名配置白名单或者修改过滤器逻辑对/static/、/resources/这类路径直接放行。还有一种情况是 Spring MVC 的mvc:default-servlet-handler /没配导致请求压根没转发到默认 Servlet。5.2 启动时提示端口被占用但 netstat 查不到现象./startup.sh执行后catalina.out里报java.net.BindException: Address already in use端口是 8080。执行netstat -tlnp | grep 8080却看不到任何占用进程。原因这是一个很隐蔽的坑。Tomcat 用的是一个 IPv6 的::地址绑定端口而netstat默认只显示 IPv4 的监听。或者端口被其他用户比如 root的进程占用了当前用户没有权限查看。解决用netstat -tlnp | grep :8080加不加引号都是一样关键是加上-p参数要 root 权限才能看到完整进程。更可靠的方案是用ss -tlnp | grep 8080这个命令在多数新系统上更准确。如果还是看不到试试lsof -i :8080。5.3 修改了 server.xml 却看不到任何变化现象改完server.xml里的端口重启 Tomcat 后访问的端口还是旧的。原因最常见的原因是CATALINA_BASE指向了别的目录你改的文件不是实际加载的文件。很多发行版会单独创建/var/lib/tomcat/conf之类的目录作为CATALINA_BASE与解压出来的conf目录不是同一个地方。解决先执行./catalina.sh configtest看看加载的是哪个配置文件路径或者直接在脚本里加一句echo $CATALINA_BASE打印出来确认。另外注意server.xml的 XML 格式必须严格合法改完先用xmllint或在线校验工具检查一遍。5.4 应用部署后 ClassNotFoundException但 jar 明明在 lib 目录里现象应用启动后某个类报ClassNotFoundException你确认了对应 jar 已经放进WEB-INF/lib或者 Tomcat 的lib目录但仍然报错。原因这个坑大概率是 jar 包冲突或者说 jar 包虽然存在但被其他 jar 里的同名类抢先加载了而那个类又缺少某个依赖。另一种常见情况是jar 包的文件名以奇怪的字符结尾比如带空格或中文Tomcat 的类加载器解析不了。解决用jar tf查看冲突 jar 里的类路径确认是不是存在同名类。如果确实有需要把其中一个 jar 移除或改名。另外可以用-verbose:classJVM 参数启动 Tomcat日志里会打印每个类的实际来源 jar这是定位类加载问题最直接的手段。5.5 启动了多个实例日志互相串现象同一台机器跑了两个 Tomcat 实例但logs/catalina.out里出现了两个应用的日志。原因两个实例的CATALINA_BASE指向了同一个目录导致日志文件、work 目录、temp 目录全被共享了。这在复制目录部署新实例时特别容易发生比如直接cp -r了一份解压目录但忘了改CATALINA_BASE或server.xml里的日志路径。解决严格分离每个实例的CATALINA_BASE。确保每个实例的conf/logging.properties里catalinaBase指向独立目录同时修改server.xml里所有端口避免端口冲突。这一步做了日志就不会串。6. 从解压包到生产基线一套可以固化的安装与验收流程新手把一个 Tomcat 跑起来往往以为事情就结束了。但真实的交付标准不是“能启动”而是“可运维、可监控、可重建”。我习惯把整个安装过程固化成一套脚本和检查清单以下是我每次部署apache-tomcat-9.0.56.zip都会执行的完整流程可以作为你的生产基线。6.1 安装与初始化脚本的核心片段# 解压并移动到指定目录 unzip apache-tomcat-9.0.56.zip -d /opt mv /opt/apache-tomcat-9.0.56 /opt/tomcat # 创建系统用户 useradd -r -s /sbin/nologin -d /opt/tomcat tomcat chown -R tomcat:tomcat /opt/tomcat # 配置环境变量 cat /etc/profile.d/tomcat.sh EOF export JAVA_HOME/usr/local/jdk-11.0.21 export CATALINA_HOME/opt/tomcat export CATALINA_BASE/opt/tomcat EOF source /etc/profile.d/tomcat.sh这段脚本做完之后Tomcat 已经具备基本的运行条件。但注意这个阶段还没有做任何调优如果直接对外提供服务性能和安全都不够。下一步是修改server.xml和 JVM 参数然后验证。6.2 验证清单五项测试全部通过才算交付部署完成后我会按顺序跑一遍验证任何一项失败都视为交付不通过。第一个测试是启动测试执行./catalina.sh run日志里出现Server startup in即通过。第二个测试是端口测试用ss -tlnp | grep 8080确认端口监听。第三个测试是 HTTP 响应测试curl -I http://127.0.0.1:8080/返回 200 或 302。第四个测试是关闭测试./shutdown.sh后进程能在 30 秒内退出。第五个测试是重启测试连续执行三次启停确认没有端口残留或临时文件累积问题。这五项测试看起来简单但很多环境问题就藏在这些细微的地方。某次交付一个内部系统时第一次启动正常重启后却一直报错原因是内存参数里-Xmx设得太大超过了服务器可用内存第一次启动侥幸成功第二次因为系统其他进程占了内存就起不来了。所以第六个隐藏测试项是重启后观察free -m的内存变化确认 Tomcat 没有把系统内存吃到极限。6.3 日志轮转一种值得养成的运维习惯catalina.out这个文件默认不会自动做大小切分时间久了能涨到几个 GB占用磁盘空间不说查日志也会卡到不行。Linux 上可以用logrotate来处理/opt/tomcat/logs/catalina.out { daily rotate 30 compress missingok copytruncate }这里的关键配置是copytruncate先复制日志文件再清空原文件好处是不需要重启 Tomcat。如果不用这个选项logrotate会删掉原文件然后创建一个新文件但是 Tomcat 的日志句柄还指向被删除的旧文件导致新的日志继续写入一个不存在的 inode磁盘空间不会释放。这个坑我建议每个人提前埋好避雷针。6.4 最后收尾一个值得保留的自我检查习惯每次部署完我会把当时用到的 Java 版本、Tomcat 版本、JVM 参数、数据库驱动版本全部记录在一个文档里下次排错时先看这份记录能省掉大量重复排查的时间。特别是针对9.0.56这个版本如果后续要升级这份记录能帮你判断新版本会不会引入行为变更。运维这件事靠的不是记忆力是沉淀下来的习惯。希望这些内容能帮到你哪怕只是帮你少踩一个坑也算值得。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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