遭遇了 ASP.NET Core 3.0 的 OutOfMemoryException 问题

2019-07-25 10:13发布

已经生产环境中 3 次遭遇这个奇怪问题,在 docker 容器内存没有达到限制值的情况下竟然出现大量 OutOfMemoryException ,从而造成站点宕机,请问如何解决?

标签: .net core
1条回答
【Aperson】
2楼-- · 2019-07-25 10:46

楼主看我码了这么多字,赏一点豆子吧。

.NET Core3.0 对GC 改动的 Merge Request

我感觉有用的就这个

 PER_HEAP_ISOLATED
    size_t heap_hard_limit;

其余代码就不看了,一是看不懂,二是根本没发现对内存的限制代码(可能不在这次提交里),只是添加了获取容器是否设置内存限制的代码,和GCHeapHardLimit的宏定义,那就意味着,GCHeadHardLimit 只是一个阈值而已。由次可见,GCHeapHardLimit 属于GC的一个小马仔,何来干掉GC呢。其中缘由,请听我慢慢道来。

其中有一段很重要的总结,是.NET Core 3.0 GC的主要变化

    // + we never need to acquire new segments. This simplies the perf
    // calculations by a lot.
    //
    // + we now need a different definition of "end of seg" because we
    // need to make sure the total does not exceed the limit.
    //
    // + if we detect that we exceed the commit limit in the allocator we
    // wouldn't want to treat that as a normal commit failure because that
    // would mean we always do full compacting GCs.
  • 首先就是,在有内存限制的 Docker 容器中,GC不需要去问虚拟内存要新的Segments,因为初始化CLR的时候,把heapSegment都分配好了。在Server GC 模式下,一个核心 CPU 对应一个进程,对应一个heap, 而一个segment 大小 就是 limit / number of heaps

正常情况下,一个 heap 是可以有多个 segment 。而在 docker 剧本中,在 GC 初始化的时候,由于segment 初始化的大小是 limit / number of heaps(当然它也的大小也是会变化的,是动态的。七万不要认为segment大小是不变的)
所以程序启动时,如果分配CPU 是一核,那么就会分配一个heap ,一个heap 中初始化只有一个segment ,大小就是 limit 。请注意这里的 limitGCHeapHardLimit 不是同一个,这里的limit 应该就是容器内存限制。所以GC 堆大小是多少?初始化大小就是容器的内存限制limit

  • 特殊的判断segment结束标志,以判断是否超过GCHeapHardLimit

  • 如果发现,在 segment 中分配内存的时候超出了GCHeadHardLimit ,那么不会把这次分配看做失败的,所以就不会发生GC。结合上面两点的铺垫我们可以发现:

    1. 首先从上述代码我们可以发现GCHeapHardLimit只是一个数字而已。它就是一个阈值。

    2. 其次 GC堆的大小: 请注意,GC堆大小不是 HeapHardLimit 而是 容器内存限制 limit。GC 分配对象的时候,如果溢出了这个GCHeapHardLimit数字,GC 也会睁一只眼闭一只眼,否则只要溢出,它就要去整个heap中 GC 一遍。所以 GCHeadHardLimit 不是 GC堆申请的segment的大小,而是 GC 会管住自己的手脚,不能碰的东西咱尽量不要去碰,要是真碰了,也只有那么一次。

如果你的程序使用内存超出了GCHeapHardLimit阈值,segment 中还是有空余的,但是 GC 就是不用,它就是等着报OutOfMemoryException错误,而且docker根本杀不死你。

但是这并不代表GCHeapHardLimit的设置是不合理的,如果你的程序自己不能合理管理对象,或者你太抠门了,那么神仙也乏术。

但是人家说了!GCHeapHardLimit 是可以修改的!

 // Users can specify a hard limit for the GC heap via GCHeapHardLimit or
    // a percentage of the physical memory this process is allowed to use via
    // GCHeapHardLimitPercent. This is the maximum commit size the GC heap 
    // can consume.
    //
    // The way the hard limit is decided is:
    // 
    // If the GCHeapHardLimit config is specified that's the value we use;
    // else if the GCHeapHardLimitPercent config is specified we use that 
    // value;
    // else if the process is running inside a container with a memory limit,
    // the hard limit is 
    // max (20mb, 75% of the memory limit on the container).

如果你觉得GCHeapHardLimit 太气人了,那么就手动修改它的数值吧。

那么如何修改GCHeapHardLimit呢,https://github.com/dotnet/coreclr/issues/25767 中提到是可以通过修改环境变量COMPlus_GCHeapLimit来修改的

而之后,我们可以通过 runtime.config来配置这个参数,具体是哪个版本我还不得知。

查看更多
登录 后发表回答