简介:阿帕奇软件基金会的 Tomcat 8.0.47 是一款 Java Web 应用服务器,主要面向 Java 开发与运维人员,提供 Servlet 与 JSP 运行容器,负责监听 HTTP 请求并将动态内容返回给浏览器,适合用于本地开发、服务测试或生产部署。资源压缩包共含 628 个文件,整体约九点七五兆,内部不仅有已编译的类文件、Java 源码、JSP 页面、HTML 静态页和依赖库,还包含多种脚本、配置文档与许可信息,目录划分清晰,能够帮助使用者快速定位核心组件的对应文件。同时,包里附带编译后的 Servlet 示例和 JSP 示例,便于对照观察容器如何处理请求、如何生成动态页面;配置文件中还可以找到安全认证、数据源与端口连接相关内容,为深入掌握 Tomcat 的部署与调优提供了真实样例。资源内将文档、示例、类库与配置文件分层存放,能够直观看到标准发布版的典型结构;目前已有 122 人学习下载,适合准备搭建 Java 开发环境的学习者,也适合需要稳定版本的运维人员参考。
1. Tomcat 8.0.47:老项目里的主力容器,新项目里的入门样板
接手一个老 Java Web 系统的运维,第一件事就是看它的运行目录,apache-tomcat-8.0.47 这串字符并不意外。它是 Tomcat 8.0.x 分支的收官版本,稳定性和兼容性经过大量生产环境验证,Servlet 3.1、JSP 2.3、WebSocket 1.1、异步处理都支持,配置模型和目录结构与后续 8.5、9、10 延续性很高,弄懂它等于提前摸清了后面几个版本的底子。适合三类人:第一次搭 Java Web 服务端的学生,接手老项目不敢随便动容器的运维,以及想把部署流程脚本化的一线开发。不过要清楚一点:8.0.x 分支已停止安全更新,内网和教研场景完全够用,面向公网的生产系统要提前评估升级路径。下面按我在生产环境跑过的顺序展开部署、配置、调优和排错。
2. 从解压到能访问首页:环境变量、目录结构与启动脚本
2.1 目录结构与环境变量:CATALINA_HOME 和 CATALINA_BASE 别混用
拿到这份压缩包先解压,位置选在固定的软件目录,目录名不要带空格。Windows 下解压到带空格的路径,脚本里的路径解析经常翻车;Linux 下也要避开权限敏感的目录。解压后标准目录一共七个,每个在排错时都有对应要看的地方。
| 目录 | 用途 | 排错时看什么 |
|---|---|---|
| bin | 启动/关闭脚本 | 异常退出先看 catalina.sh 的报错输出 |
| conf | server.xml、web.xml、tomcat-users.xml | 端口、Host、连接器全在这里 |
| lib | 容器自身依赖的 JAR 包 | 缺 jar 导致启动失败时优先检查这里的版本 |
| logs | 运行日志输出 | catalina.out 和 localhost.log 是主要排错入口 |
| temp | 临时文件目录 | 磁盘满时这里会先报警 |
| webapps | 应用发布目录 | 检查 war 是否解压、目录是否被清空 |
| work | JSP 编译产物 | JSP 不更新先清这里 |
环境变量这块,JAVA_HOME 是前提。8.0.47 在 JDK 7 或 8 下都能跑,建议直接上 JDK 8,和 8.0.x 组合最成熟。CATALINA_HOME 指向安装根目录,CATALINA_BASE 指向实例配置目录。单实例时两者指向同一路径,很多人在做多实例部署之前,一直觉得 CATALINA_BASE 是多余概念。
export JAVA_HOME=/opt/jdk1.8.0_211 export CATALINA_HOME=/opt/apache-tomcat-8.0.47 export CATALINA_BASE=$CATALINA_HOME这三行是启动前的基线。JAVA_HOME 决定 JVM 从哪来,CATALINA_HOME 决定用哪套二进制,CATALINA_BASE 决定 webapps、conf、logs 落在哪。把 BASE 指到实例目录,就能用同一份二进制跑多个实例,升级时只替换 HOME 目录,不影响各实例的应用和配置。如果暂时只有一个实例,BASE 可以先不写,默认取 HOME 的值。
我一般不建议把这三个变量写进全局文件。系统里同时存在多个 JDK 时,全局配置一旦写错,所有依赖 JAVA_HOME 的脚本都会被带偏。更可控的做法是写进 setenv.sh,让 Tomcat 自己加载。
2.2 启动脚本、setenv.sh 与首次验证
bin 目录下有两个启动入口:startup.sh 和 catalina.sh。startup.sh 只是调用 catalina.sh start,把进程放到后台;catalina.sh 才是真正的执行脚本,支持 start、stop、run 等多个动作。初次部署我建议先前台跑一遍,不用 startup.sh 直接后台起。
cd $CATALINA_HOME/bin ./catalina.sh runcatalina.sh run 会在当前终端前台运行,JVM 启动日志、异常堆栈、端口绑定错误全部直接打出来,有问题一眼能看到。确认没有报错并访问通过后,Ctrl+C 停掉,再用 startup.sh 正常后台启动。之后看日志去 logs/catalina.out。
JVM 参数不直接改 catalina.sh。catalina.sh 启动时会自动检测 bin/setenv.sh,如果存在就先 source 它,再执行后续逻辑。这个机制是官方留给自定义参数的口子:
cat > bin/setenv.sh <<'EOF' export JAVA_OPTS="-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m" EOF chmod +x bin/setenv.sh这段把 JVM 初始堆、最大堆和 Metaspace 一并配好。不写 setenv.sh,Tomcat 按 JVM 默认堆大小跑,对大多数业务应用都偏小,很容易触发后面要讲的 OOM。这里 -Xms 和 -Xmx 设置成相同值是避免堆扩缩容带来的抖动,但总内存别超过物理机可用内存,如果机器上还跑着数据库,务必留出余量。
启动后的第一次验证做三件事。第一看日志,出现 Server startup in ... ms 说明容器本身启动成功;第二看端口,确认 8080 在监听;第三访问 http://localhost:8080,看到默认首页说明整个容器链路已经通了。这三步全过后再改 server.xml 和部署业务应用,顺序反了出问题不好定位。
2.3 启动失败时的三条命令
启动失败不用慌,按固定顺序三条命令定位。先看进程在不在,再看日志报什么,最后看端口有没有被占。
jps -l tail -f logs/catalina.out netstat -tlnp | grep 8080jps -l 能列出当前用户下的 Java 进程和主类,确认 Tomcat 是否真的起来了;catalina.out 是主日志,异常堆栈、端口冲突、配置错误都会写在这里;netstat 确认端口监听情况。三条命令组合使用,基本能覆盖 80% 的启动故障。注意 jps 只能看到当前用户的进程,如果你是 root 启动,就用 ps -ef | grep java 补充确认。
3. server.xml 核心配置:端口规划、连接器参数与 Host
3.1 层级与三个端口的划分
conf/server.xml 是 Tomcat 的主配置。一个最小可用实例的层级关系是 Server 包含 Service,Service 包含若干 Connector 和一个 Engine,Engine 下面挂 Host,Host 下面挂 Context。新手可以直接把注意力放在三个数字上:8005、8080、8009。
8005 是 shutdown 控制端口,接收关闭指令后停止 JVM;8080 是 HTTP 连接端口,业务流量都走这里;8009 是 AJP 连接端口,默认配置里以注释形式存在,只有需要和后端以 AJP 协议通信时才取消注释启用。这三个端口在同一台机器上必须全局唯一,跑多个实例时尤其要检查。默认配置里是这样:
<Server port="8005" shutdown="SHUTDOWN">port 是控制端口,shutdown 是关停口令。生产环境里这个口令建议立刻改掉,默认的 SHUTDOWN 太容易被别人猜到并远程关闭服务;同时确认这个端口只监听 127.0.0.1,不暴露到公网。这不是可选项,是我接手新环境必做的前三个动作之一。
3.2 连接器参数:默认值、线程池与压缩
8080 连接器的默认配置在 server.xml 中是这样的:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />protocol="HTTP/1.1" 在 Tomcat 8.0 下面会自动选用 NIO 实现,比老的 BIO 更适合高并发。connectionTimeout 是建立连接后读请求的超时时间,单位毫秒,20000 表示 20 秒。一个正常请求在 20 秒内发不完请求头的概率很低,保持默认就好。
决定吞吐的是另外几个没有显式出现的参数,我列成一张表,这也是做生产实例调优最常动的五项。
| 参数 | 默认值 | 含义与我的调整起点 |
|---|---|---|
| maxThreads | 200 | 最大工作线程数,动态接口多的应用先提到 400~800 |
| minSpareThreads | 10 | 空闲时保留的线程数,突发流量时减少建线程的开销 |
| maxConnections | 10000 | 已建立 TCP 连接的最大数,NIO 下默认较大 |
| acceptCount | 100 | 等待队列长度,超过后连接被拒绝 |
| URIEncoding | UTF-8 | Tomcat 8 默认 UTF-8,老项目迁移时显式写上更保险 |
maxThreads 不是越大越好。单核 CPU 能真正执行的线程数有限,线程多了以后大量时间花在上下文切换上,吞吐反而下降。假设单请求平均处理 50ms,一个线程一秒能处理约 20 个请求,400 线程理论上能支撑 8000 QPS 上下,再把数据库、缓存、GC 开销算进去,400 到 800 是中小系统常落的区间。
想要统一管理线程池,常见做法是在 Service 节点下先声明一个 Executor,再让连接器引用它:
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="500" minSpareThreads="50" acceptCount="200" maxIdleTime="60000"/> <Connector port="8080" protocol="HTTP/1.1" executor="tomcatThreadPool" connectionTimeout="20000" redirectPort="8443" compression="on"/>executor 属性让连接器使用自定义线程池,线程前缀变成 catalina-exec-,线程 dump 时一眼能认出是哪个池。compression="on" 开启响应压缩,对文本类接口收益明显,代价是 CPU 开销;图片、PDF 这类本身压缩过的资源不需要再压一遍。
3.3 Host、appBase 与 Context 映射
Host 节点是应用的存放入口,默认是这样:
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> </Host>appBase 表示应用根目录,webapps 是相对 CATALINA_BASE 的路径。unpackWARs="true" 表示部署 WAR 时自动解压成目录,我保持 true,直接跑解压目录比每次从 WAR 里提取资源稳定,排查问题也方便直接看文件。autoDeploy="true" 表示 Tomcat 会周期扫描 appBase,发现新 WAR 就自动部署。开发环境建议开,生产环境建议关掉改成手动发布,避免扫描到半成品文件触发意外部署。
应用映射路径默认由 WAR 文件名决定。order.war 丢进 webapps,访问路径就是 /order。如果业务上要映射到别的地方,或者应用代码放外部目录,常见做法是显式配置 Context:
<Context path="/order" docBase="/data/apps/order" reloadable="false"/>path 是访问路径,docBase 是实际目录。这样 Tomcat 安装目录和应用代码分离,升级容器时不会把应用一并卷进去。注意 path 不能和 appBase 下已有的 WAR 文件名冲突,否则启动期会报部署冲突。reloadable="false" 生产保持 false,类文件热替换有时带着旧状态跑,出问题反而难查。
4. WAR 部署与避坑:五个高频问题的现象、原因与解决
4.1 部署方式:丢目录、Manager、脚本化
把 WAR 包复制到 webapps 目录下是最直接的部署方式,适合测试环境和一次性验证;Tomcat 自带的 Manager 应用支持页面上传 WAR、在线部署下线,适合小团队手工发布;生产上我更多用脚本化方式:把 WAR 包传到服务器,停 Tomcat,清空解压目录,启动。三种方式不是互相替代,是在不同规模环境下的取舍。脚本化每次做的动作完全一致,少很多手工遗漏。
4.2 端口被占用,启动秒挂
现象:执行 startup.sh 后,catalina.out 里报Address already in use: JVM_Bind:8080,或者是Failed to initialize end point associated with ProtocolHandler,进程虽然存在,但 8080 端口根本没监听。
原因:最常见的是重复启动,同一台服务器上已经有一个 Tomcat 在跑,又执行了一次 startup.sh;其次是 8080 被其它业务进程占用。开发环境里内嵌容器和独立 Tomcat 同时跑,端口冲突概率也很高。
解决:先用 netstat 定位占用进程:
netstat -tlnp | grep 8080看输出里 PID 和进程名,确认是不是另一个 java 进程。如果是旧的 Tomcat,用它的 shutdown 端口正常关停,不要直接 kill -9 了事;如果是其它服务占用,换端口或调整服务端口。顺手检查 8005 和 8009,多实例环境经常出现 8080 没冲突、8005 冲突的隐蔽场景,启动日志里的 Bind exception 会指向具体端口。
4.3 老参数残留:MaxPermSize 警告并不致命
现象:从 Tomcat 7 或更早版本迁移 setenv.sh 过来,启动日志打出一行Ignoring option MaxPermSize; support was removed,有人看到 warning 以为是启动失败,实际上进程正常起来了。
原因:Java 8 把方法区实现从 PermGen 换成了 Metaspace,-XX:PermSize和-XX:MaxPermSize已经不存在。JVM 遇到不认识的参数不打 fatal,而是忽略并给警告,不影响进程启动。
解决:把 setenv.sh 里的 PermSize、MaxPermSize 删掉,换成 Metaspace 参数:
export JAVA_OPTS="-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m"MetaspaceSize 是触发 GC 的阈值,不是上限;MaxMetaspaceSize 才是真正的上限。动态生成类比较多的系统,比如大量 CGLIB 代理、反射调用,Metaspace 涨得快,上限设太小会频繁 Full GC。网上流传的 64m 偏低,我一般从 256m 起步,压测时观察使用率再微调。
4.4 GET 中文乱码,问题不止在 Tomcat 一层
现象:页面表单 GET 方式提交中文,后端拿到参数全是乱码。页面 charset 声明了 UTF-8,后端也按 UTF-8 处理,但结果仍是乱码。
原因:Tomcat 8.0 的 URIEncoding 默认已经是 UTF-8,比 Tomcat 7 的 ISO-8859-1 默认值友善很多。但乱码往往是链路问题:页面没有显式声明 charset,浏览器按本地默认编码提交;setCharacterEncoding 只对 POST body 生效,GET 参数不受它控制;数据库连接串缺少 characterEncoding,数据入库再读出时又乱一次。
解决:三层同时做。Tomcat 连接器显式加 URIEncoding="UTF-8";应用入口配置统一编码的 Filter,常见框架提供的 CharacterEncodingFilter 就够,forceEncoding 设为 true;数据库连接 URL 追加 characterEncoding=UTF-8。三层缺任何一个,乱码都可能换个位置冒出来。排查时先定位是请求参数乱码还是响应显示乱码,方向不同处理点也不同。
4.5 替换同名 WAR 后应用还是旧代码
现象:把新打的 order.war 覆盖到 webapps 下,访问页面还是旧功能,日志里没有报错。
原因:两种情况最常见。Tomcat 判断 WAR 是否变化靠文件时间戳,覆盖时新文件时间戳比旧文件旧,Tomcat 认为没有新内容可部署;或者解压出来的 order 目录还在,部分 class 文件没被清掉,Tomcat 沿用了旧解压目录。
解决:我固定的发布顺序是停 Tomcat,删除 webapps/order 解压目录,删除旧 order.war,放新 order.war,再启动。不想重启整个容器,就在 Manager 里先 undeploy 再 deploy,比手动覆盖稳定得多。autoDeploy 开启时扫描周期默认在 10 秒左右,复制完文件后等一小会儿再验证,别刚复制完就去刷页面。
4.6 Manager 页面 403 与解锁步骤
现象:访问 /manager/html 出现 403 Access Denied,Tomcat 启动正常,业务应用也正常。
原因:Manager 是独立 web 应用,默认只允许指定角色访问,而默认的 tomcat-users.xml 里没有任何用户。手动配置的账号缺少 manager-gui 角色,也会得到同样 403。
解决:编辑 conf/tomcat-users.xml,在 tomcat-users 节点里添加账号:
<role rolename="manager-gui"/> <user username="admin" password="强密码" roles="manager-gui"/>manager-gui 角色只开放页面操作权限;如果准备用脚本调 Manager 接口自动部署,要配 manager-script 角色。生产环境不开放 Manager 的远程访问,只允许内网 IP 访问,并把默认 admin 账号改成复杂口令。
5. 性能调优与压测:把连接器参数调到业务能用
5.1 连接器参数的关系:连接数、队列与线程
调优之前先理清连接器的处理流程。一个请求进来,先占用一个 TCP 连接,进入 acceptCount 定义的等待队列,再由线程池里的空闲线程接管。三个关键参数管的是不同阶段:maxConnections 管有多少连接可以同时建立,acceptCount 管队列里能排多少人,maxThreads 管真正并发处理的规模。它们不是独立参数,要配合着看。
连接数满但队列没满,请求还能排队等;队列也满了,新连接才被拒绝。压测时如果把 acceptCount 设成 10、并发又拉到 500,会看到大量 Connection refused,这不是连接器挂了,而是排队区域不够。反过来,maxThreads 设到 1000 但 CPU 只有 4 核,线程切换开销会拖垮吞吐,压测数据不升反降。参数调优最忌讳孤立地看某一个值。
5.2 参数调整的边界与对照表
第 3 章给过 Executor 的示例,这里是更完整的对照表,可作为起步配置,再按业务微调。
| 参数 | 8.0.47 默认 | 起步调整 | 适用场景 |
|---|---|---|---|
| maxThreads | 200 | 500 | 动态接口、有 DB 查询的常规 Web 系统 |
| minSpareThreads | 10 | 50 | 突发流量明显,减少线程创建延迟 |
| acceptCount | 100 | 200 | 并发尖峰明显,短暂排队可以接受 |
| maxConnections | 10000 | 8000 | 连接数本身不是瓶颈,按内存衡量 |
minSpareThreads 上调意味着启动时多预留线程,启动时间略长,换来的是突发流量时不需要临时创建线程。acceptCount 调大到 200,允许 200 个请求在队列里等待,队列满后新请求才会被拒绝。我一般把 acceptCount 和 maxThreads 的比例控制在 1:2 到 1:3 之间,排队时间太长,客户端超时重试反而放大故障。
5.3 keepAlive 与超时参数:常被忽略的吞吐因素
连接器还有两个容易被忽略的参数:keepAliveTimeout 和 maxKeepAliveRequests。keepAliveTimeout 控制一个连接在处理完一个请求后,闲置多久才关闭,默认继承 connectionTimeout 的 20 秒;maxKeepAliveRequests 控制一个连接最多复用多少次。
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" keepAliveTimeout="30000" maxKeepAliveRequests="500" redirectPort="8443" />现代浏览器普遍复用连接,keepAliveTimeout 太短会导致频繁建连,太长又让慢客户端占着连接不释放。接入层有负载均衡时,连接大多在负载均衡处断开,Tomcat 侧保持默认即可;只有客户端直连 Tomcat 的场景才需要重点调这两个值。调完记得压测对比,不要只改不验。
5.4 压测验证与三个容易翻车的点
改完配置先重启,再跑压测。用 ab 这类通用 HTTP 压测工具做前后对比:
ab -n 10000 -c 200 http://localhost:8080/order/list-n 是总请求数,-c 是并发数。重点看三个指标:Requests per second 是吞吐,Failed requests 是失败数,Time per request 是平均单请求耗时。调整前跑一遍记录基线,调整后再跑一遍,数据对比决定参数去留,不要凭感觉。
压测里有三个我踩过的点。第一,压测机不要和 Tomcat 放在同一台机器,否则压测工具会抢 CPU,测出来的数据噪声很大;第二,并发数不要高于 maxThreads,比如 maxThreads 500 时并发设 200 合理,设到 1000 测的是排队能力,不是线程处理能力;第三,压测时用 jstat 看堆和 GC,如果频繁 Full GC,瓶颈在 JVM 参数而不是线程池,应该回头调 setenv.sh。很多性能问题被误判为连接器配置问题,实际 GC 才是黑匣子。
6. 安全加固与运维习惯:最后一轮检查
6.1 改掉默认 shutdown 口令与移除示例应用
收尾时做三件事。第一件是把 server.xml 里 8005 的 shutdown 口令从 SHUTDOWN 改成随机字符串,确认控制端口只监听 127.0.0.1。第二件是删除 webapps 下默认自带的示例应用,包括 ROOT、docs、examples、manager、host-manager 这些目录,Tomcat 8.0.47 解压后自带一套示例,生产环境里没有保留价值,留着反而多一个攻击面。删除前把 ROOT 目录备份一份,后面部署应用时可以用自己的 ROOT 替换默认首页。
第三件是开启访问日志。Tomcat 默认不记录完整的访问日志,排查问题时不方便复盘。在 Host 节点下加一个 Valve 就能开启:
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log." suffix=".txt" pattern="%h %l %u %t "%r" %s %b %D" />pattern 里的 %D 是处理请求耗时,单位毫秒,线上性能问题初筛就靠这一列。访问日志落盘有少量 IO 开销,但对定位问题和安全审计的价值更大。
6.2 备份与变更留痕
配置文件的后悔药只有备份。server.xml 改动前先复制一份带日期的备份,tomcat-users.xml 同样处理。不要直接覆盖原文件,用新增文件的方式保留历史版本,方便回滚。setenv.sh 里 JVM 参数每次调整后,把调整前的参数值记进变更记录,压测数据前后对比一并归档。多实例环境里,每个实例的配置独立归档,别把实例 A 的 server.xml 当成实例 B 的基线。
从那以后,我每次接手一台陌生 Tomcat,都会强制走一遍完整流程:先确认版本,再看 setenv.sh 有没有残留的旧 JVM 参数,接着检查 server.xml 的 shutdown 口令、连接器参数和 Host 配置,改完配置必做一轮压测对比,最后把 conf 目录备份归档。这套习惯帮我少踩了很多翻车现场,也把 8.0.47 的老实例稳定运行到现在。如果你手头正好缺一份干净的 Tomcat 8.0.47 安装包,按第 2 章的目录结构装起来,用第 5 章的对照表做一次调优和压测,这套配置逻辑就全通了。希望帮到你。
本文还有配套的精品资源,点击获取