一次资源未释放的问题定位

问题背景

       接到监控告警:属于产品项目的部分夜间话单未输出。查看话单文件,如下:

一次资源未释放的问题定位_第1张图片

 

某按小时输出话单中漏了几个小时的话单,02,03月12小时的话单未正常输出。先通过后台接口重新输出对应话单。

问题定位

初步分析

       先说明一下正常话单业务流程如下:

一次资源未释放的问题定位_第2张图片

 

       首先分析,话单功能正常,缺失话单前后皆有输出正确话单,那么怀疑两种可能性:1. 存在不确定的网络、数据库波动;2. 因为话单服务使用了分布式定时任务,可能存在某个节点(kubernates的某个pod)存在异常。

       先查询系统监控告警与异常日志。发现告警与日志中都存在文件写入错误的情况,且都在同一个pod上。

 

       登陆故障pod发现因为临时目录所在磁盘空间已满,导致输出临时文件时异常。

一次资源未释放的问题定位_第3张图片

 

进一步分析

       这一步的目标,查找大文件,找出磁盘满的元凶。

       在目录下查找文件,查看磁盘目录占用的空间,

一次资源未释放的问题定位_第4张图片

 

并未有占据空间的文件存在。

       由于使用的是docker+k8s的基础架构,寻求k8s平台同事帮助查找是否因为物理主机磁盘原因。经平台同事查证,pod中存在大量未释放的文件句柄:

一次资源未释放的问题定位_第5张图片

 

为删除状态,但是占用句柄的线程未释放,导致磁盘空间未释放。而占用句柄的线程,正是我们话单程序线程。

找出元凶

       这是很明显的文件资源在业务逻辑中使用,但是未释放,导致删除后还占用着文件句柄。那么问题缩小到使用资源的部分。

一次资源未释放的问题定位_第6张图片

 

       但是检查使用文件资源的代码(FileInputStream、FileOutputStream),未发现未释放的资源。

       java未释放的资源都会在heap当中不被回收,只需要导出对应的heap dump文件就可以找到未释放的资源,查找引用关系,则可定位到使用资源的方法。

使用命令进行文件导出

# jmap -dump:live,format=b,file=/logs/tmp/2.dump 7

将dump文件导入visualvm。

使用OQL查询所有InputStream与OutputStream的子类对象。重点关注文件相关的流的对象。

一次资源未释放的问题定位_第7张图片

一次资源未释放的问题定位_第8张图片

 

 

但是可以看出,无论是数量还是对象所引用的文件,与未释放的句柄都无关。检查其他非socket流的对象,也是一无所获,与期望的大量流资源未释放的场景想去甚远。

       这时陷入了僵局,甚至怀疑是否是上传文件的框架或者最近升级的jvm版本存在未处理的issue。

       查看其他有文件操作的服务,使用相同jvm版本,但未有类似情况出现。应该还是与代码逻辑相关。重新检查代码,重点查找File流之外的资源未释放情况。最终发现了一行可疑代码:

一次资源未释放的问题定位_第9张图片

 

这部分使用了java工具类Files的方法创建了一个文件读取流,用以SFTP上传时使用,只有创建而无释放相关代码。修改相关代码:

一次资源未释放的问题定位_第10张图片

 

测试验证,问题修复。

       问题原因找到,已经修复,那么为什么heap当中无相关流的信息呢?

       跟踪Files.newInputStream()方法发现最终是由Channels方法生成该流对象:

一次资源未释放的问题定位_第11张图片

 

而Channel是java nio的对象。

       普及一个java基础常识:nio通常会调用DirectByteBuffer来申请内存,而DirectByteBuffer申请的是堆外内存,是不会在heap当中管理的,所以导出的heap dump中并无相关对象。

总结

问题已经完善解决,本次问题总结以下三点

  1. 老生常谈的资源释放问题,很多java编码规范中都会提起的流关闭问题,创建流=>使用流=>关闭流这是一个基本流程,使用完流对象,必须在finally里面关闭,未关闭的流会导致类似于连接满了、磁盘满了甚至OOM的情况。这里推荐使用JDK7以后有的特性try-with-resource。写法如下:

try(//资源){
    //do something

}
catch (Exception e) {
}

 

       样例代码:

一次资源未释放的问题定位_第12张图片

 

好处是可以避免遗忘关闭资源,或者新手不知道该如何、何时、合适的关闭资源。

  1. Heap dump定位失效。使用万金油定位方法(heapdump,javacore,gcinfo),需要了解到jvm的底层原理,像本次案例中,使用heap dump进行分析时,未定位到对应heap中的对象,而考虑是否框架或JDK有未关闭的issue查找资料浪费了许多时间,后续存在有类似的场景, heap dump中无相关信息,首先考虑的应当是nio类型来缩小范围。
  2. JDK、框架商用版本中遗留的问题的概率,总是比自身项目的业务代码存在问题的概率要小。

你可能感兴趣的:(bug)