前言:此处描述的两个问题是在一年前发现并修复的。 本文仅用作历史证明,也是有关解决Java中文件描述符泄漏的初学者指南。
在Ultra ESB中,我们使用内存RAM磁盘文件缓存来进行快速且无垃圾的有效负载处理。 一段时间以前,我们在共享的SaaS AS2网关上遇到了一个问题,该缓存随着时间的推移泄漏了文件描述符。 最终,在击中系统ulimit
时,导致too many open files
错误too many open files
。
有弹性的城堡军团:流支持的MIME部分中的剩余部分?
我们发现罪魁祸首是Bouncy Castle ,这是著名的安全服务提供商,自Ultra ESB Legacy时代起就一直是我们的挚爱。
通过一些简单的工具,我们发现BC习惯了对MIME部分调用getContent()
以确定它们的类型(例如, instanceof
检查)。 没错,这本身并不是犯罪。 但是我们的大多数MIME部分都是文件支持的 ,另一端带有文件缓存文件-这意味着每个getContent()
都会为该文件打开一个新流。 因此,现在有指向我们的文件缓存的杂散流(以及文件描述符)。
这些已经足够了,我们将用尽分配给Ultra ESB(Java)进程的文件描述符配额。
让他们变得懒惰!
我们不想弄乱BC代码库。 因此,我们找到了一个简单的解决方案:使用“惰性”流创建所有文件支持的MIME部分。 我们(以前)同事Rajind写了LazyFileInputStream
-灵感LazyInputStream
从jboss-vfs
-打开只有当实际文件read
尝试。
BC很高兴,文件缓存也很高兴。 但是我们是最快乐的
Hibernate JPA:晚饭后清理,也就是关闭消耗的流
我们发现的另一个错误是某些数据库操作留下了未关闭的文件句柄。 显然,只有当我们将流支持的Blob馈送到Hibernate时,流才通常来自文件缓存项。
经过一番挖掘之后,我们提出了一个理论,即Hibernate不会关闭这些Blob条目的基础流。 (这是有道理的,因为java.sql.Blob
接口没有公开Hibernate可以用来操作基础数据源的任何方法。)但是,这是一个问题,因为丢弃的流(以及关联的文件句柄)不会得到发布直到下一个GC。
对于一个短期应用程序来说,这本来可以,但是像我们这样长期运行的应用程序很容易用完文件描述符。 例如突然而持续的峰值。
让他们自动关闭!
我们不想失去流媒体的好处,但是我们也无法控制流媒体。 您可能会说我们应该将流放在可自动关闭的结构中(例如try-with-resources )。 不错的尝试; 但是可悲的是,Hibernate在我们的执行范围之外(特别是在@Transactional
流中)读取它们。 一旦我们开始在代码范围内关闭流,我们的数据库操作就开始惨败-尖叫“流已关闭!”。
他们说, 在罗马时,就像罗马人一样 。
因此,我们决定不打扰Hibernate,而是决定自己处理流。
Rajind(是的,还是他)再次入侵了SelfClosingInputStream
包装器 。 这将跟踪从底层流读取的数据量,并在读取最后一个字节后立即将其关闭。
(我们确实考虑过使用现有选项,例如来自Apache commons-io
AutoCloseInputStream
;但是发生了,我们到处都需要一些自定义设置,例如详细的跟踪日志记录。)
底线
当涉及到Java中的资源管理时,很容易过度关注内存和CPU(处理),而忽略其余部分。 但是虚拟资源(例如临时端口和每个进程的文件描述符 )可能同样重要,甚至更多。
尤其是在长时间运行的流程(例如我们的AS2 Gateway SaaS应用程序)上,它们实际上可以成为沉默的杀手。
您可以通过两种主要方式检测这种“泄漏”:
- “单周期”资源分析 :运行一个完整的处理周期,比较前后的资源使用情况
- 长期监控 :持续记录和分析资源指标以识别趋势和异常
在任何情况下,修复泄漏都不是一件容易的事。 一旦您清楚地了解要处理的内容。
祝您好运,寻找您的资源消耗d(a)守护程序!
翻译自: https://www.javacodegeeks.com/2019/10/is-your-jvm-leaking-file-descriptors-like-mine.html