一次生产环境 Tomcat 7 + JDK 7 应用启动失败的完整排查与修复实录

文章目录

  • [一次生产环境 Tomcat 7 + JDK 7 应用启动失败的完整排查与修复实录](#一次生产环境 Tomcat 7 + JDK 7 应用启动失败的完整排查与修复实录)

一次生产环境 Tomcat 7 + JDK 7 应用启动失败的完整排查与修复实录

环境 :CentOS 7 + Tomcat 7.0.96 + JDK 1.7.0_80 + Ehcache 2.6 + Memcached
应用 :Spring MVC 项目 /dhmall,含 Quartz 定时任务、Druid 数据源、RabbitMQ、银联支付 SDK
问题现象 :Tomcat 启动后立即崩溃,报 Address already in use,伴随大量内存泄漏警告

本文将完整复盘从日志分析、端口冲突解决到资源泄漏修复的全过程,为同类老旧 Java 系统提供排错参考。


一、问题初现:看似成功,实则崩溃

首次查看日志时,被这行迷惑:

log 复制代码
INFO: Server startup in 23868 ms

但紧接着出现致命错误:

log 复制代码
SEVERE: StandardServer.await: create[localhost:8088]: 
java.net.BindException: Address already in use

随后 Tomcat 自动进入 shutdown 流程,并暴露出一系列严重警告:

log 复制代码
SEVERE: The web application [] registered the JDBC driver [...] but failed to unregister it
SEVERE: The web application [] appears to have started a thread named [schdulerFactory_Worker-1]...

结论 :这不是简单的"端口占用",而是 残留进程 + 资源泄漏 的复合型故障。


二、第一步:解决端口冲突(表面问题)

🔍 定位占用进程

bash 复制代码
# 查找占用 8088 或 8005 端口的进程
lsof -i :8088
lsof -i :8005

# 输出示例
java    12345 user ... /apache-tomcat-7.0.96/...

🛠️ 强制清理

bash 复制代码
kill -9 12345
# 再次检查确保无残留
netstat -tulnp | grep -E '8088|8005'

💡 经验:在频繁部署或异常断电后,Tomcat 进程常"假死"残留,务必手动清理。

✅ 验证重启

bash 复制代码
/data/soft/apache-tomcat-7.0.96/bin/startup.sh
tail -f logs/catalina.out

此时 Tomcat 成功启动,HTTP 服务可访问。但隐患仍在


三、第二步:修复内存泄漏(根本问题)

尽管应用能运行,但每次重启都会留下"垃圾",长期将导致 Metaspace OOM。日志中三大泄漏点:

1. JDBC 驱动未注销

log 复制代码
SEVERE: ... registered the JDBC driver [com.alibaba.druid.proxy.DruidDriver] but failed to unregister

2. MySQL 后台线程未停

log 复制代码
SEVERE: ... thread named [MySQL Statement Cancellation Timer] ...

3. Quartz 工作线程泄漏(最严重)

log 复制代码
SEVERE: ... thread named [schdulerFactory_Worker-1] ... memory leak

🛠️ 解决方案:添加全局销毁监听器

创建 AppCleanupListener.java

java 复制代码
package com.dhmall.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import net.sf.ehcache.CacheManager;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

public class AppCleanupListener implements ServletContextListener {

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(sce.getServletContext());
        
        // 1. 关闭 Quartz 调度器
        try {
            Scheduler scheduler = (Scheduler) ctx.getBean("schdulerFactory");
            if (scheduler != null && !scheduler.isShutdown()) {
                scheduler.shutdown(true); // 等待任务完成
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 2. 关闭 Ehcache(如有集群,会自动注销 RMI)
        try {
            CacheManager.getInstance().shutdown();
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 3. (可选)显式注销 JDBC 驱动(新版通常自动处理)
        // DriverManager.deregisterDriver(...);
    }

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        // 初始化逻辑(如设置 RMI hostname)
        System.setProperty("java.rmi.server.hostname", "172.26.100.204");
    }
}

web.xml 中注册:

xml 复制代码
<listener>
    <listener-class>com.dhmall.listener.AppCleanupListener</listener-class>
</listener>

效果:应用停止时,Quartz 线程池优雅关闭,Ehcache 断开 RMI 连接,JDBC 驱动被清理。


四、第三步:修复 Ehcache RMI 集群配置

原配置存在致命错误:

xml 复制代码
<cacheManagerPeerListenerFactory
    properties="hostName=localhost, port=40001, ..." />

hostName=localhost 导致其他节点无法连接!

✅ 正确做法

  1. 移除 hostName 配置
  2. 通过 JVM 参数指定对外 IP

tomcat/bin/setenv.sh(若无则新建)中添加:

bash 复制代码
export CATALINA_OPTS="$CATALINA_OPTS -Djava.rmi.server.hostname=172.26.1.2"

ehcache.xml 简化为:

xml 复制代码
<cacheManagerPeerListenerFactory
    class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
    properties="port=40001, socketTimeoutMillis=2000" />

🔒 开放防火墙

bash 复制代码
firewall-cmd --permanent --add-port=40001/tcp
firewall-cmd --reload

五、其他优化项

1. 解决 SLF4J 多绑定冲突

日志中:

log 复制代码
SLF4J: Class path contains multiple SLF4J bindings.

pom.xml 中排除冗余绑定:

xml 复制代码
<exclusion>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
</exclusion>
<exclusion>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
</exclusion>

2. 移除 WEB-INF/lib 中的 servlet-api.jar

log 复制代码
validateJarFile(.../servlet-api-2.5.jar) - jar not loaded.

该 JAR 应由 Tomcat 提供,应用中不应包含。


六、最终验证

  1. 正常启动 :无 BindExceptionServer startup in XXX ms 后持续运行
  2. 无泄漏警告 :重启时不再出现 SEVERE: ... memory leak
  3. 集群同步:多节点环境下缓存更新实时生效
  4. 外部可访问curl http://服务器IP:8088/dhmall/xxx 返回正常

七、经验总结

问题类型 教训
端口冲突 永远先查 lsof,不要被"startup"迷惑
线程泄漏 所有后台线程(Quartz/Ehcache/Timer)必须显式关闭
RMI 配置 hostName=localhost 是集群最大陷阱
老旧技术栈 JDK 7 + Tomcat 7 已 EOL,建议尽快升级

最后忠告:对于仍在使用 JDK 7/Tomcat 7 的系统,请务必建立完善的启停脚本和健康检查机制。技术债终需偿还,但在此之前,我们仍要守护好每一行 legacy code 的稳定运行。

相关推荐
七夜zippoe3 小时前
JVM类加载机制(Class Loading)详解:双亲委派模型与破坏实践
java·开发语言·jvm·类加载·双亲委派
黄昏恋慕黎明4 小时前
spring MVC了解
java·后端·spring·mvc
-Xie-5 小时前
Redis(八)——多线程与单线程
java·数据库·redis
Kuo-Teng5 小时前
LeetCode 279: Perfect Squares
java·数据结构·算法·leetcode·职场和发展
Filotimo_6 小时前
SpringBoot3整合Druid数据源
java·spring boot
百锦再6 小时前
第18章 高级特征
android·java·开发语言·后端·python·rust·django
乄bluefox6 小时前
Reactor 中的 doOnError 与 doOnCancel
java·reactor·rea
CoderYanger6 小时前
B.双指针——3194. 最小元素和最大元素的最小平均值
java·开发语言·数据结构·算法·leetcode·职场和发展·1024程序员节
程序猿20236 小时前
项目结构深度解析:理解Spring Boot项目的标准布局和约定
java·spring boot·后端