Description
Steps to Reproduce
- 编写一个处理文件上传请求的controller.
- 接收到上传文件参数后, 打开文件流, 不关闭, 直接返回.
- 在Java-Chassis 1.x分支上可以观察到一段时间后文件句柄会被清理, 而在 Java-Chassis 2.8.24 版本, 文件句柄会一直存在, 直到进程重启才会消失.
注意:
- 实际测试发现此问题在Windows上不会出现(Java8), 我们是在Linux上复现的问题.
- 打开文件流不关闭只是为了让问题必现. 实际上即使业务代码有正常的close调用也可能遇到类似的问题, 因为业务处理文件流的逻辑可能超时, 导致 Vert.x 执行临时文件清理回调时文件流仍然处于打开的状态.
Expected Behavior
预期 Java-Chassis 2.8.24 能像 Java-Chassis 1.x 分支一样, 能够自动兜底清理文件句柄.
Servicecomb Version
2.8.24
Additional Context
根因分析
导致 Java-Chassis 1.x 和 2.8.x 差异的代码在于:
在更早的版本中, 它返回的是一个 FileInputStream
:
而 2.8.20 版本对此做了上述修改, 实测返回的是 sun.nio.ch.ChannelInputStream
类型的流.
在 Java8 中, FileInputStream
的 finalize
方法会执行 close
方法, 而 sun.nio.ch.ChannelInputStream
的 finalize
方法是空的. 这就导致 Java-Chassis 2.8.19 及之前的版本, 即使业务代码由于各种各样的原因没有执行 close
方法, FileInputStream 也能在被GC回收时兜底关闭文件句柄, 而在升级到 Java-Chassis 2.8.20 之后就只能泄漏句柄了.
回溯 Java-Chassis 的代码修改记录可知, 这个变化是为了修复另外一个问题而引入的:
#4476
解决思路
Java-Chassis 已经不适合将文件输入流的类型改回 FileInputStream
了.
除了上面说的 issue #4476 的原因, 高版本的 Java 还废弃了 finalize
方法, 即使用回 FileInputStream
也不能兜底关闭文件句柄.
为了能够同时兼容 Java8 和 Java21, 也不适合选用 Cleaner 机制(Java9才具备此特性).
目前能想到的解决办法:
-
基于 Java-Chassis 自身的 Invocation 生命周期管理机制来做自动清理动作.
此方案的风险在于, 考虑到业务可能在 controller 层获取上传文件参数后, 异步调度到其他任务线程池处理, 因此在 Invocation 的结束事件发生时就直接关闭流的话可能会导致业务视角看到的Java-Chassis框架行为发生了影响业务功能的不兼容变化.
-
记录已打开的 InputStream, 在后台定时任务中监控其关闭状态.
此方案可以一定程度规避方案1的风险, 但存在内存用量上升甚至泄漏的风险, 若选择这个思路, 也需要小心进行可靠性设计.
此外, 建议Java-Chassis增加对 FileUploadPart 的打开流的调用记录的监控, 便于辅助业务分析此类文件句柄泄漏问题的触发代码位置.