> 文章列表 > netty使用异常,nio使用DirectBuffer导致内存溢出

netty使用异常,nio使用DirectBuffer导致内存溢出

netty使用异常,nio使用DirectBuffer导致内存溢出

文章目录

报错信息

  • 业务场景:雷达作为客户端,平台作为服务端,采用TCP/IP协议的socket连接,数据包采用字节的二进制数据传输
  • 平台部署某市,接入了四五十个路口和相应的雷达,在运行一周左右时,发现雷达数据停留在某个时间点,不再写入。查看了雷达数据采集服务的日志,发现内存溢出,主要有以下报错信息:
2023-04-11 15:11:31.196 ERROR 12936 --- [ntLoopGroup-3-2] io.netty.util.ResourceLeakDetector       : LEAK: ByteBuf.release() was not called before it's garbage-collected. See https://netty.io/wiki/reference-counted-objects.html for more information.
Recent access records: 
Created at:io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:402)io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:178)io.netty.buffer.AbstractByteBufAllocator.ioBuffer(AbstractByteBufAllocator.java:139)io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator$MaxMessageHandle.allocate(DefaultMaxMessagesRecvByteBufAllocator.java:114)io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:150)io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)java.base/java.lang.Thread.run(Thread.java:834)
  • 下载下来查看所有日志,发现了另一段报错,定位了出错位置
2023-04-11 14:37:21.913 ERROR 7760 --- [ntLoopGroup-3-1] io.netty.util.ResourceLeakDetector       : LEAK: ByteBuf.release() was not called before it's garbage-collected. See https://netty.io/wiki/reference-counted-objects.html for more information.
Recent access records: 
Created at:io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:96)io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:178)io.netty.buffer.Unpooled.directBuffer(Unpooled.java:127)com.newatc.socketio.protocol.PacketDecoder.decodePackets(PacketDecoder.java:57)com.newatc.socketio.handler.InPacketHandler.channelRead0(InPacketHandler.java:71)com.newatc.socketio.handler.InPacketHandler.channelRead0(InPacketHandler.java:36)io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:99)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:302)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)com.newatc.socketio.handler.AuthorizeHandler.channelRead(AuthorizeHandler.java:155)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:102)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:311)io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:432)io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)java.base/java.lang.Thread.run(Thread.java:834)

解决

  • 由于公司测试环境,没有这么多雷达和过车,只能通过手动调低内存的方式,触发此bug
  • 服务启动设置如下: -Xms128m -Xmx512m -XX:MaxDirectMemorySize=5m
  • 调低后,接入一个雷达,果然触发了bug。但也发现,如果设置的太低,kafka启动连接时,用到了NIO,也会报类似错误的
  • 找到问题后,修复起来也很简单。
  • 首先要修改报错处的代码,不要使用Direct,然后要及时释放内存占用,最后将生产环境的雷达采集服务的MaxDirectMemorySize配置设置大一点
  • 出错代码如下:
    public Packet decodePackets(ByteBuf buf, ClientHead client, boolean first, Packet packet) {if (first) {// 前七个字节为 : 标志(4)-负载长度(2)-协议版本号(1),暂未使用,直接跳过了buf.skipBytes(7);// 第8个字节为包类型short pType = buf.readUnsignedByte();PacketType packetSubType = PacketType.valueOfInner(pType);// 第9/10字节,为校验位(1)-Reserve(1),暂未使用,直接跳过了buf.skipBytes(2);// 包头十个字节,后面为负载(消息内容),负载的第一个字节为对象标识,剩余为对象数据内容short oID = buf.readUnsignedByte();ObjectId objectId = ObjectId.valueOf(oID);switch (packetSubType) {case QUERY_RESULT:break;case REPLY:break;case REPORT:packet = new Packet(PacketType.MESSAGE);packet.setSubType(packetSubType);packet.setObjectId(objectId);switch (objectId) {case REALTIME_DATA:packet.setName(EventName.REALTIME_DATA);break;case PASSING_VEHICLE:packet.setName(EventName.PASSING_VEHICLE);break;case TRAFFIC_STATUS:packet.setName(EventName.TRAFFIC_STATUS);break;case TRAFFIC_STATS:packet.setName(EventName.FLOW_STATS);break;case PERFORMANCE:packet.setName(EventName.PERFORMANCE);break;case TRAFFIC_EVENT:packet.setName(EventName.TRAFFIC_EVENT);break;case RADAR_FAULT:packet.setName(EventName.RADAR_FAULT);break;}ByteBuf args = Unpooled.directBuffer(buf.readableBytes());args.writeBytes(buf.retain());packet.setArgs(args);break;case HEART_BEAT:packet = new Packet(PacketType.PING);packet.setSubType(packetSubType);packet.setObjectId(ObjectId.HEART_BEAT_FROM_RADAR);break;default:break;}} else {ByteBuf args = packet.getArgs();ByteBuf args2 = Unpooled.directBuffer(args.capacity() + buf.readableBytes());args2.writeBytes(args);args2.writeBytes(buf.retain());packet.setArgs(args2);}
  • 这是对黏包拆包问题的一次错误处理,当时认为一个大的数据包被拆为多个数据包,第一次读取时从包头开始解析,并将负载内容放入directBuffer,非第一次时获取前面存的directBuffer拼上本次内容一起进行解析
  • 这个处理方式的错误之处有两点:一是用了directBuffer缓存数据,进而导致了内存溢出;二是netty根本不需要这么处理,数据不完整时,重置游标readerIndex直接返回,后续数据包上来再一起从头解析即可
  • 关于netty的黏包拆包问题,可以参考我的这篇博客

总结

Netty是基于NIO的框架,因此在使用Netty时可能会出现DirectBuffer导致内存溢出问题。这是因为DirectBuffer是使用操作系统的内存空间,而不是JVM内存空间,因此如果不及时释放,就会导致内存泄漏。

解决这个问题的方法有以下几种:

  1. 及时释放DirectBuffer:在使用完DirectBuffer后,一定要及时释放。可以使用ReferenceCountUtil.release()方法手动释放,也可以通过Netty的内存池机制自动释放。或者尽量不要使用DirectBuffer,使用其他类型的Buffer(参考第3条),把内存回收交给JVM。

  2. 调整Netty的内存池参数:Netty默认使用的内存池参数可能不适合业务场景,可以适当调整参数,以减少DirectBuffer的使用。对于生产场景,可以在参数配置时设置-XX:MaxDirectMemorySize给予更多的空间。

  3. 使用HeapBuffer:对于不需要跨内存空间的场景,可以使用HeapBuffer代替DirectBuffer,这样可以避免DirectBuffer的内存泄漏问题。

对于Netty使用DirectBuffer导致内存溢出的,在实际应用中,应根据具体业务需求和系统性能情况进行选择。