Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
kamikyo opened this issue Jun 7, 2021 · 8 comments

Comments

@kamikyo
Copy link

kamikyo commented Jun 7, 2021

大佬,我在使用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
Copy link
Author

kamikyo commented Jun 7, 2021

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

@cuteant
Copy link
Owner

cuteant commented Jun 9, 2021

这几天因为新冠在家里做自我隔离,没及时看信息: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
Copy link
Owner

cuteant commented Jun 9, 2021

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

@cuteant
Copy link
Owner

cuteant commented Jun 9, 2021

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

@kamikyo
Copy link
Author

kamikyo commented Jun 9, 2021

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

接入的客户端的行为的话就是非常的不稳定,连上几分钟,然后疯狂的输出一波流量,然后断线,下次再这样。不过监测下来的话整体流量是稳定的,线上(也就是服务器)大约在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
Copy link
Owner

cuteant commented Jun 9, 2021

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

  • 【提到的16MB那种内存占用块】这些都是PooledByteBufferAllocator分配的缓存池,为什么16M看源码
  • 除非并发量很小、业务处理极简,尽量不要在ChannelRead直接做业务处理,应该上消息队列,业务处理不要去占用netty的系统IO资源,推荐搭配Akka.Net
  • 服务端缓存池的最佳实践:PooledByteBuffer 用于系统I/O通信,UnpooledBuffer/ArrayPooledBuffer 用于业务
  • Buffer的使用尽量使用针对 Span、Memory扩展的方法来代替传统Byte Array的方法
  • 根据具体业务场景,做线程池、缓存池、socket各项参数调优
  • 阅读 《Netty权威指南》这本书,可对netty系统架构有个大体了解,还有就是github搜基于netty开发的项目
  • 我之前修改过eventstore 项目 EventStore-DotNetty-Fork,你可以参考下里边源码
  • 还有国内.net socket大神太多,这个项目只是微软官方放弃了DotNetty的维护,我只是站在巨人的肩膀上添砖加瓦,水平也是有限,等过段时间把现有的问题集中解决后,再考虑社区的相关问题,众人拾柴火焰高嘛,一个人的力量和精力真是有限

@kamikyo
Copy link
Author

kamikyo commented Jun 10, 2021

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

@kamikyo kamikyo closed this as completed Jun 10, 2021
@FabianHummel
Copy link

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);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants