【Glassfish修正分析】FD泄露

这是Glassfish社区的一个修正,但是并没有说明文档,这里稍作分析。

修正:[2.3.x]+ fix issue #1712

问题传送门:

https://java.net/jira/browse/GLASSFISH-21211

https://java.net/jira/browse/GRIZZLY-1789


问题简介

在服务器发布应用程序提供服务,当服务运行一段时间后有可能出现FD泄露。

出现FD泄露后,服务器的状态如下

CPU使用率:明显高于平时

TCP连接:存在等待关闭的状态(CLOSE_WAIT


问题再现

1、进入debug模式,在AbstractNIOAsyncQueueWriter#write中打一个断点(L:230),然后通过客户端发送请求“SERVER_IP:8080/xxx/test?length=12288”,这个请求会返回12288个字符“a”。

 

2、服务端运行到断点处,此时

messageSize=12388

bytesToReserver=12388

pendingBytes=12388


3、第一次执行F8,重新运行到断点,此时

messageSize=0

bytesToReserver=1

pendingBytes=101

 

4、中断SelectorRunner的运行,在SelectorRunner#run中打一个断点(L:278),第二次执行F8,重新运行到AbstractNIOAsyncQueueWriter#write中的断点,此时

messageSize =0

bytesToReserve= 1

pendingBytes =2

 

5、停止debug模式,此时

客户端连接状态: FIN_WAIT_2(客户端已经断开连接)

服务端连接状态: CLOSE_WAIT(服务端没有断开连接,FD泄露)

CPU使用率: 明显高于平时


问题分析

1、客户端向服务器发送请求

客户端发送请求“SERVER_IP:8080/xxx/test?length=12288”后,服务端通过TCPNIOTransportFilter中handleWrite方法获得TCPNIOAsyncQueueWriter,并调用TCPNIOAsyncQueueWriter的write方法。TCPNIOAsyncQueueWriter第二次调用write方法后,第二个数据长度为0的AsyncWriteQueueRecord没有被立即处理,所以TCPNIOAsyncQueueWriter还会调用第三次write方法。

第一次调用write方法,封装一个数据长度为12388的AsyncWriteQueueRecord,AsyncWriteQueueRecord中initialMessageSize属性值为12388。

第二次调用write方法,封装一个数据长度为0的AsyncWriteQueueRecord,AsyncWriteQueueRecord中initialMessageSize属性值为0。

第三次调用write方法,封装一个数据长度为0的AsyncWriteQueueRecord,AsyncWriteQueueRecord中initialMessageSize属性值为0。

 

2、调用三次write方法向写入三个AsyncWriteQueueRecord

①调用三次write方法

第一次执行F8后到达AbstractNIOAsyncQueueWriter#write中的断点,此时第一个AsyncWriteQueueRecord已经写入channel,准备写入第二个数据为0的AsyncWriteQueueRecord,这个时候中断SelectorRunner的运行。

第二次执行F8,由于SelectorRunner被中断,第二个AsyncWriteQueueRecord没有被处理,还在AsyncWriteQueueRecord队列中。此时TCPNIOAsyncQueueWriter会再次调用write方法,封装一个数据长度为0的AsyncWriteQueueRecord,所以第二次执行F8后程序再次执行到AbstractNIOAsyncQueueWriter#write中的断点。

继续运行,因为此时bytesToReserve值与pendingBytes值不相等,所以第三个数据长度为0的AsyncWriteQueueRecord也会被加入AsyncWriteQueueRecord队列,此时AsyncWriteQueueRecord队列中有两个数据长度为0的AsyncWriteQueueRecord。

②spaceInBytes增加

TCPNIOAsyncQueueWriter的write方法中,将两个数据长度为0的AsyncWriteQueueRecord添加到AsyncWriteQueueRecord队列时,spaceInBytes值增加2。

③调用processAsync方法

SelectorRunner调用TCPNIOAsyncQueueWriter的processAsync方法,将AsyncWriteQueueRecord队列中的两个AsyncWriteQueueRecord写入channel。processAsync方法会调用aggregate方法,aggregate方法能够返回队列中的AsyncWriteQueueRecord,并将返回的AsyncWriteQueueRecord从队列中删除。如果队列中有多个AsyncWriteQueueRecord,则将多个AsyncWriteQueueRecord组合为一个CompositeQueueRecord。

CompositeQueueRecord中有一个队列queue,queue中包含一组AsyncWriteQueueRecord,size属性表示所有AsyncWriteQueueRecord的数据长度总和,isEmptyRecord方法会直接返回false。

 

因为此时AsyncWriteQueueRecord队列中有两个数据长度为0的AsyncWriteQueueRecord,所以aggregate方法会返回包含两个AsyncWriteQueueRecord的CompositeQueueRecord,且此CompositeQueueRecord中size值为0,remaining方法返回值为0,isFinished方法返回true,isEmptyRecord方法返回false。此时TCPNIOAsyncQueueWriter的processAsync方法中变量如下

writen=0

done=true

bytesToRelease=0

bytesReleased=0

 所以isComplete值为默认值false,且result为AsyncResult.EXPECTING_MORE。

 

3、将任务添加到延迟任务队列

TCPNIOAsyncQueueWriter的processAsync方法执行结束后将result返回到ProcessorExecutor,因为result值为AsyncResult.EXPECTING_MORE,所以ProcessorExecutor最终会调用DefaultSelectorHandler的enque方法,这个方法会创建一个DefaultSelectorHandler$RunnableTask对象,并通过ArrayDeque的offer方法将此对象添加到SelectorRunner中的延迟任务队列。


4FD泄露

SelectorRunner中会循环调用doSelect方法,然后doSelect方法会调用SelectorHandler的preSelect方法,preSelect方法会处理延迟任务。preSelect方法会调用DefaultSelectorHandler的processPendingTaskQueue方法,在processPendingTaskQueue方法中获取延迟任务。最后会调用TCPNIOAsyncQueueWriter的processAsync方法,通过NIOConnection的getAsyncWriteQueue获取AsyncWriteQueueRecord队列。但是这个AsyncWriteQueueRecord队列中并没有元素,所以done和isComplete均为默认值。

所以result结果为AsyncResult.EXPECTING_MORE,DefaultSelectorHandler会重新创建一个延迟任务并添加到SelectorRunner的延迟任务队列。所以SelectorRunner的延迟任务队列中始终会有延迟任务存在,channel一直被使用,无法释放,从而导致FD泄露。

 

5CPU使用率升高

doSelect方法调用SelectorHandler的preSelect方法后会继续调用DefaultSelectorHandler的select方法。在select方法中存在如下代码

final booleanhasPostponedTasks = !selectorRunner.getPostponedTasks().isEmpty();

if(!hasPostponedTasks) {

    hasSelectedKeys =selector.select(selectTimeout) > 0;

} else {

    hasSelectedKeys = selector.selectNow() >0;

}

其中selectTimeout=3000,表示阻塞等待30秒。

 

首先获取延迟任务队列,调用isEmpty方法判断队列中是否存在延迟任务。因为延迟任务队列中始终存在一个延迟任务,所以调用WindowsSelectorImpl的selectNow方法。这个方法不会阻塞线程等待,不论有没有获取感兴趣事件,直接返回。所以SelectorRunner线程中没有阻塞,一直在循环执行,从而导致CPU使用率升高。


问题修正

这次问题的发生是因为执行TCPNIOAsyncQueueWriter的processAsync方法时,获得了错误的written值,使得isComplete值为false,result结果为AsyncResult.EXPECTING_MORE。最终导致每处理一个SelectorRunner延迟任务队列的任务时,会有一个新的延迟任务产生,因为延迟任务队列中一直存在一个任务,所以SelectorRunner线程无阻塞持续运行,导致FD泄露且CPU使用率升高。

 

获取错误written值的原因是执行以下代码时,如果queueRecord属于org.glassfish.grizzly.nio.transport.TCPNIOAsyncQueueWriter$CompositeQueueRecord类型,且CompositeQueueRecord中所有的AsyncWriteQueueRecord的数据长度为0,那么调用remaining方法时获得值为0,所以不会调用write0方法,written值为0。

final intwritten = queueRecord.remaining() > 0 ? (int) write0(nioConnection,queueRecord) : 0;

 

然后,通过以下代码获取bytesToRelease的值,又因为CompositeQueueRecord的isEmptyRecord方法返回值恒为false,所以bytesToRelease值为0。但是TCPNIOAsyncQueueWriter的write方法中,将数据长度为0的AsyncWriteQueueRecord添加到AsyncWriteQueueRecord队列时, 增加了spaceInBytes的值。如果bytesToRelease值为0,那么spaceInBytes的值不会减少。

final booleanisEmptyRecord = queueRecord.isEmptyRecord();

final intbytesToRelease = !isEmptyRecord ? written : (isFinished ?EMPTY_RECORD_SPACE_VALUE : 0);

 

通过以上分析可知,本次的问题是当queueRecord对象的类型不同时,written所代表的含义不同,程序没有正确获取不同情况下的written值。

queueRecord属于org.glassfish.grizzly.asyncqueue.AsyncWriteQueueRecord类型时,written表示写入channel数据的长度。

queueRecord属于org.glassfish.grizzly.nio.transport.TCPNIOAsyncQueueWriter$CompositeQueueRecord类型时,written表示写入channel数据的长度加上数据长度为0的AsyncWriteQueueRecord个数之和(下文中使用extraBytesToRelease代替)。所以当CompositeQueueRecord中所有的AsyncWriteQueueRecord的数据长度均为0时,written值出错。

 

统一written含义,使用变量written表示写入channel数据的长度。因为bytesToRelease表示需要spaceInBytes需要减少的大小,所以

bytesToRelease=written+extraBytesToRelease

为了使write0方法可以同时返回written和bytesToRelease,需要对它们进行封装,使用时直接通过封装后的对象中获取。同时这样做也会增加代码的可读性,以及保证使用时可以正确获取这两个变量。



你可能感兴趣的:(Glassfish,FD泄露,CPU使用率升高)