已经生产环境中 3 次遭遇这个奇怪问题,在 docker 容器内存没有达到限制值的情况下竟然出现大量 OutOfMemoryException ,从而造成站点宕机,请问如何解决?
问题与 GCHeapHardLimit 有关,相关链接 Running with Server GC in a Small Container Scenario Part 1 – Hard Limit for the GC Heap
楼主看我码了这么多字,赏一点豆子吧。
.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.
Segments
,因为初始化CLR的时候,把heap
和Segment
都分配好了。在Server GC
模式下,一个核心 CPU 对应一个进程,对应一个heap
, 而一个segment
大小 就是 limit / number of heaps
。正常情况下,一个 heap
是可以有多个 segment
。而在 docker 剧本中,在 GC 初始化的时候,由于segment
初始化的大小是 limit / number of heaps
(当然它也的大小也是会变化的,是动态的。七万不要认为segment
大小是不变的)
所以程序启动时,如果分配CPU 是一核,那么就会分配一个heap
,一个heap
中初始化只有一个segment
,大小就是 limit
。请注意这里的 limit
和 GCHeapHardLimit
不是同一个,这里的limit
应该就是容器内存限制。所以GC 堆大小是多少?初始化大小就是容器的内存限制limit
。
特殊的判断segment结束标志,以判断是否超过GCHeapHardLimit
如果发现,在 segment
中分配内存的时候超出了GCHeadHardLimit
,那么不会把这次分配看做失败的,所以就不会发生GC。结合上面两点的铺垫我们可以发现:
首先从上述代码我们可以发现GCHeapHardLimit
只是一个数字而已。它就是一个阈值。
其次 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
来配置这个参数,具体是哪个版本我还不得知。