我试图确定我观察到的行为是否正确或者 WildFly 是否正在泄漏文件句柄描述符。
在从 WildFly 11 升级到 14 后进行标准性能测试时,我们遇到了打开文件过多的问题。深入研究后发现,实际上是 WildFly 打开的管道数量在增加。
为了帮助重现该问题,我创建了一个包含大图像(100mb,以简化测试)的简单 JSF 2.2 应用程序。我使用标准 JSF 资源 URL 检索图像:
/contextroot/javax.faces.resource/css/images/big-image.png.xhtml
并且还尝试添加 omnifaces 并使用未映射的资源处理程序 URL:
/contextroot/javax.faces.resource/css/images/big-image.png
添加 Omnifaces 并没有改变我所看到的行为,我只是将它包括在内,因为我们最初认为它可能是一个促成因素。
我看到的行为:
WildFly 启动后,jstack 报告有两个线程匹配,default task-*
这是默认值task-core-threads
如果我对大图像发送 5 个并发请求,default task-*
则会产生 3 个新线程来处理这些请求。还将创建 3 个新的 Linux 管道。
如果我停止请求并等待 2 分钟(默认值task-keepalive
),则 3 个线程将被删除。管道保持打开状态。
定期 - 我相信大约每隔 4.5 分钟就会进行某种清理,并且会移除上述步骤中遗留的管道。
但是...如果删除了原来的两个工作线程中的一个,例如删除了任务 1、任务 3 和任务 4,剩下任务 2 和任务 5,则与任务 1 关联的管道永远不会被清理。
随着时间的推移,这些管道逐渐增多,据我所知,它们从未被移除过。这是某个地方的泄漏吗?如果是,是哪里?JSF?WildFly?Undertow?
我尝试过的事情:
WildFly 14、17 和 18
带有和不带有 Omnifaces(2.7 和 3.3)
将最小和最大线程更改为相同 - 这可以防止句柄累积,但我不想走这条路
答案1
我也遇到了这种泄漏。两个管道和一个 epoll 选择器的三元组中的句柄“丢失”了。(@Gareth:你能确认这一点吗?查看 /proc/$PID/fd 中的管道和匿名 inode)。由此看来,它似乎是由 Java NIO 通道产生的。
我发现,通过调用完整 GC 可以释放句柄(至少如此) (@Gareth:您能确认这一点吗)?我使用的是经过精心调优的 Java8-JVM,启用了 G1GC,令人欣慰的是,完整 GC 很少发生。但作为负面后果,它会同时消耗数千个 FH 三元组。
由于句柄是可释放的,所以它不是真正的泄漏,而是软/弱/幻影引用的效果。
上周已经两次达到指定的操作系统限制(带有 Wildfly 的 JVM 在 LX-Container 内运行)。因此,作为生产的第一个解决方法,我编写了一个看门狗,如果管道句柄级别达到限制,它会使用 jcmd 调用 FGC。
这是在运行大约 20 多个应用程序的 Wildfly-13(平衡)对上观察到的。它似乎与具体应用程序无关,因为如果我禁用单个应用程序的负载平衡(在其中一个对上),它也会发生(在两个 Wildfly 上)。
它不会“显示”在我们的其他 Wildflies(对)上,但有另一组具有其他用例的应用程序。内存循环更多,堆上的“压力”也更大。也许这会以另一种方式触发持有文件句柄的对象的同步释放。
通过使用“内存分析工具”查看堆转储,我发现 和 的实例数量相当高,并且sun.nio.ch.EPollArrayWrapper
与sun.nio.ch.EPollSelectorImpl
的入站引用相等org.xnio.nio.NioXnio$FinalizableSelectorHolder
。
答案2
将 WildFly 运行时从 Java 8 迁移到 11 后开始遇到此问题。
Java 11 将默认 GC 算法更改为G1。 在G1老生代对象收集非常有选择性,并且只有在超过某个堆占用阈值后才会收集。如果只有少数对象被提升到老生代,并且有助于达到此阈值,则可能会在org.xnio.nio.NioXnio$FinalizableSelectorHolder
那里堆积很长时间,保留打开的文件描述符。
在我的例子中,将垃圾收集切换到使用并发标记和清除解决了这个问题,尽管我相当确定 G1 可以进行调整以更积极地收集老一代。-XX:-G1UseAdaptiveIHOP
并且-XX:InitiatingHeapOccupancyPercent
是切换播放–XX:NewRatio
。另一种方法实际上可能是减少老生代堆( )或整个堆的大小。