Netty内存池泄漏问题

转载自:https://blog.csdn.net/broadview2006/article/details/84559920

  为了提升消息接收和发送性能,Netty针对ByteBuf的申请和释放采用池化技术,通过PooledByteBufAllocator可以创建基于内存池分配的ByteBuf对象,这样就避免了每次消息读写都申请和释放ByteBuf。由于ByteBuf涉及byte[]数组的创建和销毁,对于性能要求苛刻的系统而言,重用ByteBuf带来的性能收益是非常可观的。

  内存池是一把双刃剑,如果使用不当,很容易带来内存泄漏和内存非法引用等问题,另外,除了内存池,Netty同时也支持非池化的ByteBuf,多种类型的ByteBuf功能存在一些差异,使用不当很容易带来各种问题。

  业务路由分发模块使用Netty作为通信框架,负责协议消息的接入和路由转发,在功能测试时没有发现问题,转性能测试之后,运行一段时间就发现内存分配异常,服务端无法接收请求消息,系统吞吐量降为0。

案例:

public class RouterServerHandler extends ChannelInboundHandlerAdapter {
    static ExecutorService executorService = Executors.newSingleThreadExecutor();
    PooledByteBufAllocator allocator = new PooledByteBufAllocator(false);
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf reqMsg = (ByteBuf)msg;
        byte [] body = new byte[reqMsg.readableBytes()];
        executorService.execute(()->
        {
            //解析请求消息,做路由转发,代码省略
            //转发成功,返回响应给客户端
            ByteBuf respMsg = allocator.heapBuffer(body.length);
            respMsg.writeBytes(body);//作为示例,简化处理,将请求返回
            ctx.writeAndFlush(respMsg);
        });
    }
  //后续代码省略
}

  进行一段时间的性能测试之后,日志中出现异常,进程内存不断飙升,存在内存泄漏。
  这里通过代码分析发现,请求ByteBuf被Netty框架申请后没有被释放,而导致内存泄漏。需要做如下释放操作:

@Override 
public void channelRead(ChannelHandlerContext ctx, Object msg) { 
    ByteBuf reqMsg = (ByteBuf)msg;byte [] body = new byte[reqMsg.readableBytes()]; 
    ReferenceCountUtil.release(reqMsg); 
//后续代码省略

ByteBuf申请和释放的理解误区
  有一种说法认为Netty框架分配的ByteBuf框架会自动释放,业务不需要释放;业务创建的ByteBuf则需要自己释放,Netty框架不会释放。但实际上不是这样的。为了在实际项目中更好地管理ByteBuf,下面分4种场景进行说明。

  1. 基于内存池的请求ByteBuf
      这类ByteBuf主要包括PooledDirectByteBuf和PooledHeapByteBuf,它由Netty的NioEventLoop线程在处理Channel的读操作时分配,需要在业务ChannelInboundHandler处理完请求消息之后释放(通常在解码之后),它的释放有两种策略。

策略1
  业务ChannelInboundHandler继承自SimpleChannelInboundHandler,实现它的抽象方法channelRead0(ChannelHandlerContext ctx, I msg),ByteBuf的释放业务不用关心,由SimpleChannelInboundHandler负责释放,相关代码如下(SimpleChannelInboundHandler):

@Override 
public void channelRead(ChannelHandlerContext ctx, Object msg) throws
Exception { 
    boolean release = true; 
    try { 
        if (acceptInboundMessage(msg)) { 
            I imsg = (I) msg; 
            channelRead0(ctx, imsg); 
        } else { 
            release = false; 
            ctx.fireChannelRead(msg); 
        } 
    } finally { 
        if (autoRelease && release) { 
            ReferenceCountUtil.release(msg); 
        } 
    } 
} 

  如果当前业务ChannelInboundHandler需要执行,则调用channelRead0之后执行ReferenceCountUtil.release(msg)释放当前请求消息。如果没有匹配上需要继续执行后续的ChannelInboundHandler,则不释放当前请求消息,调用ctx.fireChannelRead(msg)驱动ChannelPipeline继续执行。

策略2
  在业务ChannelInboundHandler中调用ctx.fireChannelRead(msg)方法,让请求消息继续向后执行,直到调用DefaultChannelPipeline的内部类TailContext,由它来负责释放请求消息,代码如下(TailContext):

protected void onUnhandledInboundMessage(Object msg) { 
    try { 
        logger.debug( 
        "Discarded inbound message {} that reached at the tail of the pipeline. " + 
        "Please check your pipeline configuration.", msg); 
    } finally { 
        ReferenceCountUtil.release(msg); 
    } 
} 
  1. 基于非内存池的请求ByteBuf
      如果业务使用非内存池模式覆盖Netty默认的内存池模式创建请求ByteBuf,例如通过如下代码修改内存申请策略为Unpooled:
//代码省略 
.childHandler(new ChannelInitializer() { 
    @Override 
    public void initChannel(SocketChannel ch) throws Exception { 
        ChannelPipeline p = ch.pipeline(); 
        ch.config().setAllocator(UnpooledByteBufAllocator.DEFAULT);
        p.addLast(new RouterServerHandler()); 
    } 
 }); 
} 

  也需要按照内存池的方式释放内存。

  1. 基于内存池的响应ByteBuf
      只要调用了writeAndFlush或者flush方法,在消息发送完成后,Netty框架会主动进行内存释放,业务不需要主动释放内存。

  2. 基于非内存池的响应ByteBuf
      无论是基于内存池还是非内存池分配的ByteBuf,如果是堆内存,则将堆内存转换成堆外内存,然后释放HeapByteBuffer,待消息发送完成,再释放转换后的DirectByteBuf;如果是DirectByteBuffer,则不需要转换,待消息发送完成之后释放。因此对于需要发送的响应ByteBuf,由业务创建,但是不需要由业务来释放。

你可能感兴趣的:(#,【Netty4】)