持續集成環境--Tomcat熱部署導致線程泄漏


一、問題由來

我們組用jenkins部署了持續集成環境,(jenkins部署war包到遠程服務器的tomcat)。

每次提交了代碼,jenkins上一鍵構建,就可以自動拉取最新代碼,打war包,熱部署到遠程環境上的tomcat。

一切都很好,只是一次用jconsole偶然連上去一看,遠程環境上的tomcat上,線程數竟多達700多個。。。

 

二、排查代碼

查看線程堆棧,幾百個線程中,線程名為“UserService-InformImAndCcm”打頭的,多達130+,但是在代碼中,只搜到一處線程池配置:

 

一個qq群里,有人說我們的參數配錯了,我一度動搖了,但后來還是覺得不對,我理解的線程池就是:

超過核心線程數后,仍然有task,就丟隊列,如果隊列滿了,就繼續開線程,直到達到maximumPoolSize,如果后續隊列再滿了,則拒絕任務。

也就是說,線程不可能超過maximumPoolSize。

。。。

后來任務一多,忘了。今天又想起來,做個測試,因為我感覺,這事,可能和熱部署有關系。

 

三、本地測試--多次熱部署同一應用

1、本地環境配置

很簡單,一個war包,兩個tomcat自帶的war包,用來控制reload應用。

 

配置好了后,啟動tomcat

 

2、打開jconsole進行監控

主要是監控線程。

 

3、reload應用一次

打開localhost:9080/manager/html,如果不能訪問,請在tomcat下面的conf中的tomcat-users.xml配置:

    <role rolename="manager-gui"/>
    <user username="admin" password="admin" roles="manager-gui"/>

 

 

 4、觀察jconsole中的線程數是否增加

 5、反復重試前面3-4步

如果不出意外(程序中有線程泄漏)的話,jconsole中的線程圖應該是下面這樣,一步一個台階:

 

 6、查看tomcat下logs中的catalina.log

這里面可能會有些線程泄漏的警告,如下:

 

四、問題出現的原因

Tomcat熱部署的實現機制,暫時沒有研究。

不過根據在catalina.log日志中出現的:

26-Dec-2018 13:06:24.920 信息 [http-nio-9081-exec-34] org.apache.catalina.core.StandardContext.reload Reloading Context with name [/CAD_WebService] is completed

在idea中通過如下騷操作:

找到了關聯的源碼:

 

進入該Servlet的reload:

protected void reload(PrintWriter writer, ContextName cn,
            StringManager smClient) {

        try {
            Context context = (Context) host.findChild(cn.getName());
            。。。。。。刪除無關代碼
 context.reload();         }

    }

 

這里的context,實現類是org.apache.catalina.core.StandardContext,該類的reload方法:

public synchronized void reload() {

        setPaused(true);

        try {
            stop();
        } catch (LifecycleException e) {
        }
       。。。刪除無關代碼
        try {
            start();
        } catch (LifecycleException e) {
        }

        setPaused(false);

        if(log.isInfoEnabled())
            log.info(sm.getString("standardContext.reloadingCompleted",
                    getName()));

    }

 

StandardContext類,未實現自己的stop,因此調用了基類org.apache.catalina.util.LifecycleBase#stop:

public final synchronized void stop() throws LifecycleException {

            stopInternal();  //無關代碼已刪除
 }

在org.apache.catalina.core.StandardContext中,重寫了stopInternal:

    protected synchronized void stopInternal()  {

        try {
            // Stop our child containers, if any
            final Container[] children = findChildren();

            for (int i = 0; i < children.length; i++) {
                children[i].stop();
            }
    }

在這里,會查找當前對象(當前對象代表我們要reload的context,即一個應用),這里查找它下面的子container,那就是會查找到各servlet的wrapper。

然后調用這些servlet wrapper的stop。

wrapper的標准實現為:org.apache.catalina.core.StandardWrapper。其stopInternal如下:

protected synchronized void stopInternal() throws LifecycleException {
        // Shut down our servlet instance (if it has been initialized)
        try {
            unload();
        } catch (ServletException e) {
            getServletContext().log(sm.getString
                      ("standardWrapper.unloadException", getName()), e);
        }

    }

這里准備在unload中,關閉servlet。

org.apache.catalina.core.StandardWrapper#unload:

 
         
protected volatile Servlet instance = null;

public
synchronized void unload() throws ServletException { // Nothing to do if we have never loaded the instance if (!singleThreadModel && (instance == null)) return; unloading = true; // Call the servlet destroy() method try { instance.destroy(); } // Deregister the destroyed instance instance = null; instanceInitialized = false; }

從上看出,這里開始調用servlet的destroy方法了。

 

spring應用的servlet,想必大家都很熟了,org.springframework.web.servlet.DispatcherServlet。

它的destroy方法由父類org.springframework.web.servlet.FrameworkServlet實現,#destroy:

    public void destroy() {
        getServletContext().log("Destroying Spring FrameworkServlet '" + getServletName() + "'");
        // Only call close() on WebApplicationContext if locally managed...
        if (this.webApplicationContext instanceof ConfigurableApplicationContext && !this.webApplicationContextInjected) {
            ((ConfigurableApplicationContext) this.webApplicationContext).close();
        }
    }

 

這里,主要是針對spring 容器進行關閉,比如各種bean的close方法等等。

實現在這里,org.springframework.context.support.AbstractApplicationContext#doClose:

protected void doClose() {
        if (this.active.get() && this.closed.compareAndSet(false, true)) {

            LiveBeansView.unregisterApplicationContext(this);

            try {
                // Publish shutdown event.
                publishEvent(new ContextClosedEvent(this));
            }
            catch (Throwable ex) {
                logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
            }

            // Stop all Lifecycle beans, to avoid delays during individual destruction.
            getLifecycleProcessor().onClose();// Destroy all cached singletons in the context's BeanFactory.
            destroyBeans();

            // Close the state of this context itself.
            closeBeanFactory();

            // Let subclasses do some final clean-up if they wish...
            onClose();

            this.active.set(false);
        }
    }

 

問題分析到現在,我們可以發現,針對spring bean中的線程池,是沒有地方去關閉線程池的。

所以,每次reload,在stop的過程中,線程池都沒得到關閉,於是造成了線程泄漏。

 

五、解決辦法

1:網上的解決辦法是說:實現一個javax.servlet.ServletContextListener,實現其jcontextDestroyed方法,然后注冊到servlet中。

 

2:我這邊覺得,按照上面的分析,直接在關閉bean的時候,關閉線程池也可以:

針對,spring應用,在bean中,如果有線程池實例變量的話,讓bean實現org.springframework.beans.factory.DisposableBean接口:

    @Override
    public void destroy() throws Exception {
        logger.info("about to shutdown thread pool");
        pool.shutdownNow();
    }

 

 不過說實話,上面的兩種方案我都試了,不起作用。明天弄個純凈的工程試下吧,目前的project里代碼太雜。

 

2019-02-11日更新:

針對上面的第二種方法,調用線程池的shutdownNow,會循環給池里的線程調用該線程的interrupt方法。

interrupt方法,是否有效果,這個只能取決於具體的線程的run方法實現。

比如看下面我們當時線程的實現就是有問題的:

 

查看blockingqueue的take方法:

 

但我們的線程實現里,捕獲了異常,繼續無限循環。。。(這個是歷史代碼。。。哎)

所以,正確的做法是,要保證線程在被interrupt后,可以正常結束。

處理方式有幾種:

參考https://www.ibm.com/developerworks/cn/java/j-jtp05236.html

1、不捕捉 InterruptedException,將它傳播給調用者

 

2、捕獲后重新拋出

 

3、在runable中,無法拋出時,捕獲后,重新設置中斷,讓調用方可以感知

 

4、最不建議的方式:吞了異常;或者只打個日志。

 

 如果大家有什么想法,歡迎和我交流

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM