一、問題由來
我們組用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、最不建議的方式:吞了異常;或者只打個日志。
如果大家有什么想法,歡迎和我交流