netty analyst(1)-one netty bug for memory leak

问题描述

使用FUZZ测试的方法,启动N线程,不断执行如下步骤:创建连接->发送几个消息->释放连接。持续N小时后停止,观察到内存存在增长不回收现象,执行heap dump寻找内存泄露的地方,反复比较查找后可定位在SelectedSelectionKeySet:持有大量select key。

issue_3

排除以下几种情况:

  • GC时机未到:手工触发GC之后,内存仍然持有不释放;
  • 抓取heapdump的时机是刚好处理select keys的过程中,所以导致仍然有很多待处理的情况:排除这种情况,因为select key中间穿插了一些null, 而处理代码中,遇到null会认为结束,所以这意味着null之后的N多select key以后也永远无法处理。
1
2
3
4
final SelectionKey k = selectedKeys[i];
            if (k == null) {
                break;
            }

问题原因

NIO的基本处理步骤包括两步:

  • selector寻出所有就绪的通道事件(例如读、写等)
  • 按顺序处理所有就绪的通道事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void run() {
for (;;) {
 
……
select(oldWakenUp); //select出所有就绪事件
 
……
 
cancelledKeys = 0;
needsToSelectAgain = false;
……
 
processSelectedKeys(); //处理
 
}

在处理所有就绪事件时,有一个优化:select again:

在“选出”和处理“完”所有selectedKeys之间的时间段内,如果cancel的keys超过了256(测试中不断释放连接触发),那么直接放弃之后仍需要处理的select keys, 直接重新做select和具体的处理。

1
2
3
4
5
6
7
8
void cancel(SelectionKey key) {
        key.cancel();
        cancelledKeys ++;
        if (cancelledKeys >= CLEANUP_INTERVAL) {  //cancel keys > 256
            cancelledKeys = 0;
            needsToSelectAgain = true//enable select again.
        }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
    for (int i = 0;; i ++) {
        final SelectionKey k = selectedKeys[i];
        if (k == null) {
            break;
        }
        selectedKeys[i] = null;
 
        final Object a = k.attachment();
      ……
        processSelectedKey(k, (AbstractNioChannel) a);
 
        if (needsToSelectAgain) {
            for (;;) {
                if (selectedKeys[i] == null) {
                    break;
                }
                selectedKeys[i] = null;
                i++;
            }
 
            selectAgain();
 
            selectedKeys = this.selectedKeys.flip();
            i = -1;
        }
    }
}

此时面临的问题是:

Select Again之前那些尚未处理的select key如何处理,因为后面有select again过程,所以这部分未处理的select key无需保留,假设保留,存在的后果是:

当select again时,SelectedSelectionKeySet(包含keysA和keysB)此时已经使用另外一个keys,假设之前使用的是keysA,则使用keysB.使用keysB添加所有的select key并且处理完之后,下次添加则使用keysA. 此时如果添加的新的key的size<上轮keysA之前已消费的容量,则keysA中存在3段内容:新的key, null(或许N个),老的key。此时再重新消费keysA时,老的key仍然存在,因为遇到null就跳出了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public boolean add(SelectionKey o) {
    if (o == null) {
        return false;
    }
 
    if (isA) {
        int size = keysASize;
        keysA[size ++] = o;
        keysASize = size;
        if (size == keysA.length) {
            doubleCapacityA();
        }
    } else {
        int size = keysBSize;
        keysB[size ++] = o;
        keysBSize = size;
        if (size == keysB.length) {
            doubleCapacityB();
        }
    }
 
    return true;
}

因此必须要将select again之前未处理的selected key全部移除。而原有的代码因为一个简单的错误,并没有办法删除所有的keys.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  if (k == null) {
      break;
  }
  selectedKeys[i] = null//消费一个,置Null一个
……
 
  if (needsToSelectAgain) {
      for (;;) {
          if (selectedKeys[i] == null) {  //此时肯定是null,所以直接跳出,未删除后面的keys.
              break;
          }
          selectedKeys[i] = null;
          i++;
      }

 

解决方案

略微修改即可:

1
2
3
4
5
6
7
8
if (needsToSelectAgain) {
    for (;;) {
        i++;  //把i++移动到此
        if (selectedKeys[i] == null)
            break;
        }
        selectedKeys[i] = null;
    }

造成的影响是: 内存有一部分持有不释放,但是在正常的应用使用中,一般不会出现,因为要求断连的速度特别大,且超过了业务本身的处理速度。超过的越多,不释放的越多,但是正常情况下不会超过,即使超过也不会超过太多。

 

 

 

发布者

傅, 健

程序员,Java、C++、开源爱好者. About me

发表评论