接到监控告警:属于产品项目的部分夜间话单未输出。查看话单文件,如下:
某按小时输出话单中漏了几个小时的话单,02,03月12小时的话单未正常输出。先通过后台接口重新输出对应话单。
先说明一下正常话单业务流程如下:
首先分析,话单功能正常,缺失话单前后皆有输出正确话单,那么怀疑两种可能性:1. 存在不确定的网络、数据库波动;2. 因为话单服务使用了分布式定时任务,可能存在某个节点(kubernates的某个pod)存在异常。
先查询系统监控告警与异常日志。发现告警与日志中都存在文件写入错误的情况,且都在同一个pod上。
登陆故障pod发现因为临时目录所在磁盘空间已满,导致输出临时文件时异常。
这一步的目标,查找大文件,找出磁盘满的元凶。
在目录下查找文件,查看磁盘目录占用的空间,
并未有占据空间的文件存在。
由于使用的是docker+k8s的基础架构,寻求k8s平台同事帮助查找是否因为物理主机磁盘原因。经平台同事查证,pod中存在大量未释放的文件句柄:
为删除状态,但是占用句柄的线程未释放,导致磁盘空间未释放。而占用句柄的线程,正是我们话单程序线程。
这是很明显的文件资源在业务逻辑中使用,但是未释放,导致删除后还占用着文件句柄。那么问题缩小到使用资源的部分。
但是检查使用文件资源的代码(FileInputStream、FileOutputStream),未发现未释放的资源。
java未释放的资源都会在heap当中不被回收,只需要导出对应的heap dump文件就可以找到未释放的资源,查找引用关系,则可定位到使用资源的方法。
使用命令进行文件导出
# jmap -dump:live,format=b,file=/logs/tmp/2.dump 7
将dump文件导入visualvm。
使用OQL查询所有InputStream与OutputStream的子类对象。重点关注文件相关的流的对象。
但是可以看出,无论是数量还是对象所引用的文件,与未释放的句柄都无关。检查其他非socket流的对象,也是一无所获,与期望的大量流资源未释放的场景想去甚远。
这时陷入了僵局,甚至怀疑是否是上传文件的框架或者最近升级的jvm版本存在未处理的issue。
查看其他有文件操作的服务,使用相同jvm版本,但未有类似情况出现。应该还是与代码逻辑相关。重新检查代码,重点查找File流之外的资源未释放情况。最终发现了一行可疑代码:
这部分使用了java工具类Files的方法创建了一个文件读取流,用以SFTP上传时使用,只有创建而无释放相关代码。修改相关代码:
测试验证,问题修复。
问题原因找到,已经修复,那么为什么heap当中无相关流的信息呢?
跟踪Files.newInputStream()方法发现最终是由Channels方法生成该流对象:
而Channel是java nio的对象。
普及一个java基础常识:nio通常会调用DirectByteBuffer来申请内存,而DirectByteBuffer申请的是堆外内存,是不会在heap当中管理的,所以导出的heap dump中并无相关对象。
问题已经完善解决,本次问题总结以下三点
try(//资源){
//do something
}
catch (Exception e) {
}
样例代码:
好处是可以避免遗忘关闭资源,或者新手不知道该如何、何时、合适的关闭资源。