☰
Tomcat 8.0.47 部署配置与故障排查实战指南
2026/10/26 18:56:25 网站建设 项目流程

简介:Apache Tomcat 8.0.47 是面向 Java Web 开发者、运维学习者与教学人员的轻量级 Servlet/JSP 容器发行包,可用于本地搭建 Web 运行环境、部署 war 应用、验证 JSP 动态页面与 Servlet 请求处理流程。压缩包共 628 个文件,约 9.75MB,包含 html 文档、java 源码与 class 字节码、jsp 页面、jar 依赖库,以及 xml、properties 等配置文件和启动脚本,较完整地保留了发行版目录结构。现有 122 人学习下载,适合希望从容器配置、应用部署到源码结构逐层理解 Tomcat 的读者。借助 Catalina、Jasper、Coyote 等核心组件相关文件,可对照 server.xml、context.xml、web.xml 理解连接器、虚拟主机、上下文与安全约束配置;结合 manager/admin 管理应用和 logs 日志,还能梳理应用监控与故障排查思路,降低环境搭建与版本适配成本,作为 Java Web 入门和项目部署的参考环境。

1. Tomcat 8.0.47 不是“老古董压缩包”:谁在找它、找来干什么

搜索 apache-tomcat-8.0.47 下载的人,多半不是刚入门。手上有一套跑了好些年的 Java Web 项目,编译级别停在 JDK 7 或 8,Servlet 规范要求 3.1,换个容器就抛 NoSuchMethodError,回头翻当年那套环境,八成就落在 Tomcat 8.0.x 这条线上。8.0.47 是这条线里靠后的维护版本,解压即用、不带安装器、目录结构简单直接,对只想把 war 包扔进去跑起来的人最省事。它适合三类场景:给遗留系统复现一套可运行环境、在本地验证 Servlet 3.1 与 JSP 2.3 的具体行为、以及需要一份干净的容器做对照测试。新项目别把它当默认选项,边界在后几章会讲清楚。

2. 解压到跑通 8080:目录、环境变量与两条必跑命令

拿到压缩包后最忌讳的动作是直接双击 startup。容器启动涉及 JAVA_HOME、CATALINA_HOME、CATALINA_BASE 三个变量的配合,路径里还夹着中文或空格,脚本解析就会出错,而报错信息往往不给提示。这一章把顺序固定下来:先解压到规范路径,再分清两个 CATALINA 变量,然后跑一次配置校验,最后才启动。

2.1 解压路径与三个变量的分工

Linux 下用 tar 解压,Windows 下直接解压即可,唯一硬性要求是路径不含空格和中文。解压完成后别急着改名成tomcat,保留版本号目录,后面同时维护两三个版本时不容易搞混。

# 解压到统一目录,路径不含空格与中文字符 tar -zxvf apache-tomcat-8.0.47.tar.gz -C /opt/ mv /opt/apache-tomcat-8.0.47 /opt/tomcat8047 # 三个变量分开设,不要混在一个 export 里 export JAVA_HOME=/opt/jdk1.8.0 export CATALINA_HOME=/opt/tomcat8047 export CATALINA_BASE=/opt/tomcat8047 export PATH=$JAVA_HOME/bin:$PATH

参数说明:CATALINA_HOME只被 bin 下的脚本用来定位 catalina.sh、setclasspath.sh 和 lib;CATALINA_BASE才决定 conf、logs、temp、webapps、work 这几个可写目录的位置。单实例部署时两者指向同一目录即可,CATALINA_BASE不设也默认等于 HOME。多实例场景才是这个设计真正发挥作用的时候:HOME 只留一份,复制多份 BASE 目录出来,各自的 conf 里改端口,共享同一套 jar 和脚本,升级容器时不用逐个改。

JAVA_HOME这一条是启动失败的头号原因。指向 JRE 目录、指向带空格的Program Files、或者只在当前 shell 里 export 过但没写进 profile,都会导致startup.sh静默退出。判断方法很简单:echo $JAVA_HOME后接$JAVA_HOME/bin/java -version,能打印出版本号才算过关。

目录各自的职责值得记一遍,出问题时第一个动作往往就是去对应目录翻。

目录作用常见误操作
bin启停脚本、setenv.sh、catalina.sh把 JAVA_HOME 硬写进 catalina.sh,换版本时被覆盖
confserver.xml、web.xml、tomcat-users.xml、logging.properties直接改全局 web.xml,影响所有应用
lib容器级 jar,所有应用共享业务驱动扔进来,和 WEB-INF/lib 版本打架
logscatalina、localhost、access 三类日志只盯 catalina.out,忽略 localhost 里的应用异常
webapps默认自动部署目录手工删解压目录不彻底,残留旧 class
workJSP 编译产物与会话序列化文件改了 JSP 还是老页面,不清理这里
temp上传与大文件临时落盘挂在小分区上,上传一多就写满

2.2 bin/setenv.sh 与 JVM 参数:写在哪比写什么更重要

Tomcat 的启动脚本会读两种变量:JAVA_OPTS和CATALINA_OPTS。区别在于前者在 stop 命令里也会生效,后者只在 start 时生效。堆内存、GC 参数这类只跟运行进程有关的东西,写进CATALINA_OPTS更干净,避免停服务时也申请一遍大内存。

不建议直接修改catalina.sh,因为升级容器时它会被整个覆盖。正确做法是在 bin 下新建setenv.sh,脚本会自动 source 它;Windows 对应setenv.bat。

# /opt/tomcat8047/bin/setenv.sh,创建后记得 chmod +x export CATALINA_OPTS="-Xms512m -Xmx1024m \ -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m \ -Dfile.encoding=UTF-8 -Duser.timezone=GMT+08:00 \ -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/tomcat8047/logs"

参数说明:-Xms与-Xmx设成同值可以省掉堆伸缩的开销,容器类应用一般这么配;MetaspaceSize和MaxMetaspaceSize是 JDK 8 的写法,如果这套环境跑在 JDK 7 上,对应的是-XX:PermSize与-XX:MaxPermSize,写错了不会报错,但也不会生效,这是很容易被忽略的一点。HeapDumpOnOutOfMemoryError会在内存溢出时自动导出快照,事后定位比凭猜强得多,代价只是多占一点磁盘。

2.3 configtest 先跑一遍,再谈启动

catalina.sh configtest只解析 conf/server.xml、检查端口配置和 Listener 类能否加载,不启动服务。改过 server.xml 之后先跑它,比直接 startup 再看一堆日志快得多。

cd /opt/tomcat8047 # 第一步:只校验配置,不启服务 ./bin/catalina.sh configtest # 第二步:配置无误后再后台启动 ./bin/startup.sh # 第三步:确认进程与端口都在 ps -ef | grep -v grep | grep Bootstrap ss -lnt | grep -E '8080|8005' # 第四步:确认能访问到欢迎页,返回 200 就说明容器本身没问题 curl -I http://127.0.0.1:8080/

Windows 环境下把./bin/catalina.sh换成bin\catalina.bat,startup.sh换成startup.bat,其余验证方式一样。startup.sh只是把catalina.sh start包了一层,真正的输出重定向到logs/catalina.out,所以前台调试时用./bin/catalina.sh run,报错会直接打印在终端上,比翻日志快。

JDK 版本这一项要单独说。Tomcat 8.0.x 要求 Java 7 及以上,实际搭配 JDK 8 最稳定。JDK 11 及以上不建议硬上,8.0.x 的很多反射调用和类加载假设是按 Java 8 及以前写的,即便勉强跑起来,也会在启动或热部署阶段出现无法解释的问题。复现遗留环境时,JDK 版本要和 Tomcat 版本一起复现,只对上一个不算对上。

3. server.xml 三处必改:线程池、编码与压缩的取舍

server.xml 是这个容器里唯一必须看懂的文件。默认配置能跑,但扛不住真实流量,也解决不了编码问题。这一章只改三处:把 Connector 的线程模型换成 Executor 引用、把线程数与连接数按容量估一遍、把编码和压缩一次配到位。改之前先备份server.xml,这是常规操作。

3.1 Executor 抽取与 maxThreads 的估算方法

默认 Connector 自己维护线程池,多个 Connector 时每个都要单独配。用<Executor>抽出来共享,配置只写一份,AJP 和 HTTPS 连接器可以直接引用同一个。

<!-- conf/server.xml,放在 <Service> 内部、<Connector> 之前 --> <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="300" minSpareThreads="30" maxIdleTime="60000" prestartminSpareThreads="true"/> <Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" executor="tomcatThreadPool" connectionTimeout="20000" acceptCount="300" maxConnections="10000" maxPostSize="10485760" URIEncoding="UTF-8"/>

参数说明:maxThreads是同时处理请求的工作线程上限,估法是“单请求平均耗时(秒)× 目标 QPS”,比如单请求 100ms、目标 500 QPS,理论上需要 50 个线程,留三倍余量就是 150 到 200。acceptCount是内核 accept 队列长度,连接数打满之后新请求在这里排队,超过才被拒。maxConnections在 NIO 下是瞬时连接数上限,超过的连接不会立刻失败,而是排进 accept 队列。三个参数的关系是:maxConnections 管“同时挂住多少连接”,maxThreads 管“同时处理多少请求”,acceptCount 管“还能容忍多少等待”。

connectionTimeout在 8.0 里的默认值是 60000 毫秒,对内部服务太长,收紧到 20000 更合适。maxPostSize默认只有 2MB,表单里带一个稍大的附件就会直接返回 400,排查时容易误判成前端问题,所以一次性调到 10MB。

3.2 URIEncoding 与 POST 编码是两件不同的事

Tomcat 8.0 起,URIEncoding的默认值已经是 UTF-8,GET 请求的路径参数一般不用额外配。但 POST 请求体的编码不走这条路径,Servlet 3.1 规范里没有统一配置项(那是 4.0 才加的),必须由应用侧在读取参数前显式设置。

<!-- conf/server.xml 中确认 Connector 上带这两个属性,压缩只压文本类 --> <Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" executor="tomcatThreadPool" URIEncoding="UTF-8" compression="on" compressionMinSize="2048" noCompressionUserAgents="gozilla, traviata" compressibleMimeType="text/html,text/xml,text/css,text/javascript,application/json,application/javascript"/>

参数说明:compressionMinSize设成 2048 字节,太小的响应压缩后反而更大;compressibleMimeType只列文本类,图片、视频、压缩包不要放进去,压了几乎不减小还会多吃 CPU;noCompressionUserAgents用于回避极少数对压缩支持有问题的老客户端。

应用侧对应要配的还有三处:JSP 页头的pageEncoding="UTF-8"、响应头setContentType("text/html;charset=UTF-8")、以及一个把request.setCharacterEncoding("UTF-8")放在最前面的 Filter。第三处的顺序是硬约束,一旦在设置编码之前调用过getParameter,请求体已经按默认编码解析完,再设置就无效了,这是乱码排查里最常见的一个死结。

3.3 三处容易照抄错的地方

第一处是 SSL 配置。网上大量 Tomcat 文档讲的是SSLHostConfig那套写法,那是 8.5 之后的结构,8.0.47 里照抄会直接导致 Connector 初始化失败。8.0 用的还是 Connector 上的scheme、secure、SSLEnabled、keystoreFile、keystorePass、sslProtocol这一组属性,并且需要指定实现类,不指定时默认走的是非 SSL 实现。

第二处是protocol="HTTP/1.1"这个简写。在 8.0 里它默认解析成 NIO 实现,和显式写Http11NioProtocol等价;但有些老教程会说它走 BIO,那是 7.0 时代的行为,照 7.0 的思路去配maxThreads,容量估算会整体偏掉。

第三处是 shutdown 端口。<Server port="8005">是接收关闭指令的端口,默认只监听本机。生产环境如果不打算用shutdown.sh停服务,把它改成-1可以直接禁用,少一个暴露面。但要注意,改成 -1 之后shutdown.sh会失效,停机流程得改成发信号或者走进程管理,改之前先确认运维那边能接受。

4. 部署 war 之后发生了什么:context path、docBase 与 reloadable

把 war 包丢进 webapps 就完事,这句话只在“路径刚好对、类刚好不冲突、不需要外部目录”这三件事同时成立时才对。真实项目里这三个条件经常有一个不满足,然后就变成“文件明明在,访问就是 404”。这一章把部署规则讲透。

4.1 autoDeploy 与 unpackWARs 决定的两套行为

默认Host上的autoDeploy="true"和unpackWARs="true",含义是:容器启动时扫描 webapps 目录,发现 war 就解压成同名目录,然后用解压后的目录作为应用根;运行期间检测到 war 时间戳变化,自动重新部署。

<!-- conf/server.xml 中 Host 段,这几项决定部署行为 --> <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 %D"/> </Host>

context path 的推导规则很直接:war 叫ROOT.war映射到/,叫app.war映射到/app,叫app##1.2.war同样是/app并且支持并行部署两个版本。这条规则决定了访问地址,很多 404 是因为 war 名和预期路径差了一个字母。

unpackWARs="false"时容器不解压,直接从 war 里读类,好处是部署快、磁盘占用小,坏处是 JSP 编译和静态资源读取都要走一层解压逻辑,首次访问明显变慢,调试期不建议这么配。

4.2 docBase 指向外部目录:两种写法与各自的约束

项目目录不方便挪到 webapps 下时,用 Context 指定docBase。生产上常见的是代码目录和容器目录分开挂载,容器整体升级不影响代码。

<!-- 方式一:在 conf/Catalina/localhost/ 下新建 app.xml,文件名即 context path --> <Context docBase="/data/apps/app" reloadable="false" crossContext="false"> <Resources cachingAllowed="true" cacheMaxSize="102400"/> </Context>
<!-- 方式二:直接写在 server.xml 的 Host 里,改动需要重启才生效 --> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Context path="/app" docBase="/data/apps/app" reloadable="false"/> </Host>

两种写法的差别在于生效时机和维护成本。conf/Catalina/localhost/下的小文件是推荐做法,一个应用一个文件,热部署能感知,删文件即下线,不污染主配置。写进 server.xml 的 Context 改动要重启,而且容器版本升级时容易被覆盖,只适合临时验证。

参数说明:docBase用绝对路径时不受 appBase 限制;reloadable="true"会在 WEB-INF/classes 或 WEB-INF/lib 变化时整应用重载,开发期方便,生产上每次重载都会清空会话、重新编译 JSP,是一次明显的抖动,务必关掉。crossContext默认 false,除非确实要在应用之间共享 ServletContext,否则别开。

4.3 work 目录与 SESSIONS.ser:那些“改了不生效”的源头

JSP 最终会被编译成 Java 类放在work/Catalina/localhost/<context>/下。改了 JSP 却还是老页面,八成是时间戳没触发重编译,或者 IDE 同步文件时保留了旧修改时间。彻底一点的做法是停服务后删掉对应的 work 子目录,重启后重新编译。

另一个常被忽略的是会话持久化。容器正常关闭时会把会话序列化到work/Catalina/localhost/<context>/SESSIONS.ser,下次启动再读回来。这个机制的前提是会话里的对象都能序列化,并且类的 serialVersionUID 没变。开发期频繁改实体类,就会出现启动时反序列化失败、会话全部丢失的情况,日志里通常只有一行 warning。团队里我一般建议开发环境直接关掉会话持久化,在 Context 里加<Manager pathname="" />,省掉这类无效排查。

5. 五个典型问题:启动退出、端口占用、404、乱码、内存溢出

下面五条是按出现频率排的,每一条都能对应到一个具体的日志位置或命令输出。遇到问题时按顺序对一遍,比漫无目的地翻日志快。

5.1 现象:双击 startup.bat 窗口一闪而过,Linux 下 startup.sh 执行完没反应

原因:JAVA_HOME未设置、指向 JRE 而非 JDK、或者路径里带空格导致 setclasspath.sh 解析失败。脚本把错误信息打到了标准输出,而后台启动把这些输出丢进了 catalina.out,窗口一闪就什么都看不见。

解决:先用前台模式跑一次,错误会直接打在终端上。

# 前台启动,报错直接可见,Ctrl+C 退出 cd /opt/tomcat8047 ./bin/catalina.sh run # 同时确认变量本身没问题 echo "$JAVA_HOME" "$JAVA_HOME/bin/java" -version

如果终端里出现Neither the JAVA_HOME nor the JRE_HOME environment variable is defined,那就是变量没生效,检查是不是只在一个终端里 export 过。Windows 下把 JAVA_HOME 设成系统变量并确保路径不含空格。

5.2 现象:日志里明确写着 Address already in use: bind,服务起不来

原因:端口被占。多数人只记得改 8080,忘了 8005(shutdown 端口)和 8009(AJP 端口)以及redirectPort指的 8443。同一台机器上跑两个实例时,只改一个端口必然冲突。

解决:先查出占用者,再决定是换端口还是停进程。

# 查端口占用,ss 比 netstat 更直接 ss -lntp | grep -E '8005|8009|8080|8443' # 没有 ss 的环境用 lsof lsof -i:8080 # 多实例部署时四处一起改,缺一处就冲突 # <Server port="8006"> # <Connector port="8081" ... redirectPort="8444"/> # <Connector port="8010" protocol="AJP/1.3"/>

还有一种隐蔽情况:上次没停干净,进程还在但端口处于 TIME_WAIT,看起来像没占用。直接ps -ef | grep Bootstrap确认进程,用kill -15走正常停机流程,别用-9,否则 work 和 temp 下的临时文件会残留。

5.3 现象:war 明明放在 webapps 下,访问对应路径返回 404

原因:三种可能。war 文件名与期望的 context path 不一致;解压目录残留,容器用的是旧目录而不是新 war;work 下缓存了旧的 JSP 编译结果。

解决:按顺序核对,不要跳步。

# 第一步:看解压目录和 war 是否同时存在且时间戳一致 ls -l --time-style=full-iso /opt/tomcat8047/webapps/ # 第二步:彻底清掉旧产物再重启,注意先停服务 cd /opt/tomcat8047 ./bin/shutdown.sh rm -rf webapps/app webapps/app.war rm -rf work/Catalina/localhost/app # 重新放入 war 后启动 ./bin/startup.sh

判断 context path 最可靠的方法是看 logs 里的启动日志,容器会打印每个应用的 context 初始化和映射结果,那一行Deployment of web application archive ... has finished in ... ms就是准信。

5.4 现象:表单提交的中文在后台读出来是问号,或者页面返回一串乱码字符

原因:GET 路径参数和 POST 请求体走的是两套编码逻辑。8.0 起URIEncoding默认 UTF-8,GET 一般不出问题;POST 体在 Servlet 3.1 下没有统一配置,谁先读谁决定编码。

解决:应用侧加过滤器,且必须在任何getParameter之前生效。

// 过滤器里第一件事就是设编码,顺序错了后面全白做 public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { // 必须在读取任何参数之前设置,否则请求体已按默认编码解析完毕 req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); } }

配套还要检查三处:JSP 页头的pageEncoding="UTF-8"、数据库连接串上的characterEncoding=utf8、以及容器启动参数里的-Dfile.encoding=UTF-8。四处缺一处,乱码就会以不同形态出现:页面乱、日志乱、库里乱,现象不同但根因往往都在这里。

5.5 现象:跑几天就抛 OutOfMemoryError: Metaspace(JDK 8)或 PermGen space(JDK 7)

原因:CATALINA_OPTS没配,容器用的是默认的小额元空间;或者配了但写在catalina.sh里,容器升级后被覆盖;再或者开发环境开了reloadable="true",反复热部署导致旧类卸载不掉,元空间被逐步耗尽。

解决:参数落到 setenv.sh,同时确认热部署关闭。

# bin/setenv.sh 中确认存在,改完重启才生效 export CATALINA_OPTS="-Xms1024m -Xmx1024m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/tomcat8047/logs" # 运行期观察元空间与堆的实际占用 jstat -gcutil $(jps | grep Bootstrap | awk '{print $1}') 5000

看M列(Metaspace 使用率)是否在持续上涨不回落,如果是,基本可以确定是类加载泄漏,重点查热部署和动态代理生成类。如果O列(老年代)持续上涨,那是对象泄漏,方向完全不同,别混着查。

6. 启动前自检脚本与日志三件套:把排查前置到启动之前

上面五条问题里,有四条可以在启动之前用几十秒检查出来。我习惯在容器目录旁放一个前置检查脚本,检查完再启动,能省掉大量“起了但不对”的时间。

#!/usr/bin/env bash # preflight.sh —— 启动前自检,放在容器目录同级,chmod +x 后使用 set -u BASE="${CATALINA_BASE:-/opt/tomcat8047}" fail=0 # 1) 关键脚本与配置是否齐全 [ -x "$BASE/bin/catalina.sh" ] || { echo "[缺失] bin/catalina.sh 不可执行"; fail=1; } [ -f "$BASE/conf/server.xml" ] || { echo "[缺失] conf/server.xml"; fail=1; } # 2) 配置文件语法与监听类能否加载,不启服务 "$BASE/bin/catalina.sh" configtest >/dev/null 2>&1 \ || { echo "[配置] server.xml 解析失败,单独跑 configtest 看详情"; fail=1; } # 3) 端口占用,四处都要看 for p in 8005 8009 8080 8443; do if ss -lnt 2>/dev/null | awk '{print $4}' | grep -q ":$p$"; then echo "[端口] $p 已被占用"; fail=1 fi done # 4) 可写目录权限,容器起不来常常是因为这里 for d in logs temp work webapps; do [ -w "$BASE/$d" ] || { echo "[权限] $BASE/$d 不可写"; fail=1; } done if [ "$fail" -eq 0 ]; then echo "自检通过,可以启动" else echo "先修上面的问题再启动" fi exit "$fail"

脚本里的四组检查对应四类典型故障:脚本不可执行和配置解析失败对应“起不来”,端口占用对应“起了但报 bind 错误”,目录权限对应“启动了但写不了日志、JSP 编译不了”。这四组都是几秒钟能验完的事,比启动后翻三分钟日志高效。

日志这边有三份文件必须知道各看什么,很多人只盯 catalina.out,结果应用异常一直在另一个文件里刷。

日志文件内容什么时候看
catalina.out标准输出与标准错误,含启动横幅和未捕获异常启动失败、进程莫名退出
catalina.YYYY-MM-DD.log容器自身日志,按天切分部署过程、Connector 初始化
localhost.YYYY-MM-DD.log应用级日志,含每个 context 的异常栈应用报错、404、类加载失败
localhost_access_log.YYYY-MM-DD.txt访问日志,含状态码与耗时排查慢请求、确认请求是否打到容器

访问日志里我一般会加%D输出处理耗时(毫秒),排查“接口慢”时能直接判断是容器层慢还是应用层慢。这一步加上之后,很多原本要抓包的问题在日志里就能定位。

还有一个容易被忽略的验证习惯:改完任何配置,不要只看“服务起来了”,要发一个真实请求确认返回码和内容。curl -I看头部状态码,curl -s ... | head看响应体开头几行,两步做完才算验证过。从那次因为改了一个Host属性导致所有应用静默降级之后,我每次动完 server.xml 都强制走一遍 configtest、发一个真实请求、再看一眼 localhost 日志尾部这三步,顺序不乱。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询