文档章节

分析Tomcat类加载机制触发的Too many open files问题

 时间财富网
发布于 2016/11/24 09:49
字数 3501
阅读 37
收藏 0
点赞 0
评论 1

  分析Tomcat类加载机制触发的Too many open files问题

  Too many open files意思是打开文件太多了那么碰到Tomcat类加载机制触发的Too many open files问题要如何来处理,我们这边一起来看看细节吧。

  说起Too many open files这个报错,想必大家一定不陌生。在Linux系统下,如果程序打开文件句柄数(包括网络连接、本地文件等)超出系统设置,就会抛出这个错误。

  不过最近发现Tomcat的类加载机制在某些情况下也会触发这个问题。今天就来分享下问题的排查过程、问题产生的原因以及后续优化的一些措施。

  在正式分享之前,先简单介绍下背景。

  Apollo配置中心是携程框架研发部(笔者供职部门)推出的配置管理平台,提供了配置中心化管理、配置修改后实时推送等功能。

  有一个JavaWeb应用接入了Apollo配置中心,所以用户在配置中心修改完配置后,配置中心就会实时地把最新的配置推送给该应用。

  一、故障现象

  某天,开发在生产环境照常通过配置中心修改了应用配置后,发现应用开始大量报错。

  查看日志,发现原来是redis无法获取到连接了,所以导致接口大量报错。

  Caused by: redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool

  at redis.clients.util.Pool.getResource(Pool.java:50)

  at redis.clients.jedis.JedisPool.getResource(JedisPool.java:99)

  at credis.java.client.entity.RedisServer.getJedisClient(RedisServer.java:219)

  at credis.java.client.util.ServerHelper.execute(ServerHelper.java:34)

  … 40 more

  Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketException: Too many open files

  at redis.clients.jedis.Connection.connect(Connection.java:164)

  at redis.clients.jedis.BinaryClient.connect(BinaryClient.java:82)

  at redis.clients.jedis.BinaryJedis.connect(BinaryJedis.java:1641)

  at redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:85)

  at org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:868)

  at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:435)

  at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:363)

  at redis.clients.util.Pool.getResource(Pool.java:48)

  … 43 more

  Caused by: java.net.SocketException: Too many open files

  at java.net.Socket.createImpl(Socket.java:447)

  at java.net.Socket.getImpl(Socket.java:510)

  at java.net.Socket.setReuseAddress(Socket.java:1396)

  at redis.clients.jedis.Connection.connect(Connection.java:148)

  … 50 more

  由于该应用是基础服务,有很多上层应用依赖它,所以导致了一系列连锁反应。情急之下,只能把所有机器上的Tomcat都重启了一遍,故障恢复。

  二、初步分析

  由于故障发生的时间和配置中心修改配置十分吻合,所以后来立马联系我们来一起帮忙排查问题(配置中心是我们维护的)。不过我们得到通知时,故障已经恢复,应用也已经重启,所以没了现场。只好依赖于日志和CAT(实时应用监控平台)来尝试找到一些线索。

  从CAT监控上看,该应用集群共20台机器,不过在配置客户端获取到最新配置,准备通知应用该次配置的变化详情时,只有5台通知成功, 15 台通知失败。

  其中 15 台通知失败机器的 JVM 似乎有些问题,报了无法加载类的错误(NoClassDefFoundError),错误信息被catch住并记录到了CAT。

  5 台成功的信息如下:

  15 台失败的如下:

  报错详情如下:

  java.lang.NoClassDefFoundError: com/ctrip/framework/apollo/model/ConfigChange

  …

  Caused by: java.lang.ClassNotFoundException: com.ctrip.framework.apollo.model.ConfigChange

  at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1718)

  at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1569)

  … 16 more

  配置客户端在配置更新后,会计算配置的变化并通知到应用。配置的变化会通过 ConfigChange 对象存储。

  由于是该应用启动后第一次配置变化,所以 ConfigChange 类是第一次使用到,基于 JVM 的懒加载机制,这时会触发一次类加载过程。

  这里就有一个疑问来了,为啥 JVM 会无法加载类?这个类com.ctrip.framework.apollo.model.ConfigChange 和配置客户端其它的类是打在同一个 jar 包里的,不应该出现 NoClassDefFoundError 的情况。

  而且,碰巧的是,后续redis报无法连接错误的也正是这 15 台报了 NoClassDefFoundError 的机器。

  联想到前面的报错Too many open files, 会不会也是由于文件句柄数不够,所以导致JVM无法从文件系统读取jar包,从而导致 NoClassDefFoundError?

  三、故障原因

  关于该应用出现的问题,种种迹象表明那个时段应该是进程句柄数不够引起的,例如无法从本地加载文件,无法建立 redis 连接,无法发起网络请求等等。

  前一阵我们的一个应用也出现了这个问题,当时发现老机器的 Max Open Files 设置是 65536 ,但是一批新机器上的 Max OpenFiles 都被误设置为 4096 了。

  虽然后来运维帮忙统一修复了这个设置问题,但是需要重启才会生效。所以目前生产环境还有相当一部分机器的 Max Open Files 是 4096 。

  所以,我们登陆到其中一台出问题的机器上去查看是否存在这个问题。但是出问题的应用已经重启,无法获取到应用当时的情况。不过好在该机器上还部署了另外的应用, pid 为 16112 。通过查看 /proc/16112/limits 文件,发现里面的 Max Open Files 是 4096 。

  所以有理由相信应用出问题的时候,它的 Max Open Files 也是 4096 ,一旦当时的句柄数达到 4096 的话,就会导致后续所有的 IO 都出现问题。

  所以故障原因算是找到了,是由于 Max Open Files 的设置太小,一旦进程打开的文件句柄数达到 4096 ,后续的所有请求(文件 IO ,网络 IO )都会失败。

  由于该应用依赖了 redis ,所以一旦一段时间内无法连接 redis ,就会导致请求大量超时,造成请求堆积,进入恶性循环。(好在 SOA 框架有熔断和限流机制,所以问题影响只持续了几分钟)

  四、疑团重重

  故障原因算是找到了,各方似乎对这个答案还算满意。不过还是有一个疑问一直在心头萦绕,为啥故障发生时间这么凑巧,就发生在用户通过配置中心发布配置后?

  为啥在配置发布前,系统打开的文件句柄还小于 4096 ,在配置发布后就超过了?

  难道配置客户端在配置发布后会大量打开文件句柄?

  4.1、代码分析

  通过对配置客户端的代码分析,在配置中心推送配置后,客户端做了以下几件事情:

  1.之前断开的 http long polling 会重新连接

  2.会有一个异步 task 去服务器获取最新配置

  3.获取到最新配置后会写入本地文件

  4.写入本地文件成功后,会计算 diff 并通知到应用

  从上面的步骤可以看出,第 1 步会重新建立之前断开的连接,所以不算新增,第 2 步和第 3 步会短暂的各新增一个文件句柄(一次网络请求和一次本地 IO ),不过执行完后都会释放掉。

  4.2、尝试重现

  代码看了几遍也没看出问题,于是尝试重现问题,所以在本地起了一个demo应用(命令行程序,非web),尝试操作配置发布来重现,同时通过bash 脚本实时记录打开文件信息,以便后续分析。

  for i in {11000}

  do

  lsof -p 91742 > /tmp/20161101/$i.log

  sleep 0.01

  done

  然而本地多次测试后都没有发现文件句柄数增加的情况,虽然洗清了配置客户端的嫌疑,但是问题的答案却似乎依然在风中飘着,该如何解释观测到的种种现象呢?

  五、柳暗花明

  尝试自己重现问题无果后,只剩下最后一招了 - 通过应用的程序直接重现问题。

  为了不影响应用,我把应用的war包连同使用的Tomcat在测试环境又独立部署了一份。不想竟然很快就发现了导致问题的原因。

  原来Tomcat对webapp有一套自己的WebappClassLoader,它在启动的过程中会打开应用依赖的jar包来加载class信息,但是过一段时间就会把打开的jar包全部关闭从而释放资源。

  然而如果后面需要加载某个新的class的时候,会把之前所有的jar包全部重新打开一遍,然后再从中找到对应的jar来加载。加载完后过一段时间会再一次全部释放掉。

  所以应用依赖的jar包越多,同时打开的文件句柄数也会越多。

  同时,我们在Tomcat的源码中也找到了上述的逻辑。

  之前的重现实验最大的问题就是没有完全复现应用出问题时的场景,如果当时就直接测试了Tomcat,问题原因就能更早的发现。

  5.1、重现环境分析

  5.1.1 Tomcat刚启动完

  刚启动完,进程打开的句柄数是443。

  lsof -p 31188 | wc -l

  5.1.2 Tomcat 启动完过了几分钟左右

  启动完过了几分钟后,再次查看,发现只剩192个了。仔细比较了一下其中的差异,发现WEB-INF/lib下的jar包句柄全释放了。

  lsof -p 31188 | wc -l

  lsof -p 31188 | grep "WEB-INF/lib" | wc -l

  5.1.3 配置发布后 2 秒左右

  然后通过配置中心做了一次配置发布,再次查看,发现一下子又涨到422了。其中的差异恰好就是WEB-INF/lib下的jar包句柄数。从下面的命令可以看到,WEB-INF/lib下的jar包文件句柄数有228个之多。

  lsof -p 31188 | wc -l

  lsof -p 31188 | grep "WEB-INF/lib" | wc -l

  5.1.4 配置发布30秒后

  过了约30秒后,WEB-INF/lib下的jar包句柄又全部释放了。

  lsof -p 31188 | wc -l

  lsof -p 31188 | grep "WEB-INF/lib" | wc -l

  5.2 TomcatWebappClassLoader逻辑

  通过查看Tomcat(7.0.72 版本 )的逻辑,也印证了我们的实验结果。

  5.2.1 加载类逻辑

  Tomcat 在加载 class 时,会首先打开所有的jar文件,然后遍历找到对应的jar去加载:

  5.2.2 关闭 jar 文件逻辑

  同时会有一个后台线程定期执行文件的关闭动作来释放资源:

  5.3故障场景分析

  对于应用出现故障的场景,由于是应用启动后第一次配置变化,所以会使用到一个之前没有引用过的 class:  com.ctrip.framework.apollo.model.ConfigChange , 进而会触发Tomcat类加载,并最终打开所有依赖的jar包 , 从而导致在很短的时间内进程句柄数升高。 ( 对该应用而言,之前测试下来的数字是 228 )。

  虽然现在无从知晓该应用在出问题前总的文件句柄数,但是从CAT监控可以看到光TCP连接数(Established和TimeWait之和 )就在3200+了,再加上一些 jdk 加载的类库(这部分Tomcat不会释放)和本地文件等,应该离4096的上限相差不多了。所以这时候如果Tomcat再一下子打开本地228个文件,自然就很容易导致Too manyopen files的问题了。

  六、总结

  6.1 问题产生原因

  所以,分析到这里,整件事情的脉络就清晰了:

  1.应用的Max Open Files限制设置成了4096

  2.应用自身的文件句柄数较高,已经接近了4096

  3.用户在配置中心操作了一次配置发布,由于Tomcat的类加载机制,会导致瞬间打开本地200多个文件,从而迅速达到4096上限

  4.Jedis 在运行过程中需要和Redis重新建立连接,然而由于文件句柄数已经超出上限,所以连接失败

  5.应用对外的服务由于无法连接Redis,导致请求超时,客户端请求堆积,陷入恶性循环

  6.2 后续优化措施

  通过这次问题排查,我们不仅对Too many open files这一问题有了更深的认识,对平时不太关心的Tomcat类加载机制也有了一定了解,同时也简单总结下未来可以优化的地方:

  1、 操作系统配置

  从这次的例子可以看出,我们不仅要关心应用内部,对系统的一些设置也需要保持一定的关注。如这次的Max Open Files配置,对于普通应用,如果流量不大的话,使用4096估计也OK。但是对于对外提供服务的应用,4096就显得太小了。

  2 、 应用监控、报警

  对应用的监控、报警还得继续跟上。比如是否以后可以增加对应用的连接数指标进行监控,一旦超过一定的阈值,就报警。从而可以避免突然系统出现问题,陷于被动。

  3、 中间件客户端及早初始化

  鉴于Tomcat的类加载机制,中间件客户端应该在程序启动的时候做好初始化动作,同时把所有的类都加载一遍,从而避免后续在运行过程中由于加载类而产生一些诡异的问题。

  4、 遇到故障,不要慌张,保留现场

  生产环境遇到故障,不要慌张,如果一时无法解决问题的话,可以通过重启解决。不过应该至少保留一台有问题的机器,从而为后面排查问题提供有利线索。

  需要更多编程资讯可以到时间财富网。

© 著作权归作者所有

共有 人打赏支持
粉丝 2
博文 37
码字总数 57314
作品 0
成都
加载中

评论(1)

时间财富网
这个不错哟,特别分享出来
谈谈 Tomcat 架构及启动过程[含部署]

谈谈 Tomcat 架构及启动过程[含部署] ImportNew2018-01-061 阅读 Tomcat 原文出处: Rainstorm 这个题目命的其实是很大的,写的时候还是很忐忑的,但我尽可能把这个过程描述清楚。因为这是读...

ImportNew ⋅ 01/06 ⋅ 0

谈谈 Tomcat 架构及启动过程[含部署]

原文出处:Rainstorm 这个题目命的其实是很大的,写的时候还是很忐忑的,但我尽可能把这个过程描述清楚。因为这是读过源码以后写的总结,在写的过程中可能会忽略一些前提条件,如果有哪些比较...

Rainstorm ⋅ 01/06 ⋅ 0

Tomcat项目运行时加载jar包或类文件的顺序

1.在tomcat/common/lib下的jar文件,若更新或新增了,则只能重启服务器,才能重新加载jar包,使jar包生效。 2.如果application的WEB-INF/lib下的jar文件更新,则可以不重启tomcat便能使之生效...

JackMo2015 ⋅ 2016/12/20 ⋅ 1

Java 反射机制详解

Class类简介: Class对象 虚拟机在class文件的加载阶段,把类信息保存在方法区数据结构中,并在Java堆中生成一个Class对象,作为类信息的入口。 声明两个类, 和 获取Class对象一般有三种方式...

xrzs ⋅ 2012/08/30 ⋅ 0

谈谈 Tomcat 架构及启动过程 [ 含部署 ]

(点击上方公众号,可快速关注) 来源:Rainstorm, github.com/c-rainstorm/blog/blob/master/tomcat/谈谈%20Tomcat%20架构及启动过程%5B含部署%5D.md 这个题目命的其实是很大的,写的时候还...

ImportNew ⋅ 01/10 ⋅ 0

The method getJspApplicationContext(ServletContext

The method getJspApplicationContext(ServletContext) is undefined for the typ 搜索了半天,原来是类加载机制的问题,在tomcat的conf下的context.xml里面加上一句 <Loader delegate="true......

土鳖的弟弟 ⋅ 2015/06/11 ⋅ 0

tomcat内存溢出OutOfMemoryError

出现OutOfMemoryError PermGen space系统错误,通过在网上查阅资料,发现这个错误并不是Tomcat的问题,而JVM设计自身的一个缺陷,JVM把内存分了不同的区, PermGen space的全称是Permanent G...

郏高阳 ⋅ 2013/04/24 ⋅ 0

Mule ESB Http项目转换为Tomcat项目(10) 关于日志问题的补充

在(9)中我们提到了如何让ESB项目转换为Web项目后日志信息能输出到控制台和日志文件,在继续研究中,我还发现了以下一些问题: 1. 关于jansi-64.dll的问题。 log4j2-core库引用了jansi库,jan...

杨延庆 ⋅ 2016/07/01 ⋅ 0

ClassLoader在jvm和tomcat中的区别

发现问题: 前段时间使用ClassLoader类的方法来加载classPath路径下的配置文件,其中用到 、 、 这几个方法的时候出现问题。 问题出现在,程序用main方法的方式运行的时候能够正常运行,但是...

wangtx ⋅ 2016/04/27 ⋅ 0

java 触发类的初始化的方法

什么是类的初始化,什么是类的实例化,什么情况下会触发初始化,可能很多人都有这样的疑问? 那么首先我们需要了解一下jvm的类加载过程。 从JVM上来看,类的加载机制从加载到虚拟机内存到卸载...

Object_小风 ⋅ 2015/10/21 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

JDK1.6和JDK1.7中,Collections.sort的区别,

背景 最近,项目正在集成测试阶段,项目在服务器上运行了一段时间,点击表格的列进行排序的时候,有的列排序正常,有的列在排序的时候,在后台会抛出如下异常,查询到不到数据,而且在另外一...

tsmyk0715 ⋅ 31分钟前 ⋅ 0

spring RESTful

spring RESTful官方文档:http://spring.io/guides/gs/rest-service/ 1. 可以这么去理解RESTful:其实就是web对外提供的一种基于URL、URI的资源供给服务。不是一个原理性知识点。是一个方法论...

BobwithB ⋅ 32分钟前 ⋅ 0

C++ 中命名空间的 5 个常见用法

相信小伙伴们对C++已经非常熟悉,但是对命名空间经常使用到的地方还不是很明白,这篇文章就针对命名空间这一块做了一个叙述。 命名空间在1995年被引入到 c++ 标准中,通常是这样定义的: 命名...

柳猫 ⋅ 35分钟前 ⋅ 0

@Conditional派生注解

@Conditional派生注解(Spring注解版原生的@Conditional作用) 作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效; @Conditional扩展注解 作用(判...

小致dad ⋅ 36分钟前 ⋅ 0

适配器模式

适配器模式 对象适配器 通过私有属性来实现的类适配器 通过继承来实现的接口适配器 通过继承一个默认实现的类实现的

Cobbage ⋅ 39分钟前 ⋅ 0

Java 限流策略

概要 在大数据量高并发访问时,经常会出现服务或接口面对暴涨的请求而不可用的情况,甚至引发连锁反映导致整个系统崩溃。此时你需要使用的技术手段之一就是限流,当请求达到一定的并发数或速...

轨迹_ ⋅ 43分钟前 ⋅ 0

GridView和子View之间的间隙

默认的情况下GridView和子View之间会有一个间隙,原因是GridView为了在子View被选中时在子View周围显示一个框。去掉的办法如下: android:listSelector="#0000" 或 setSelector(new ColorDra...

国仔饼 ⋅ 46分钟前 ⋅ 0

idea插件开发

1 刷新页面要使用多线程 2 调试要使用restart bug 不要去关闭调试的idea 否则再次启动会卡住

林伟琨 ⋅ 46分钟前 ⋅ 0

Java 内存模型

物理机并发处理方案 绝大多数计算任务,并不是单纯依赖 cpu 的计算完成,不可避免需要与内存交互,获取数据。内存要拿到数据,需要和硬盘发生 I/O 操作。计算机存储设备与 cpu 之间的处理速度...

长安一梦 ⋅ 53分钟前 ⋅ 0

思路分析 如何通过反射 给 bean entity 对象 的List 集合属性赋值?

其实 这块 大家 去 看 springmvc 源码 肯定可以找到实现办法。 因为 spirngmvc 的方法 是可以 为 对象 参数里面的 list 属性赋值的。 我也没有看 具体的 mvc 源码实现,我这里只是 写一个 简...

之渊 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部