这里描述的两个问题是在一年多前发现并修复的。本文仅作为历史证明和解决 Java 中文件描述符泄漏的初学者指南。
在 Ultra ESB 中,我们使用内存中 RAM 磁盘文件缓存来快速、无垃圾地处理有效负载。前段时间,我们在共享的 SaaS AS2 网关上遇到了一个问题,该缓存随着时间的推移会泄漏文件描述符。最终导致在达到系统限制时出现过多的打开文件错误。
The Legion of the Bouncy Castle: Leftless from your stream-backed mime parts?
我们发现,罪魁祸首之一是 Bouncy Castle ——这家著名的安全提供商自 Ultra ESB Legacy 时代以来一直是我们深爱的。
通过一个简单的自制工具包 ,我们发现 BC 习惯于调用 MIME 部分以确定它们的类型(例如,检查)。诚然,这本身并不是犯罪;但是我们的大多数 MIME 部分都是文件支持 的,另一个是文件缓存文件——这意味着每个部分都会打开一个新的文件流。现在有杂散流(以及文件描述符)指向我们的文件缓存。 getcontent() instanceof getcontent()
这些已经足够了,我们将用尽分配给 Ultra ESB (Java) 进程的文件描述符配额。
溶液?让他们懒惰!
我们不想弄乱 BC 代码库。因此,我们找到了一个简单的解决方案:使用“惰性”流创建所有文件支持的 MIME 部分。我们的(前)同事 Rajind 受 JBoss-VFS 的 lazyinputstream 的启发编写了一个 ,它只有在尝试 A 时才会打开实际文件。 lazyfileinputstream read
BC 很高兴,文件缓存也很高兴,但我们是最高兴的。
Hibernate JPA :晚饭后清理,又名关闭消耗的流
我们发现的另一个错误是某些数据库操作留下了未关闭的文件句柄。显然,这只是当我们将流支持的 blob 喂入休眠状态时,流通常来自文件缓存条目。
经过一番挖掘,我们提出了一个理论,即 Hibernate 并没有关闭这些博客条目的基础流。(这是有道理的,因为该接口不会公开 Hibernate 可用于操作基础数据源的任何方法。但是,这是一个问题,因为丢弃的流(以及关联的文件句柄)要等到下一个 GC 才会发布。 java.sql.blob
这对于短期应用程序来说本来是可以的,但是像我们这样长期运行的应用程序很容易用完文件描述符;例如在突然和持续的峰值的情况下。
溶液?让他们自动关闭!
我们不想失去流媒体的好处,但我们也无法控制我们的流媒体。你可能会说我们应该把我们的流放在可自动关闭的结构中(比如, try-with-resources )。不错的尝试;但遗憾的是,Hibernate 在我们的执行范围之外读取了它们(尤其是在 @transactional 流中)。一旦我们开始关闭代码范围内的流,我们的数据库操作就开始惨遭失败——尖叫着“流已经关闭!
他们说,在罗马时,要像罗马人那样做 。
因此,我们没有惹恼 Hibernate,而是决定自己处理流。
Rajind(是的,又是他)破解了一个 自关闭的 InputStream 包装器 。这将跟踪从底层流读取的数据量,并在读取最后一个字节后立即将其关闭。
自动关闭。照顾好自己!
(我们确实考虑过使用现有的选项,如来自Apache Commons-IO的AutoCloseInputStream ;但是我们确实需要在这里和那里进行一些自定义 - 例如详细的跟踪日志记录。
底线
当谈到 Java 中的资源管理时,很容易过度关注内存和 CPU(处理)而忘记其余部分。 虚拟资源(如临时端口和每个进程的文件描述符 )可能同样重要,甚至更重要。
特别是在像我们的 AS2 Gateway SaaS 应用程序这样长时间运行的进程中,它们实际上可以成为沉默的杀手。
您可以通过两种主要方式检测这种类型的“泄漏”:
“单周期”资源分析 — 运行一个完整的处理周期,比较之前和之后的资源使用情况。
长期监控 — 持续记录和分析资源指标,以识别趋势和异常情况。
无论如何,修复泄漏并不太困难;一旦你清楚地了解了你正在处理的事情。
祝你好运,追捕你的资源猪 d(a)emons!