简介Tomcat 8.0.47 是一份基于 Java 的 Web 服务器资源包面向 Java Web 开发人员、运维工程师以及入门学习者实现了 Servlet 与 JSP 规范适合部署小型到中型 Web 应用、搭建本地学习环境也可作为理解容器原理的参考材料。压缩包内共包含 628 个文件文件类型主要包括 html 静态页面、Java 源码、编译后的 class 文件、JSP 动态页面、jar 依赖库以及 xml、sh、bat 等配置与启动脚本整体压缩后大小约为 9.75 兆字节目前已有 122 人学习下载。包内保留了完整的 Tomcat 目录结构解压后即可直接部署运行其中可以看到 Catalina 容器核心、Jasper 引擎、Coyote 连接器等相关文件并支持通过 server.xml、web.xml 调整端口、连接器和安全约束借助 manager 管理界面监控部署状态从 logs 日志中排查运行问题。无论用于实际部署还是深入学习 Tomcat 的组件协作方式、目录组织或配置思路这份资源都具备较高的参考价值。1. 还在用 Tomcat 8.0.47 的生产环境这版老实例到底能扛多少并发如果你接手过任何一台上线超过三年的服务器大概率见过 Tomcat 8.0.47 的目录结构。Apache 下的这个经典版本是很多旧系统的默认容器没换过。它的价值不是新特性而是稳定、资料多、踩坑记录全。但问题也出在这网上教程大多写给 8.5 或 9.0直接套用到 8.0.47 上经常出现配置项不识别、连接池行为不一样的情况。这篇文章从部署核对、数据源配置、JVM 参数一直写到常见故障排查用的都是 8.0.47 实际会遇到的情况适合正在维护旧系统、或者被分配了迁移升级任务的开发者和运维。没有空泛的原理全部是可验证的操作。2. 部署前的环境与配置核对lib 目录、server.xml、web.xml 的一张核对清单2.1 环境变量与启动脚本JAVA_HOME 和 CATALINA_HOME 最容易埋雷Tomcat 8.0.47 要求 JDK 7 及以上但我在生产环境见过两种典型问题JAVA_HOME 指向了 JRE或者 CATALINA_HOME 与实际解压目录不一致。前者会导致后续无法用 jstack、jmap 这类排查工具后者会让 Tomcat 找到一套错误的配置文件表象是改了 server.xml 不生效。启动脚本里最常用的检查命令如下# 先确认 Java 版本再确认 Tomcat 是否真的用了这个 Java echo $JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64/bin/java -version # 如果只想临时指定一次不写入全局环境变量 export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 export CATALINA_HOME/opt/apache-tomcat-8.0.47 $CATALINA_HOME/bin/startup.sh我一般会写进 /etc/profile.d/tomcat.sh但注意用source重新加载后再启动避免当前 shell 没拿到新变量导致脚本里实际用的是旧值。这段代码的核心逻辑是启动前用 echo 输出两个变量确认不是空路径因为 Tomcat 的 catalina.sh 对空变量有默认值兜底一旦兜底生效配置就完全不可控。2.2 server.xml 核心标签Executor、Connector、Host 的参数边界8.0.47 的 server.xml 里最值得逐行核对的是连接器和线程池。默认配置是并发能力偏保守的多个 Connector 各用自己的线程池高峰期容易互相抢资源。常见的做法是把线程池抽出来共用Executor nametomcatThreadPool namePrefixcatalina-exec- maxThreads400 minSpareThreads50 prestartminSpareThreadstrue/ Connector executortomcatThreadPool port8080 protocolHTTP/1.1 connectionTimeout20000 acceptCount300 maxKeepAliveRequests100 URIEncodingUTF-8/这段配置里最关键的参数是maxThreads和acceptCount。maxThreads 是最大工作线程数超过这个数后新连接不会立即被拒绝而是排进 acceptCount 的队列。如果两者都设得太小连接会直接被重置表现是浏览器转圈几秒然后报 502。8.0.47 的 BIO 已不建议在压力大的场景使用NIO 是默认选择Connector 的 protocol 按 HTTP/1.1 写就行不需要显式写成 NIO除非你明确要压测对比。Host 标签里有一个常被忽略的属性autoDeploy。开发环境开着方便但生产环境开着只要 webapps 目录里出现新的 war 包或者目录变化Tomcat 就会热部署引发类加载器混乱。我接触的系统里线上一般设置为 false发布时手动触发热部署。2.3 web.xml 中的会话超时与全局配置8.0.47 的 web.xml 位于 conf 目录是全局生效的。会话超时、MIME 映射、欢迎文件列表都在这里。有个容易忽略的细节全局 web.xml 里的 session-config如果你在应用自己的 web.xml 里写了相同配置应用的会覆盖全局的这符合 Servlet 规范的覆盖逻辑不是 Tomcat 例外。建议在应用层面统一写全局只保留默认 MIME 映射避免多个应用互相影响。session-config session-timeout30/session-timeout cookie-config http-onlytrue/http-only securefalse/secure /cookie-config /session-configsession-timeout 单位是分钟生产系统如果做的是后台管理类应用30 分钟是常见值。http-only 建议保留开启能避免部分 XSS 会话劫持问题。secure 标记在只有 HTTP 访问的内网环境不要开否则浏览器会拒绝回传 JSESSIONID用户第一次登录后直接跳回登录页。3. 数据源与连接池实战从 JNDI 配置到数据库连接池参数的取舍3.1 JNDI DataSource 配置模板Resource 标签怎么放到 Context很多旧系统在代码里自己写 JDBC 连接每次访问数据库都新建连接压力稍微一上来数据库就大量 sleep 连接。改造方式是把数据源下沉到 Tomcat 的 JNDI应用通过javax.sql.DataSource获取连接。8.0.47 中可以在 conf/context.xml 直接配置全局数据源也可以放在应用 META-INF/context.xml后者隔离性更好每个应用自己维护。Context Resource namejdbc/orderDB authContainer typejavax.sql.DataSource factoryorg.apache.tomcat.jdbc.pool.DataSourceFactory driverClassNamecom.mysql.jdbc.Driver urljdbc:mysql://192.168.1.10:3306/order?useUnicodetrueamp;characterEncodingUTF-8 usernameapp_user passwordencrypted_password initialSize10 maxActive80 maxIdle20 minIdle5 maxWait10000 validationQuerySELECT 1 testOnBorrowtrue testWhileIdletrue timeBetweenEvictionRunsMillis30000 minEvictableIdleTimeMillis60000 removeAbandonedtrue removeAbandonedTimeout60 logAbandonedtrue/ /Context注意javax.sql.DataSource不要引用错包factory 必须用 Tomcat 自带连接池的工厂类如果用 DBCP 工厂很多参数行为和默认值会不一样。上面配置的核心逻辑是让连接池在启动时创建 10 个连接上限 80当请求超过连接数时最长等待 10 秒。testOnBorrow 是每次取连接前验证是否有效生产环境必须开否则数据库重启后应用拿到的全是死连接。removeAbandoned 表示超过 60 秒没用完的连接会被强制回收配合 logAbandoned 可以定位代码里忘记归还连接的位置但要注意这个机制只对物理连接生效在应用层持有连接做长事务的场景千万别开。3.2 连接池参数怎么按业务定初始大小和最大活跃数的经验值我处理过的系统里最常见的配置事故是 maxActive 设得和数据库 max_connections 一样大。比如 MySQL 默认最大连接数 151你每个 Tomcat 节点 maxActive 设 200两个节点一起跑数据库直接拒绝连接。正确做法是先确认数据库上限再除以节点数留 20% 余量。比如数据库允许 200 个连接2 个 Tomcat 节点每个节点 maxActive 建议在 80 左右。initialSize 不要太大启动时建立太多连接会让应用启动变慢一般在 10 到 20。minIdle 和 maxIdle 控制空闲存活数短连接请求多的系统把 minIdle 调高能明显降低创建连接的延迟但空闲连接本身也会占用数据库资源要根据实际请求频率来不是越大越好。maxWait 这个参数非常关键。如果连接池耗尽应用线程会进入等待。等待时间过长前端请求堆积表现是卡顿和超时等待时间过短数据库压力还没消掉应用直接抛连接获取失败异常。10 秒是相对保守的值如果要更快速失败可以设 3000让上游快速返回错误而不是把所有线程都挂起等待。3.3 Tomcat JDBC Pool 与 DBCP2 的选择8.0.47 里的默认差异8.0.47 默认自带的连接池实现是 Tomcat JDBC Pool和 DBCP2 在参数名上接近但细节行为不同。最大区别是 validationQuery 的处理Tomcat Pool 在配置了 testWhileIdle 时即使不写 validationQuery它也会根据 JDBC4 的 isValid 方法做检测DBCP2 也会这样但老版本 DBCP 在 MySQL 驱动低版本下会偶发假阳性。如果你维护的系统里有历史代码硬编码依赖 DBCP 的某些 SqlLog 类迁移到 Tomcat Pool 时会编译不过这种就沿用 DBCP否则优先用默认的 Tomcat Pool少引入一套依赖库还能减少连接池版本冲突。选择原则是能用默认就不额外加 jar出了问题排查路径短很多这条路我走过太多次了。4. 压力测试与 JVM 调优GC 日志、线程栈和三个参数组合4.1 先从最小 JVM 参数模板开始别把网上的大堆配置直接拷过来JVM 调优是最容易变得“玄学”的环节网上能找到大量大内存参数直接贴到 8.0.47 的 catalina.sh 里新老系统内存管理模型不同反而容易触发严重停顿。推荐的起点模板如下先把日志和基础参数做对再根据压力测试结果调整JAVA_OPTS-server -Xms1g -Xmx2g -Xss512k -XX:MetaspaceSize128m -XX:MaxMetaspaceSize256m \ -XX:UseParallelGC -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/var/log/tomcat \ -Xloggc:/var/log/tomcat/gc.log -XX:PrintGCDetails -XX:PrintGCDateStamps \ -Djava.awt.headlesstrue -Dfile.encodingUTF-8这里有几个关键参数MetaspaceSize 和 MaxMetaspaceSize 在 8.0.47 对应的 JDK 7/8 里很关键如果没设置加载大量 class 的应用会不断触发 Full GC 来调整元空间表现是 CPU 高但业务慢。UseParallelGC 适合多数 Web 应用追求吞吐量的场景如果你的系统对响应时间要求更敏感可以改用 G1但 G1 在 8.0.47 早期版本里稳定性不如 ParallelGC除非你真的用了压测数据证明 G1 更好否则从 ParallelGC 起步更稳。HeapDumpPath 必须指向有足够磁盘空间的位置生产上我见过因为没有这个参数内存溢出后无法拿到 dump 文件导致反复重启的情况。4.2 压测时看三个指标GC 频率、线程状态和堆使用趋势压测时不要只盯着 TPS先把 GC 日志抓到再说。启动后先跑一轮小流量然后看 gc.log# 统计 GC 次数和停顿时间用 awk 按行过滤即可 grep Full GC /var/log/tomcat/gc.log | wc -l # 查看最近一次 Full GC 的停顿时间 grep Full GC /var/log/tomcat/gc.log | tail -5Full GC 次数如果超过每 10 分钟一次说明堆大小或对象生命周期设计有问题。此时用 jstat 观察实时数据更直观# 每 1 秒打印一次堆使用情况共打印 20 次 jstat -gcutil 12345 1000 2012345 是 Tomcat 的 PID可以用 jps 拿到。输出的 S0、S1、E、O 分别代表幸存区、Eden 区、老年代使用率重点关注 O 列的波动趋势。如果老年代使用率稳定在 80% 以上每次 GC 后下降不了多少说明有大对象或者内存泄漏嫌疑下一步要抓堆。如果老年代在每次 Full GC 后能回到 30% 以下说明只是堆容量偏小调大 -Xmx 即可。线程栈抓取是定位线程卡死的手段压测中如果发现请求无响应先抓一遍线程# 抓取线程快照连续抓 3 次间隔 5 秒避免单次快照误判 jstack 12345 /tmp/thread_1.txt sleep 5 jstack 12345 /tmp/thread_2.txt sleep 5 jstack 12345 /tmp/thread_3.txt对比三个文件里同一线程的栈顶如果每次都在相同位置基本可以断定是锁等待或外部资源调用超时。把多次快照的线程状态汇总统计处于 WAITING 状态的线程数有没有持续上升就能判断连接池或线程池参数是否需要调整。4.3 三套参数组合给不同并发量级的系统做参考根据压测结果实际会落到三种参数组合。并发量在 200 以下的中小型管理系统JVM 堆 1g 到 2g线程池 maxThreads 200acceptCount 100GC 用 ParallelGC这是最省心的组合压测时几乎不会出现明显瓶颈。并发量在 500 左右的应用系统堆调到 2g 到 4gmaxThreads 400acceptCount 200此时要特别留意数据库连接池和文件句柄上限单纯调 Tomcat 线程池并不能解决全部问题。并发量超过 1000 的网关类系统先考虑加节点而不是单机调优单机硬扛的代价是频繁 Full GC 和不可预测的响应时间遇到这种规模第一反应应该是横向扩展而不是查参数表。参数调整后必须重新压测做前后对比不要凭感觉。每次只改一个变量比如这次只调 maxThreads下次再改 acceptCount两个一起容易互相掩盖效果出了问题往回查也麻烦。这个习惯帮我避过很多坑。5. 避坑排查五个反复出现的问题与对策5.1 应用重启后偶发 404类加载器冲突现象应用启动日志显示正常但访问某个 Servlet 时偶发 404刷新几次又能通没有固定规律。原因webapps 目录下有历史遗留的旧版本 war 解压目录和当前加载的应用形成了同名不同版本的类加载器叠加。8.0.47 热部署时不会完全清理旧类容易造成同一个路径由两个不同版本类各响应一次。解决关闭autoDeploy后删除 webapps 下所有同名目录重启确保只加载当前要发布的版本。检查CATALINA_BASE/webapps下是否存在带时间戳后缀的临时目录如果有先停服务再清掉。5.2 Tomcat 启动时端口占用netstat 看到的是 TIME_WAIT现象重启时报Port already in use但netstat -anp | grep 8080却看不到 LISTEN 状态的进程。原因端口被占用未必是当前进程可能是上个进程退出后留下的 TIME_WAIT 连接8.0.47 在端口复用参数未开启时遇到大量 TIME_WAIT 可能无法立即绑定。解决先netstat -tlnp | grep 8080看监听进程如果没有 LISTEN再检查ss -s看 TIME_WAIT 数量是否异常。CIPHER 严重时可以等待 60 秒或者用sysctl -w net.ipv4.tcp_tw_reuse1临时加快回收生产环境这个值建议由网络运维统一评估不要自己一改就完事。这条问题我一周内遇到过两次后来总结出规律先查监听再查等待不要一看到端口占用就杀进程容易误杀别的服务。5.3 高负载下 Tomcat 停止响应线程池打满但 CPU 不高现象请求全部超时查看线程栈发现大量线程卡在java.net.SocketInputStream.socketRead0。原因外部接口变慢导致调用线程长时间阻塞Tomcat 工作线程全部被占用新请求进不来。这不是线程池不够大而是下游服务拖住了线程。解决给调用下游服务的代码设置显式超时时间例如 3 秒不要依赖默认的无限等待。同时调低 maxThreads 到 200 左右让快速失败的请求能有机会被处理总比所有线程挂死好。设置超时后压测观察线程状态里 RUNNABLE 的比例是否回升这个方法比单纯加大线程池有效得多。5.4 数据库连接池耗尽后恢复不了连接泄漏的隐藏点现象压测结束后数据库连接数依然保持在高位应用里报获取连接超时。原因代码里使用完的 ResultSet、Statement 没有关闭连接无法归还池中。部分框架封装下连接关闭了但游标没有释放。解决在连接池配置中开启removeAbandoned和logAbandoned前者强制回收后者打印泄漏时的调用栈。找到调用栈后修复对应的 try-with-resources 结构确保 finally 块关闭所有资源。排查时注意 SQL 查询量大时ResultSet 不关闭也会占用物理连接这种问题在日志里往往看不到异常只能靠 logAbandoned 输出定位。5.5 修改 server.xml 后配置不生效从自定义环境脚本查起现象改了端口或参数重启后生效但过一段时间自动变回旧值或者只有部分配置文件加载了修改。原因部署环境存在多个 CATALINA_BASE 目录启动脚本没有显式指定Tomcat 有时会去读取默认路径下的配置文件同时克隆虚拟机模板里的 Tomcat 目录可能保留了旧配置。解决在 catalina.sh 顶部强制指定CATALINA_BASE为实际使用的目录并在 server.xml 开头用注释标记修改时间和原因。排查时先ps aux | grep tomcat看实际启动的是哪个目录下的脚本再用ls -l /proc/[pid]/cwd确认当前工作目录这两个命令能快速定位是不是读错了配置文件。6. 验证技巧十分钟检查实例健康与隐藏风险配置和调优做完了最后一步不是直接放量而是快速验证。我习惯在每轮调整后用一组命令做体检不依赖监控平台能够快速确认核心指标。第一个动作看整个 JVM 的堆情况。用 jmap 输出堆直方图能直观看到有哪些对象占用了大量内存。历史系统常见的是 char[] 和 byte[] 占比过高这通常是字符串操作或者文件读取没有释放缓冲区。如果始终看到某个自定义业务对象排在列表前面说明内存泄漏点就在这些对象的创建路径上。jmap -histo:live 12345 | head -30第二个动作检查线程池实际大小和执行状态。不需要看配置值直接看运行值。用 jstack 抓一次线程快照统计http-nio-8080-exec-开头的线程数量如果运行中的线程数经常达到配置的 maxThreads说明请求吞吐已经接近边界。再看这些线程的栈顶如果大量出现在 JDBC 调用相关代码应当优先查数据库连接池的连接获取时间而不是盲目扩容。jstack 12345 | grep http-nio-8080-exec | wc -l第三个动作确认 GC 日志里的一个关键指标Full GC 后的老年代使用率趋势。连续对比三轮压测的 gc.log如果每轮 Full GC 后老年代基线不断上移说明有稳定的内存累积可能不是泄漏但是存在缓存未清理的问题。确认方式是定时抓两次堆直方图对比如果某个类对象数量持续增加顺着它的引用链找持有者一般都能定位到缓存的 key 设计问题或静态集合的清理缺失。grep Full GC gc.log | awk {print $NF} | tail -10最后用一个综合命令一次性输出关键状态的快照。包括进程存活、端口监听、JVM 堆使用率和线程数适合调整完参数后快速确认服务状态没有偏离基线。# 按需组合输出一行状态信息 echo PID$(ps -ef | grep [o]rg.apache.catalina.startup.Bootstrap | awk {print $2}) \ PORT$(ss -tlnp | grep 8080 | wc -l) \ THREADS$(ps -L -p $(pgrep -f org.apache.catalina.startup.Bootstrap) 2/dev/null | wc -l)我把这一整套方法固化成了一个固定的发布检查脚本每次改完配置、升级完依赖都要强制走一遍这些命令才允许提交上线。Tomcat 8.0.47 的调试之所以让人头疼是因为问题往往不在单个配置项上而是多个参数互相影响比如线程池配置相对保守时连接池的 maxWait 偏长会导致线程被无效挂起堆直方图里又会显示出大量等待中的线程对象看起来像内存问题实质还是线程问题。这种干扰项在排查时很容易分散注意力所以我的习惯是每次只改一个维度其余保持基线不变验证完再动下一个。这套做法帮助我减少了很多重复排障的时间也希望帮到你。本文还有配套的精品资源点击获取