文章出處

問題定位及排查

上周無意中調試程序在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釋放資源即可。


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 AutoPoster 的頭像
    AutoPoster

    互聯網 - 大數據

    AutoPoster 發表在 痞客邦 留言(0) 人氣()