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导致内存溢出的,在实际应用中,应根据具体业务需求和系统性能情况进行选择。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

坚持是一种态度

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值