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

Tomcat 8.5.100 tar.gz 离线部署、调优与避坑实战

Tomcat 8.5.100 tar.gz 离线部署、调优与避坑实战 ★ FEATURED ARTICLE
简介面向Java Web开发者与运维人员的Apache Tomcat 8.5.100 Linux部署包对应Java EE 8规范下的Servlet 4.0、JSP 2.3与EL 3.0运行环境可用于搭建本地测试服务、部署WAR应用及学习容器配置。压缩包解压后共623个文件以html管理页面与文档、class与java源码示例、jsp页面、jar依赖及xml、properties配置为主另有sh、bat启停脚本和少量图片、字体资源整体约10.38MB目录保持官方发行版结构便于直接对照conf、webapps、logs等模块查阅。已有2035人学习下载。读者可据此完成Tomcat安装、server.xml端口与连接器调整、Manager用户权限启用、JVM与线程池调优并结合日志文件和JMX工具排查启动与运行故障适合作为Java Web环境搭建与Tomcat机制学习的完整参考包。1. 搜 tomcat apache 找包的人多半不是在起新项目会去搜 tomcat apache 下载 tar.gz 的人十有八九不是在起新项目。新项目一套 Spring Boot 就带内嵌容器了没人手动解压二进制包。真实场景是一套跑了七八年的 Java Web 系统web.xml里写着 servlet 3.1代码里import javax.servlet.*往上升 Tomcat 9 勉强能动升到 Tomcat 10 直接全部报错因为包名全换成了jakarta.servlet。这种时候 8.5 线就是最省事的落点而apache-tomcat-8.5.100.tar.gz正是这条线上靠后的二进制包——解压即用不写注册表不往系统目录里塞东西一个 JDK 加一个目录就能跑。它解决的问题不是“怎么装一个 Web 服务器”而是“怎么在一台不能随便装软件、不能联网拉依赖的机器上把一个 javax 时代的 war 稳稳跑起来”。适合三类人维护存量项目的后端、要在内网离线部署的运维、需要一个干净容器做实验的教学环境。下面按我实际拆包的顺序讲从目录职责到server.xml再到翻车点。2. 拆包先看目录tar.gz 里七个目录各自管什么tar.gz 这种包最大的好处是“透明”里面有什么、改了哪里一眼能看见最大的坏处也是这个——所有东西都摊在你面前改错一个文件排查起来比装 rpm 还费劲。所以第一步不是急着startup.sh而是把目录职责摸清楚知道哪些能动、哪些动了升级就丢。2.1 解压位置、权限与目录职责解压位置有讲究。放/root下最省事但一旦用非 root 用户跑 systemd 服务读不到 jar 也写不了日志报错还特别隐晦。我一般统一放/opt并且留一个不带版本号的软链升级时只改软链服务脚本和 Nginx 配置一个字都不用动。# 1) 解压到 /opt--no-same-owner 防止包内 uid 覆盖本机用户 sudo mkdir -p /opt sudo tar -zxvf apache-tomcat-8.5.100.tar.gz -C /opt --no-same-owner # 2) 建稳定软链后续所有脚本、systemd、Nginx upstream 都写 /opt/tomcat sudo ln -sfn /opt/apache-tomcat-8.5.100 /opt/tomcat # 3) 交给专用用户logs/work/temp 必须可写否则启动时静默失败 sudo useradd -r -s /sbin/nologin tomcat sudo chown -R tomcat:tomcat /opt/apache-tomcat-8.5.100--no-same-owner这个参数是容易忽略的压缩包里的文件属主信息会被原样还原如果打包机器上用的是 uid 1000而你的机器上 uid 1000 是另一个业务账号权限就会很尴尬。软链用-sfn-n表示把软链本身当文件处理避免在软链已存在时被解引用到目标目录里那样会建出一个/opt/apache-tomcat-8.5.100/apache-tomcat-8.5.100的嵌套目录——这种问题不会报错只会在你几个月后清理磁盘时才发现。目录职责能不能动bincatalina.sh、startup.sh、shutdown.sh、setclasspath.sh不改升级会覆盖confserver.xml、web.xml、context.xml、tomcat-users.xml、logging.properties主要改这里lib容器自身与所有应用共享的 jar只放 JDBC 驱动这类公共包webapps默认部署目录可以整个换到外部路径logscatalina.out、localhost_access_log.*、localhost.*.log必须配轮转temp临时文件文件上传组件会用建议单独挂盘workJSP 编译出来的.java/.class可以整个删掉重启自动重建lib那一行要特别说一句。很多人图省事把业务 jar 甚至servlet-api.jar往里扔结果就是类加载冲突容器自己的类加载器优先级和你以为的不一样ClassCastException、NoSuchMethodError这类问题会以完全不相干的形态冒出来排查成本极高。work目录倒是可以放心删JSP 改坏了、页面报奇怪的编译错误停服删work再起十次有八次能好——这是最便宜的一招“后悔药”。2.2 JAVA_HOME 与 CATALINA_HOME 的边界catalina.sh启动时会先找JAVA_HOME找不到再退回JRE_HOME都没有才去PATH里捞java。这个回退链看着贴心实际是坑PATH里的java很可能是一个 JRE跑是能跑但一旦涉及需要完整 JDK 能力的场景比如某些编译逻辑就会缺东西。所以我的习惯是永远显式指定JAVA_HOME不让脚本自己猜。JAVA_HOME/usr/local/jdk1.8.0_xxx CATALINA_HOME/opt/tomcat # 安装目录只读 CATALINA_BASE/opt/tomcat # 运行时实例目录可写CATALINA_HOME和CATALINA_BASE单实例时是同一个值多实例部署时才拆开bin、lib共享一份每个实例各自有conf、logs、webapps、work、temp。这个设计的好处是升级一次容器版本五个实例全跟着升坏处是某个实例的setenv.sh写错了只有它起不来很容易被当成偶发问题。真正靠谱的做法是把环境变量写进bin/setenv.sh。catalina.sh里有这么一段逻辑只要$CATALINA_BASE/bin/setenv.sh存在就source它。这个口子是官方预留的升级不会覆盖也不会像改catalina.sh那样下次换包全丢。# /opt/tomcat/bin/setenv.sh —— 注意这个文件默认不存在要自己建且必须有可执行权限 CATALINA_PID/opt/tomcat/tomcat.pid # 有了 pid 文件shutdown.sh 才能精准停不会误杀别的 java 进程 JAVA_HOME/usr/local/jdk1.8.0_xxx CATALINA_OPTS-server -Xms2g -Xmx2gCATALINA_PID这一行值得单独强调。没有它的时候shutdown.sh是发信号给“所有匹配的 java 进程”机器上一旦跑了别的 Java 服务停 Tomcat 顺手把别人也停了这种事我见过不止一次。2.3 第一次启动与三项验证启动之前先跑version.sh它不启容器只打印容器版本和 JVM 版本能第一时间确认JAVA_HOME指对了没有。/opt/tomcat/bin/version.sh # 看 Server version 与 JVM Version 是否匹配预期 /opt/tomcat/bin/startup.sh tail -n 50 /opt/tomcat/logs/catalina.out # 重点找 SEVERE 和 Server startup in ... ms ss -lntp | grep -E 8080|8005|8009 # 业务端口、shutdown 端口、AJP 端口 curl -I http://127.0.0.1:8080/ # 期望 200返回体是默认首页version.sh输出的JVM Version是判断环境是否干净的第一道关。ss那一行则决定后面安全配置的方向8005是 shutdown 端口8009是 AJP 端口这两个在生产环境上出现说明默认配置一个字都没改。curl -I返回的响应头里会带Server: Apache-Coyote/1.1之类的信息等于把容器类型和大致版本告诉了外面这也需要处理。catalina.out要注意它是标准输出重定向的产物不会自动切分。跑上几个月几个 G 很常见而且这个文件写满磁盘会把整个服务拖死。要么配 logrotate 按天切要么在logging.properties里把 ConsoleHandler 关掉只留 FileHandler。这是后话但一开始就得知道。3.server.xml的 Connector端口、线程、编码三处必改conf/server.xml是整个容器最该动、也最容易改坏的文件。它用的是 XML但没有任何 schema 校验兜底标签名拼错、属性名写错容器照样启动只是那个配置静默失效。我见过最典型的例子有人把maxThreads写成了maxThread压测时线程数一直停在 200查了两天才发现。3.1 Connector 关键参数表8.5 的 Connector 默认走 NIOprotocolHTTP/1.1会被自动映射到Http11NioProtocol。下面这几个参数是实际调优时绕不开的参数8.5 默认值什么时候要改port8080前面挂 Nginx 就改成 8081 并只监听本机address不限制必须显式写127.0.0.1或内网 IPmaxThreads200按 QPS×平均 RT 估算一般不超过 500minSpareThreads10低峰保留量调大可减少突发时的线程创建抖动acceptCount100全忙时的等待队列长度超了直接拒连maxConnections10000NIO与线程数不是一回事见下文说明connectionTimeout60000毫秒内网可降到 20000防止慢连接长期占位URIEncodingUTF-8从 7 迁过来的旧配置里若写着 ISO-8859-1要删掉compressionoff文本类接口开on压缩比通常 3~5 倍compressibleMimeType一串默认 MIME记得补上application/jsonmaxThreads和maxConnections的区别值得单独说清楚因为这俩经常被混为一谈。NIO 模式下一条连接在空闲等待数据时是不占用工作线程的由 Poller 负责监听事件只有真正有请求要处理才交给http-nio-8081-exec-*线程。所以maxConnections远大于maxThreads是完全正常的前者限制的是“同时挂多少个连接”后者限制的是“同时有多少个请求在处理”。把maxThreads调到 2000 并不会让系统变快只会让线程上下文切换把 CPU 吃满。connectionTimeout的默认 60 秒在公网场景下是合理的但纯内网、前面还有 Nginx 的情况下长连接由 Nginx 维持Tomcat 这一侧的 60 秒反而会让慢客户端占住连接调到 20 秒更合适。3.2 关掉 8005 与 AJP改监听地址下面这段是我在几乎所有部署里都会用的骨架重点是三处Server port-1、Connector 加address、AJP 整段注释掉。!-- conf/server.xml -- !-- shutdown 端口设为 -1 表示禁用只能靠 kill 或 systemd 停比留一个裸的 8005 安全 -- Server port-1 shutdownSHUTDOWN !-- 业务 Connector只监听本机外部流量一律走 Nginx -- Connector port8081 protocolorg.apache.coyote.http11.Http11NioProtocol address127.0.0.1 maxThreads200 minSpareThreads20 acceptCount100 maxConnections10000 connectionTimeout20000 URIEncodingUTF-8 compressionon compressionMinSize2048 compressibleMimeTypetext/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json relaxedQueryChars[]|{} / !-- AJP不用就整段注释掉8009 暴露在网络上属于历史遗留的高危项 -- !-- Connector port8009 protocolAJP/1.3 redirectPort8443 / -- /ServerServer port-1改动很小但收益明确默认的 8005 端口配shutdownSHUTDOWN任何人只要能连上这个端口并发一个字符串容器就停了。内网也不该留这个口子。关掉之后停服就只能靠shutdown.sh依赖CATALINA_PID或者systemctl stop这反而倒逼你把进程管理规范化。AJP 那一段是历史包袱。AJP 协议本身设计于信任边界之内早期常配 Apache httpd 做前端后来大量部署把它直接暴露在可访问网络上就成了问题源头。现在前后端基本都是 Nginx 反代 HTTPAJP 没有保留的理由注释掉最干脆。relaxedQueryChars这个属性放在这里是因为它和 URL 校验绑在一起下一节会展开——它是 8.5 上被问得最多的一个“玄学 400”。3.3 URIEncoding 与 access log 的两个细节编码这件事在 8.5 上其实比 7 省心从 Tomcat 8 开始URIEncoding的默认值就是 UTF-8GET 请求里的中文参数不需要额外配置。真正的坑在于“从 7 迁移过来的配置”。很多老项目的server.xml里明确写着URIEncodingISO-8859-1甚至GBK当年是为了兼容某套老系统直接复制到 8.5 上新接口的中文参数就全乱了。所以迁移时的动作不是“加一行”而是“检查有没有这一行有就删掉”。还要区分清楚URIEncoding只管 URL 路径和查询串的解码POST 请求体的编码是应用层的事靠request.setCharacterEncoding(UTF-8)或者框架里的CharacterEncodingFilter处理容器管不了。这一点经常被搞混导致有人反复改server.xml却怎么也修不好表单里的中文。访问日志则是另一个必须动的地方默认配置基本没法用!-- 放在 Host 标签内部 -- Valve classNameorg.apache.catalina.valves.AccessLogValve directorylogs prefixaccess_log suffix.txt rotatabletrue fileDateFormatyyyy-MM-dd bufferedtrue pattern%{X-Forwarded-For}i %a %t quot;%rquot; %s %b %D /rotatabletrue配fileDateFormat按天切分这是必须的否则单文件无限增长。bufferedtrue减少磁盘 IO代价是进程被强杀时最后一点日志会丢能接受。%D是请求处理耗时微秒排性能问题时比什么都直接%{X-Forwarded-For}i放在最前面是因为 Nginx 反代之后%a记录的永远是127.0.0.1真实客户端 IP 只能从这个头里拿——前提是 Nginx 侧确实配了proxy_set_header X-Forwarded-For否则这一列全是空的。4. 把 war 和配置放进去部署路径、上下文与 JVM到这一步容器能起来了接下来是业务真正落地的地方。部署方式选错后面每次发布都要停服JVM 参数写错位置下次升级容器全丢。这两件事都属于“一次做对长期省心”。4.1 三种部署方式与上下文路径方式做法上下文路径适用场景直接扔webappscp app.war webapps/war 文件名如/app单应用、图省事server.xml加Context写在Host内由path属性决定不推荐改配置要重启整个容器conf/Catalina/localhost/xxx.xml文件名即上下文路径/xxx推荐不动主配置第三种方式是我一直用的conf/Catalina/localhost/目录下的文件名直接决定上下文路径文件内容只描述这一个应用。好处是发布时只碰一个独立文件不动机器上共用的server.xml出问题也好回滚。# 1) war 放容器外部避免升级容器时被一起清掉 mkdir -p /data/apps cp myapp.war /data/apps/myapp.war # 2) 按上下文名建描述文件文件名 myapp.xml - 访问路径 /myapp cat /opt/tomcat/conf/Catalina/localhost/myapp.xml EOF ?xml version1.0 encodingUTF-8? Context docBase/data/apps/myapp.war reloadablefalse unpackWARstrue crossContextfalse !-- 数据源放这里凭据走环境变量或外部配置别硬编码进 war -- Resource namejdbc/main authContainer typejavax.sql.DataSource maxTotal50 maxIdle10 maxWaitMillis3000 driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://127.0.0.1:3306/app?useUnicodetrueamp;characterEncodingutf8 usernameapp passwordCHANGE_ME / /Context EOFreloadablefalse是生产必须项。设成true时容器会周期性扫描WEB-INF/classes和WEB-INF/lib下所有文件的时间戳稍有变化就重新加载整个应用。开发时方便生产上就是持续的 IO 和 CPU 开销而且重新加载会触发类加载器泄漏——每重载一次泄漏一点跑上几周 Metaspace 就满了。unpackWARstrue让 war 解压到磁盘某些依赖文件路径的框架读模板、读静态资源必须有真实目录才能工作。XML 里的必须写成amp;这在 JDBC URL 带多个参数时特别容易忘症状是启动时解析报错但错误信息指向的行号经常是上一行看着莫名其妙。4.2setenv.sh里堆内存、元空间与 GCJVM 参数写哪里决定了你下次换容器版本时要不要重来一遍。写catalina.sh是最差的选择升级即丢写/etc/profile.d只影响登录 shellsystemd 根本不读。唯一正确的位置是bin/setenv.sh。# /opt/tomcat/bin/setenv.sh JAVA_HOME/usr/local/jdk1.8.0_xxx # JAVA_OPTS 在 start 和 stop 时都会生效只放通用参数 JAVA_OPTS-XX:UseG1GC -XX:MaxGCPauseMillis200 # CATALINA_OPTS 只作用于启动重量级参数放这里 CATALINA_OPTS-server \ -Xms4g -Xmx4g \ -XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m \ -Xss1m \ -XX:HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath/data/dump \ -Dfile.encodingUTF-8 \ -Duser.timezoneAsia/Shanghai逐个说。-Xms和-Xmx设成一样是为了避免堆在运行期反复扩容缩容扩容触发的是 Full GC业务低峰期看不出来高峰期就是一次几百毫秒的停顿。MaxMetaspaceSize必须设上限因为 Metaspace 用的是本地内存不设的话类加载泄漏不会 OOM而是直接把这台机器的内存吃光然后被系统 OOM Killer 干掉——进程悄无声息地消失日志里什么都没有非常难查。HeapDumpOnOutOfMemoryError配HeapDumpPath是那条“后悔药”真出问题时至少有个堆快照能分析不然只能靠猜。注意HeapDumpPath指向的目录要提前建好并且可写否则转储失败等于没配。-Duser.timezone影响日志时间戳、new Date()的默认值和 JDBC 驱动对时间类型的处理。容器所在机器时区不对时这条比改系统时区安全得多因为改系统时区会影响机器上其他服务。-XX:UseG1GC在 JDK 8 上是可用的堆大于 4G 时比 CMS 稳停顿更可控。如果是小堆2G 以内且延迟要求不极端默认的 Parallel 也够用没必要为了“新”而换。4.3 systemd 托管与开机自启startup.sh手动起服务在生产上不可接受机器重启后服务不会自己回来进程挂了也没人拉。用 systemd 托管是最小成本的做法。# /etc/systemd/system/tomcat.service [Unit] DescriptionTomcat 8.5 Application Server Afternetwork.target [Service] Typeforking Usertomcat Grouptomcat EnvironmentJAVA_HOME/usr/local/jdk1.8.0_xxx EnvironmentCATALINA_HOME/opt/tomcat EnvironmentCATALINA_BASE/opt/tomcat EnvironmentCATALINA_PID/opt/tomcat/tomcat.pid PIDFile/opt/tomcat/tomcat.pid ExecStart/opt/tomcat/bin/startup.sh ExecStop/opt/tomcat/bin/shutdown.sh Restarton-failure RestartSec10 LimitNOFILE65535 SuccessExitStatus143 [Install] WantedBymulti-user.targetTypeforking对应startup.sh后台拉起进程的行为必须配PIDFile否则 systemd 找不到主进程systemctl status一直显示activating。SuccessExitStatus143是因为shutdown.sh结束时返回码可能是 14312815被 SIGTERM 终止不加这行 systemd 会把正常停止当失败然后根据Restarton-failure又给你拉起来看着像“停不掉”。LimitNOFILE65535是另一个必设项。默认的文件句柄限制往往是 1024连接数一上来就报Too many open files而这个错误在 Tomcat 日志里表现为莫名其妙的连接重置很难往句柄数上想。Environment里重复写JAVA_HOME看着冗余但 systemd 不读/etc/profile.d也不读.bashrc这是唯一能保证它拿到正确 JDK 的方式。5. 避坑与排查五个我实际翻过的车下面这几条都是真实遇到的按“现象 → 原因 → 解决”写遇到同类问题可以直接对照。5.1 启动阶段的三笔账现象一执行startup.sh只回一句The CATALINA_HOME environment variable is not defined correctly或者一闪就回到提示符logs里什么都没有。原因在setclasspath.sh它要靠$JAVA_HOME/bin/java判断 JDK 是否可用JAVA_HOME没设或者指到了一个 JRE 目录脚本直接退出catalina.out还没开始写。解决是把JAVA_HOME显式写进bin/setenv.sh别依赖export的临时变量也别指望PATH里的java——那多半是个软链指向的可能是 JRE。现象二BindException: Address already in use8080 起不来但ps里又看不到明显的 Tomcat 进程。原因通常是上一轮没停干净。kill -9强杀之后端口还在TIME_WAIT或者被一个残留的 java 进程占着也可能是机器上另有一套服务用了同一个端口。解决分两步先ss -lntp | grep 8080找出真正占用的 pid确认是不是自己的如果是残留有CATALINA_PID时用shutdown.sh -force没有就顺着 pid 停。别直接kill -9那样work目录里会留下编译到一半的中间文件下次启动可能出现莫名其妙的 JSP 编译错误。现象三接口报NoClassDefFoundError: javax/servlet/ServletContext或者ClassCastException指向一个看着完全无关的类。原因是 war 里打进了servlet-api.jar或tomcat-embed-core.jar。Maven 里javax.servlet:javax.servlet-api的 scope 应该是provided打 war 时被排除一旦写成默认的compile它就被塞进WEB-INF/lib。容器的类加载顺序是 bootstrap → webapp → system而javax.servlet.*在容器的过滤名单里本该被忽略但如果打进的是 embed 包包名和内部结构不完全一致过滤就失效了两套类同时存在报什么错都有可能。解决方式是检查 war 里的WEB-INF/lib清单把容器已提供的依赖全部排除。5.2 部署与访问阶段的两笔账现象四war 扔进webapps半天没反应访问/myapp返回 404日志里也没有明显报错。三个常见原因叠在一起。一是autoDeploy被关掉了容器不会主动扫描新文件二是 war 文件名带了版本号myapp-1.0.war的上下文路径是/myapp-1.0而不是/myapp三是unpackWARsfalse时某些依赖真实文件路径的框架会读不到资源。判断方法是看catalina.out里有没有Deployment of web application archive ... has finished这一行——有说明部署本身成功问题在路径没有说明根本没触发部署。稳妥做法是用 4.1 节那种conf/Catalina/localhost/myapp.xml显式指定docBase路径和文件名解耦怎么改 war 名字都不影响访问地址。现象五接口返回 400日志里写着Invalid character found in the request target。这是 8.5 上最容易被当成“玄学”的一个。容器按 RFC 7230 严格校验请求行里的字符|、{、}、[、]、\这些不在允许集合里的字符会被直接拒掉。前端在查询参数里塞了个 JSON 串、或者导出功能带了个ids[1,2,3]就中招了。解决办法有两个前端老老实实encodeURIComponent这是根治或者在 Connector 上加relaxedQueryChars[]|{}把真正用到的字符放开。别图省事把能想到的字符全列进去这等于把容器的输入校验关掉后面出事更难查。6. 从能跑到跑得稳压测前我必做的一轮核对容器能起来、页面能打开离“能扛住流量”还差一段。压测之前我固定跑一遍三条命令花不到两分钟能挡掉大部分上线后才会暴露的问题。PID$(cat /opt/tomcat/tomcat.pid) # 1) 监听面核对8005/8009 不该出现8081 应只绑 127.0.0.1 ss -lntp | grep -E 8080|8081|8005|8009 # 2) 压测期间看 GCFGC 次数基本不涨说明堆给够了持续上涨就是堆太小或有内存泄漏 jstat -gcutil $PID 1000 10 # 3) 看工作线程是否顶到 maxThreads接近 200 说明瓶颈在应用处理速度 jcmd $PID Thread.print | grep -c http-nio-8081-exec # 4) 连接状态CLOSE_WAIT 堆积说明应用没关流TIME_WAIT 多则是短连接太频繁 ss -sjstat那一条重点看FGC列和FGCT列。压测十分钟FGC从 0 涨到 3、5 次还算正常涨到几十次说明对象晋升太快要么堆不够要么有地方在疯狂创建大对象。jcmd Thread.print统计http-nio-8081-exec开头的线程数这个数字长期贴着maxThreads加线程没用得回头看慢 SQL 或者外部接口超时。还有一个容易被忽略的比例数据库连接池的maxTotal要小于 Connector 的maxThreads。如果线程池 200、连接池 50那么 150 个线程会在拿连接这一步排队表现为“CPU 不高、线程都在等”从容器指标上完全看不出来。我一般把连接池设成线程数的 1/4 到 1/2再用maxWaitMillis设个 3 秒超时让它快速失败而不是无限等待。最后是日志。上线前确认三件事catalina.out有轮转策略、access_log开了buffered、JVM 参数里有HeapDumpPath且目录可写。这三件事平时毫无存在感出事那天就是全部。从那以后我每次部署完都强制走一遍这套核对——version.sh看环境、ss看监听面、jstat看堆、Thread.print看线程四步走完再交给压测。跳过哪一步后面都得用更长的时间补回来。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站