Netty堆外内存排查

Netty 网关内存泄漏排查

参考资料 美团技术的帖子

背景: 物联网平台中,生产环境中Netty网关突然出现了崩溃的现象。并且报内存泄漏的问题。

错误初现

生产环境突然出现网关连不上的情况,当时赶紧拷贝了错误日志,并重启了网关。但是第二天上线检查,发现所有的网关又报错了,都不能服务了。 先看一下错误日志:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 16777216 byte(s) of direct memory (used: 4116185065, max: 4116185088)

at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:640)

at io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:594)

at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:764)

at io.netty.buffer.PoolArena$DirectArena.newChunk(PoolArena.java:740)

at io.netty.buffer.PoolArena.allocateNormal(PoolArena.java:244)

at io.netty.buffer.PoolArena.allocate(PoolArena.java:214)

at io.netty.buffer.PoolArena.allocate(PoolArena.java:146)

at io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:324)

at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:185)

at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:176)

at io.netty.buffer.AbstractByteBufAllocator.ioBuffer(AbstractByteBufAllocator.java:137)

at io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator$MaxMessageHandle.allocate(DefaultMaxMessagesRecvByteBufAllocator.java:114)

at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:147)

at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:628)

at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:563)

at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:480)

at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:442)

at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:884)

at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)

at java.lang.Thread.run(Thread.java:748)

2019-01-21 14:55:09,108 nioEventLoopGroup-3-3 WARN [DefaultChannelPipeline:151] An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.

问题出现,有几种可能:

1
2
3
4
5
1、 没有正确的识别远程连接关闭了,在连接关闭的情况下还保持了连接。每个连接消耗了16mb的内存,共用4000多个连接在等待关闭没有被正确关闭。

2、 bytebuf没有被正确回收

3、打印日志,造成线程阻塞

问题定位

根据错误日志

1
PlatformDepedeng.incrementMemory()

全局搜索 PlatformDependent,找到日志报错的地方:

1
failed to allocate 16777216 byte(s) of direct memory (used: 4116185065, max: 4116185088)

可以看出来类PlatformDependent是用来统计netty网关的堆外内存的使用。计数器为 DIRECT_MEMORY_COUNTER,如果发现已使用内存大于堆外内存的上限(用户自行指定),就抛出一个自定义 OOM Error,异常里面的文本内容正是我们在日志里面看到的。

既然知道了内存的统计方法,我们就可以通过反射拿到内存的统计值。堆外内存统计字段是 DIRECT_MEMORY_COUNTER,我们可以通过反射拿到这个字段,然后定期 Check 这个值,就可以监控 Netty 堆外内存的增长情况。

反射进行堆外内存监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DirectMemoryReport extends Thread {

public static AtomicLong directMemory;
@Override
public void run() {
try {
Field field = PlatformDependent.class.getDeclaredField("DIRECT_MEMORY_COUNTER");
field.setAccessible(true);
directMemory = (AtomicLong)field.get(PlatformDependent.class);
while (true){
Thread.sleep(1000);
System.out.println("**********");
System.out.println(directMemory.get()/1024);
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}

这里我们每 1s check 一下内存的使用情况。注意这里一定要限定一下 jvm 的内存使用。开始我没有限定 jvm 的内存使用,发现每次连接都会保留大概16m的堆外内存。应该是netty根据内存的使用情况,为了提高效率自己使用的堆外内存。限定一下jvm的内存使用情况,可以更好的观察内存的变化情况。

内存限定:

1
-Xms16m -Xmx32m

之后进入debug模式,把内存打印的现在挂起 Thread。一步步的观察内存的使用情况。最后果然发现,有一块代码 ByteBuf 的申请没有释放。

总结

Netty 为了提高效率,并引入了计数的概念。ByteBuf 的申请需要自己释放。但是Netty默认会在ChannelPipline的最后添加一个tail handler帮你完成ByteBuf的release。因此如果是 Netyy msg 传递过程中使用的 ByteBuf 就不需要手动释放。

ByteBuf创建:

1
ByteBuf heapBuf = Unpooled.buffer();

ByteBuf的释放:

1
2
3
4
5
6
每个Handler对ByteBuf的处理有以下三种方式: 
- 对原消息不做处理,调用 ctx.fireChannelRead(msg)把原消息往下传,此时不做释放;
- 将原消息转化为新的消息并调用 ctx.fireChannelRead(newMsg)往下传,那必须把原消息release掉;
- 如果已经不再调用ctx.fireChannelRead(msg)传递任何消息,那更要把原消息release掉;

说明: Netty的ChannelPipleline的末端有TailHandler,如果每个Handler都把消息往下传,TailHandler会释放掉ReferenceCounted类型的消息。 如果我们的业务Hanlder不再把消息往下传了,这个TailHandler就派不上用场。

Donate comment here