持续集成环境--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、最不建议的方式:吞了异常;或者只打个日志。
如果大家有什么想法,欢迎和我交流
持续集成环境--Tomcat热部署导致线程泄漏的更多相关文章
- JavaWeb+SVN+Maven+Tomcat +jenkins搭建持续集成环境和自动部署
https://blog.csdn.net/wh52788/article/details/80900477 https://blog.csdn.net/liyong1028826685/articl ...
- [Docker][ansible-playbook]3 持续集成环境之分布式部署
预计阅读时间: 30分钟 本期解决痛点如下:1. 代码版本的多样性,编译环境的多样性如何解决?答案是使用docker,将不同的编译环境images统统打包到私有仓库上,根据需求进行下载,从宿主机上挂载 ...
- [ansible-playbook]4 持续集成环境之分布式部署利器 ansible playbook学习
3 ansible-play讲的中太少了,今天稍微深入学习一点 预计阅读时间:15分钟 一: 安装部署 参考 http://getansible.com/begin/an_zhuang_ansile ...
- Tomcat热部署,Web工程中线程没有终止
近期项目中,用 jenkins 热部署 web工程时,发现工程中静态持有的线程(将ScheduledExecutorService定时任务存储在静态Map中),导致不定时出现数据库访问事务关闭异常,如 ...
- 使用Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境(一)
前言 但凡一个略有规模的项目都需要一个持续集成环境的支撑,为什么需要持续集成环境,我们来看一个例子.假如一个项目,由A.B两位程序员来协作开发,A负责前端模块,B负责后端模块,前端依赖后端.A ...
- 使用Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境
前言 但凡一个略有规模的项目都需要一个持续集成环境的支撑,为什么需要持续集成环境,我们来看一个例子.假如一个项目,由A.B两位程序员来协作开发,A负责前端模块,B负责后端模块,前端依赖后端.A和B都习 ...
- Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境
使用Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境(一) 2015-01-14 20:28 by 飘扬的红领巾, 4322 阅读, 5 评论, 收藏, 编辑 ...
- 使用Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境(二)
前言 上一篇随笔Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境(一)介绍maven和nexus的环境搭建,以及如何使用maven和nexus统一管理库 ...
- Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境(二)
上一篇随笔Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境(一)介绍maven和nexus的环境搭建,以及如何使用maven和nexus统一管理库文件和版本,以 ...
随机推荐
- 一款CSS3仿Google Play的垂直菜单
之前分享过一款非常酷的CSS3垂直下拉动画菜单,是多级菜单.今天我们来看一款也是用CSS3制作的垂直菜单,是仿Google Play的菜单,菜单项都带有可爱的小图标,可以先来看看效果图: 当然你可以在 ...
- Nginx + Tomcat 反向代理 如何在高效的在一台服务器部署多个站点
上一篇分享了 Nginx + Tomcat 反向代理 负载均衡 集群 部署指南,感觉还是相当实用型的,但是一般集群部署是基于大访问量的,可能有的企业用不到,类似一些企业官网,访问量并不是很大,基于这个 ...
- u3d中 rect[2] == rt->GetGLWidth() && rect[3] == rt->GetGLHeight()错误的原因及解决方法
原文:http://blog.csdn.net/wolf96/article/details/38363161 官方是这么解释的 http://issuetracker.unity3d.com/iss ...
- Mongodb学习笔记(2)--修改器
修改器 利用原子的更新修改器,可以使得这种部分更新极为高效,更新修改器是一种特殊的键,用来指定复杂的更新操作,比如调整,增加或删除,还可以操作数组或内嵌文档. $inc $inc修改器用来增加已有键的 ...
- 设置回车的默认按钮detectEnter
场景: 页面有一个搜索文本框和搜索按钮.正常情况下,当我在搜索文本框输入关键字后按回车键就可以触发搜索按钮进行内容搜索,但由于页面上还有其它按钮,而且默认不是搜索按钮,怎样才能实现回车就触发我们的搜索 ...
- 3. beeGo 自己写Controller 和 请求数据处理
Controller Controller等同于Django里的view,处理逻辑都是在Controller里面完成的,下面就写一个最简单的Controller. 我们在写自己的controller的 ...
- android LinearLayout添加分隔线
方法一: 可以放置一个ImageView组件,然后将其设为分隔线的颜色或图形. 分隔线View的定义代码如下: [html] view plaincopy <ImageView androi ...
- 【搞笑签名】390个qq个性昵称或签名,周末前娱乐一下
1 来瓶82年的矿泉水 2 名不正则言承旭 3 天涯何处无芳草,还是母乳喂养好 4 她的妈妈不爱我 5 你丫的 6 农夫三拳 7 猪嚼在恋√痛 8 马驴脸猛鹿 9 小白兔兽性大发 10 曰捣一乱 11 ...
- Hash冲突的解决方法
虽然我们不希望发生冲突,但实际上发生冲突的可能性仍是存在的.当关键字值域远大于哈希表的长度,而且事先并不知道关键字的具体取值时.冲突就难免会发 生.另外,当关键字的实际取值大于哈希表的长度时,而且表中 ...
- linux nginx svn 更新前端代码
1.进入项目前端代码目录中 root@TServer:~# cd /home/p/web/gongti/ 2.更新svn上最新的代码版本 root@TServer:/home/p/web/gongti ...