cuteant / SpanNetty

Port of Netty(v4.1.51.Final) for .NET
MIT License
301 stars 47 forks source link

Huge Memory Usage-DotNetty.Buffers.HeapArena and DotNetty.Buffers.DirectArena #58

Closed kamikyo closed 3 years ago

kamikyo commented 3 years ago

大佬,我在使用Dotnetty(Spannetty同样如此)时会出现内存一直涨的问题(接服务器的客户端网络不稳,经常性的会断线重连)。我通过dump分析,发现内存主要耗费在DotNetty.Buffers.HeapArena 和 DotNetty.Buffers.DirectArena 上面。下面分两个场景来描述: 1。服务器上(cpu 8核,内存128GB)的代码配置如下: .Option(ChannelOption.Allocator, UnpooledByteBufferAllocator.Default) dump分析会有大量的DotNetty.Buffers.HeapArena,每块16MB

2。家里使用的配置如下:

Environment.SetEnvironmentVariable("io.netty.allocator.pageSize", "4096");
Environment.SetEnvironmentVariable("io.netty.allocator.maxOrder", "1");
...
.Option(ChannelOption.Allocator, ArrayPooledByteBufferAllocator.Default)

dump分析会有大量的DotNetty.Buffers.HeapArena和DotNetty.Buffers.DirectArena,每块大约8KB

接下来我会将配置调整为:

Environment.SetEnvironmentVariable("io.netty.allocator.numHeapArenas", "0");
Environment.SetEnvironmentVariable("io.netty.allocator.numDirectArenas", "0");
...
.Option(ChannelOption.Allocator, UnpooledByteBufferAllocator.Default);

再进行测试,有结果会继续更新在这里。

在原dotnetty的issues里我找到了这样的issue Huge Memory Usage-DotNetty.Buffers.HeapArena 感觉最终也还是没有一个解决的办法 是否我dotnetty的使用方式不对,还是说这块是一个历史上的遗留问题?

注:家里测试的话,接收流量大约在1.6MB/s左右

kamikyo commented 3 years ago

更新一下结果,修改配置之后没有出现“内存泄露”的情况。但是按照之前的人的说法是,这样做配置会导致性能下降。不知是否可以既使用内存的池化技术,又不出现内存泄露的使用方式

cuteant commented 3 years ago

这几天因为新冠在家里做自我隔离,没及时看信息:pray: DotNetty中设计是这样的, PooledByteBuffer 是进程内共享的,所以在应用端不管你用于不用,默认配置下都会自动根据配置分配一部分内存做为缓存池来使用,这种设计在服务端来看是没问题的,对客户端就不怎么友好。 如果这样配置了:

Environment.SetEnvironmentVariable("io.netty.allocator.numHeapArenas", "0");
Environment.SetEnvironmentVariable("io.netty.allocator.numDirectArenas", "0");
...
.Option(ChannelOption.Allocator, UnpooledByteBufferAllocator.Default);

你要确保在同一进程内不在使用 PoolByteBuffer,也可以使用 ArrayPooledByteBufferAllocator 代替 UnpooledByteBufferAllocator 当然性能最好的还是 PooledByteBuffer

cuteant commented 3 years ago

后期会做些修改,配置应该跟随 Bootstrap/ServerBootstrap 实例,PooledByteBuffer实现根据实际需求来延迟分配

cuteant commented 3 years ago

但是有一点要说,系统分配PoolByteBuffer不会造成内存泄漏,就像使用 System.Buffers.ArrayPool.Shared 一样,如果内存一直增长的话,还是看业务代码部分

kamikyo commented 3 years ago

首先感谢大佬能在百忙之中抽出时间回复我

接入的客户端的行为的话就是非常的不稳定,连上几分钟,然后疯狂的输出一波流量,然后断线,下次再这样。不过监测下来的话整体流量是稳定的,线上(也就是服务器)大约在3~5MB/s。正如之前提到,整个代码仅是调整了配置项

Environment.SetEnvironmentVariable("io.netty.allocator.numHeapArenas", "0");
Environment.SetEnvironmentVariable("io.netty.allocator.numDirectArenas", "0");
...
.Option(ChannelOption.Allocator, UnpooledByteBufferAllocator.Default);

其它没有做任何改动,这样做之后dump就再也找不到16MB那种内存占用块了。如果不是“内存泄露”,那是因为什么原因导致dotnetty复用效率低了吗? 如果说业务代码会有问题我觉得应该就是出在这里,还请大佬指点一下:

public override void ChannelRead(IChannelHandlerContext context, object message)
{
    if (!(message is IByteBuffer buffer)) return;
    IByteBuffer reply = CreateApply(buffer);
    if (null != reply)
    {
        context.WriteAsync(reply);
    }
    byte[] datas = new byte[buffer.ReadableBytes];
    //复制一份byte数据出去
    buffer.ReadBytes(datas);
    string ip = context.Channel.RemoteAddress.ToString();
    //将datas传递给另一个线程使用
    SomeMethod(datas)
}

另一个线程

public SomeMethod(byte[] datas)
{
    //将datas转为了对应的实体类
    SomeModel m = new SomeModel();
    foreach (byte b in datas)
    {
        if(b == 0x01)
        {
            m.P1 = "a";
            ...
        }
    }
}

我看了一下源码buffer.ReadBytes(datas);这种复制最终貌似是使用Unsafe.CopyBlockUnaligned(ref dst[dstIndex], ref src[srcIndex], unchecked((uint)length));这个进行复制(我不清楚使用PoolByteBuffer最终会具体使用哪种复制逻辑),这个方法我自己试了一下,好像是深度复制。也就是说另一个线程操作datas就影响dotnetty回收那个message。

另外还有一个可疑的地方就是代码中的context.WriteAsync(reply); 其中reply是使用Unpooled.WrappedBuffer创建的,没有显示的释放这个reply。我看了一下源码(请原谅我看不懂源码,到现在我都不知道bootstrap,channel,allocator,buffer之间是在哪些环节里组合上的),貌似WriteAsync完事之后会自动释放这个reply。 还请大佬有时间帮我解答一下。

另外,说一个题外话。dotnetty在国内的社区圈还是很有人气的,能和dotnetty一较高一下的就只有一个suppersocket了。但是suppersocket的设计理念个人感觉不及netty(毕竟是java社区那么多人智慧的积累)。所以,大佬为何不建一个dotnetty的交流群(当然,最终极有可能变成大佬单方面的疑难问题解答群。。。。)

cuteant commented 3 years ago

@kamikyo 看你的描述我无法判断客户端短线的原因,还有几个建议如下:

kamikyo commented 3 years ago

非常感谢大佬的指导,我会抽时间逐条进行实践。再次感谢!

FabianHummel commented 1 year ago

Sorry to bother, but using these options still "leak" memory (in 8kb steps), which is a lot better than 16mb at a time, but still not acceptable. I have created an issue demonstrating the problem.

Environment.SetEnvironmentVariable("io.netty.allocator.numHeapArenas", "0");
Environment.SetEnvironmentVariable("io.netty.allocator.numDirectArenas", "0");
...
.Option(ChannelOption.Allocator, UnpooledByteBufferAllocator.Default);