Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 341|回复: 0

Tomcat shutdown执行后无法退出进程问题排查及解决

[复制链接]
  • TA的每日心情
    奋斗
    前天 14:56
  • 签到天数: 778 天

    [LV.10]以坛为家III

    2047

    主题

    2105

    帖子

    71万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    715238
    发表于 2021-5-6 17:59:34 | 显示全部楼层 |阅读模式

    问题定位及排查

    上周无意中调试程序在Linux上ps -ef|grep tomcat发现有许多tomcat的进程,当时因为没有影响系统运行就没当回事。而且我内心总觉得这可能是tomcat像nginx一样启动多个进程。

    后来测试在一次升级后反馈说怎么现在tomcat进程无法shutdown?这让我有点意外,看来这个问题并没有这么简单。于是开始思考问题会出在哪里。

    复现问题

    先是另外一台服务器部署,然后shutdown后再ps进程是空的,这说明tomcat不会自动产生新的进程。那就有可能系统代码出了什么问题吧?最近另一个位同事有比较多的修改,可能是因为这些修改吧。光猜想也找不到问题,只好用jvisuale来看一下系统的dump,发现shutdown之后进程没有退出,而且里面有许多线程还在运行,有些还是线程池。

    看来是有线程没有释放导致的泄露吧?于是用tail命令打开catalina.out查看最后shutdown.sh,在控制台输出了下面这些内容:

    Nov 28, 2016 10:41:08 AM org.apache.catalina.loader.WebappClassLoader clearReferencesThreads
    SEVERE: The web application [/] appears to have started a thread named [Component socket reader] but has failed to stop it. This is very likely to create a memory leak.
    

    确实有许多的线程没有关闭,在关闭时还提示了泄漏。从这些线程的名字可以确认了,是这近新增了一个openfire的whack外部组件导致的。这个whack可以连接到openfire服务器,实现一套扩展组件服务的功能,我们主要用来发送IM消息。这样做的好处是开启线程数少,效率高,并发性能很不错。

    查看代码

    先看一下ExternalComponentManager的实现,因为它是用来外部扩展组件的管理者,我们的操作基本是根据它来完成的。

    下面的代码便是是创建一个ExternalComponentManager,并且设置参数同时连接到服务器。

    private void CreateMessageSender() {
    	manager = new ExternalComponentManager(configHelper.getOpenfireHost(),
    			configHelper.getOpenfireExternalCompPort());
    	manager.setSecretKey(SENDER_NAME, configHelper.getOpenfirePwd());
    	manager.setMultipleAllowed(SENDER_NAME, true);
    	try {
    		msc = new MessageSenderComponent("senderComponent", manager.getServerName());
    		manager.addComponent(SENDER_NAME, msc);
    	} catch (ComponentException e) {
    		logger.error("CreateMessageSender error.", e);
    	}
    }
    

    那么最重要的是在哪里启动了线程?毕竟最终影响系统的是线程没有关闭。所以沿着addComponent这调用看看吧:

    public void addComponent(String subdomain, Component component, Integer port) throws ComponentException {
        if (componentsByDomain.containsKey(subdomain)) {
            if (componentsByDomain.get(subdomain).getComponent() == component) {
                // Do nothing since the component has already been registered
                return;
            }
            else {
                throw new IllegalArgumentException("Subdomain already in use by another component");
            }
        }
        // Create a wrapping ExternalComponent on the component
        ExternalComponent externalComponent = new ExternalComponent(component, this);
        try {
            // Register the new component
            componentsByDomain.put(subdomain, externalComponent);
            components.put(component, externalComponent);
            // Ask the ExternalComponent to connect with the remote server
            externalComponent.connect(host, port, subdomain);
            // Initialize the component
            JID componentJID = new JID(null, externalComponent.getDomain(), null);
            externalComponent.initialize(componentJID, this);
        }
        catch (ComponentException e) {
            // Unregister the new component
            componentsByDomain.remove(subdomain);
            components.remove(component);
            // Re-throw the exception
            throw e;
        }
        // Ask the external component to start processing incoming packets
        externalComponent.start();
    }
    
    

    代码也比较简单,就是创建了一个wapper类ExternalComponent将我们自己的Component包装了一下。其中最为重要的是最后一句:externalComponent.start();

    public void start() {
        // Everything went fine so start reading packets from the server
        readerThread = new SocketReadThread(this, reader);
        readerThread.setDaemon(true);
        readerThread.start();
        // Notify the component that it will be notified of new received packets
        component.start();
    }
    

    原来这里启动了一个读取线程,用于接收Openfire服务器发来的数据流。查看线程构造函数:

    public SocketReadThread(ExternalComponent component, XPPPacketReader reader) {
        super("Component socket reader");
        this.component = component;
        this.reader = reader;
    }
    

    可以看到,这个线程的名字是“Component socket reader”,在前面的日志里确实有这个线程。

    解决问题

    那么接下来的主要问题是如何关闭这个SocketReadThread,按理说会有相应的实现,发现externalComponent.start()这个方法有名字叫star,那么是不是有与其匹配的方法呢?确实有的一个shutdown的方法:

    public void shutdown() {
        shutdown = true;
        // Notify the component to shutdown
        component.shutdown();
        disconnect();
    }
    
    

    原来这里调用了component.shutdown();最后还调用了一个disconnect,继续看代码:

    private void disconnect() {
        if (readerThread != null) {
            readerThread.shutdown();
        }
        threadPool.shutdown();
        TaskEngine.getInstance().cancelScheduledTask(keepAliveTask);
        TaskEngine.getInstance().cancelScheduledTask(timeoutTask);
        if (socket != null && !socket.isClosed()) {
            try {
                synchronized (writer) {
                    try {
                        writer.write("</stream:stream>");
                        xmlSerializer.flush();
                    }
                    catch (IOException e) {
                        // Do nothing
                    }
                }
            }
            catch (Exception e) {
                // Do nothing
            }
            try {
                socket.close();
            }
            catch (Exception e) {
                manager.getLog().error(e);
            }
        }
    }
    
    

    发现这里就有了线程shutdown的调用,OK,说明就是它了。

    因为最外层代码使用的是ExternalComponentManager,那么在ExternalComponentManager中调用了ExternalComponent shutdown的方法是removeComponent,那么就是它了。

    也就是说只要在最后应用关闭时调用removeComponent方法就可以释放线程资源。这里当然就可以借助ServletContextListener来完成咯。

    public class MessageSenderServletContextListener implements ServletContextListener{
    	private final static Logger logger = LoggerFactory
    			.getLogger(MessageSenderServletContextListener.class);
    
    	@Override
    	public void contextInitialized(ServletContextEvent sce) {
    		logger.debug("contextInitialized is run.");
    	}
    
    	@Override
    	public void contextDestroyed(ServletContextEvent sce) {
    		logger.debug("contextDestroyed is run.");
    		MessageSender msgSender = SpringUtil.getBean(MessageSender.class);
    		try {
    			msgSender.shutdown();
    			logger.debug("MessageSender is shutdown.");
    		} catch (ComponentException e) {
    			logger.error(e.getMessage());
    		}
    	}
    
    }
    

    实现contextDestroyed方法,从spring中获得MessageSender类,调用shutdown释放资源即可。

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-8-19 09:45 , Processed in 0.066318 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表