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
43io.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
51、 没有正确的识别远程连接关闭了,在连接关闭的情况下还保持了连接。每个连接消耗了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
24public class DirectMemoryReport extends Thread {
public static AtomicLong directMemory;
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就派不上用场。