Glibc内存管理--ptmalloc2源代码分析(十一)

4. 问题分析及解决

通过前面几节对ptmalloc实现的粗略分析,尝试去分析和解决我们遇到的问题,我们系统遇到的问题是glibc内存暴增,现象是程序已经把内存返回给了Glibc库,但Glibc库却没有把内存归还给操作系统,最终导致系统内存耗尽,程序因为OOM被系统杀掉。

请参考3.2.2节对ptmalloc的设计假设与3.2.7节对ptmalloc的使用注意事项,原因有如下几点:

  1. 64位系统上使用默认的系统配置,也就是说ptmallocmmap分配阈值动态调整机制是开启的。我们的NoSql系统经常分配内存为2MB,并且这2MB的内存很快会被释放,在ptmalloc回收2MB内存时,ptmalloc的动态调整机制会认为2MB对我们的系统来说是一个临时的内存分配,每次都用系统调用mmap()向操作系统分配内存,ptmalloc认为这太低效了,于是把mmap的阈值设置成了2MB+4K,当下次再分配2MB的内存时,尽量从ptmalloc缓存的chunk中分配,缓存的chunk不能满足要求,才考虑调用mmap()进行分配,提高分配的效率。
  2. 系统中分配2M内存的地方主要有两处,一处是全局的内存cache,另一处是网络模块,网络模块每次分配2MB内存用于处理网络的请求,处理完成后就释放该内存。这可以看成是一个短生命周期的内存。内存cache每次分配2MB,但不确定什么时候释放,也不确定下次会什么时候会再分配2MB内存,但有一点可以确定,每次分配的2MB内存,要经过比较长的一段时间才会释放,所以可以看成是长生命周期的内存块,对于这些cache中的多个2M内存块没有使用free list管理,每次都是先从cachefree调用一个2M内存块,再从Glibc中分配一块新的2M内存块。Ptmalloc不擅长管理长生命周期的内存块,ptmalloc设计的假设中就明确假设缓存的内存块都用于短生命周期的内存分配,因为ptmalloc的内存收缩是从top chunk开始,如果与top chunk相邻的那个chunk在我们NoSql的内存池中没有释放,top chunk以下的空闲内存都无法返回给系统,即使这些空闲内存有几十个G也不行。
  3.   Glibc内存暴增的问题我们定位为全局内存池中的内存块长时间没有释放,其中还有一个原因就是全局内存池会不定期的分配内存,可能下次分配的内存是在top chunk分配的,分配以后又短时间不释放,导致top chunk升到了一个更高的虚拟地址空间,从而使ptmalloc中缓存的内存块更多,但无法返回给操作系统。
  4. 另一个原因就是进程的线程数越多,在高压力高并发环境下,频繁分配和释放内存,由于分配内存时锁争用更激烈,ptmalloc会为进程创建更多的分配区,由于我们的全局内存池的长时间不释放内存的缘故,会导致ptmalloc缓存的chunk数量增长得更快,从而更容易重现Glibc内存暴增的问题。在我们的ms上这个问题最为突出,就是这个原因。
  5. 内存池管理内存的方式导致Glibc大量的内存碎片。我们的内存池对于小于等于64K的内存分配,则从内存池中分配64K的内存块,如果内存池中没有,则调用malloc()分配64K的内存块,释放时,该64K的内存块加入内存中,永不还回给操作系统,对于大于64K的内存分配,调用malloc()分配,释放时调用free()函数换回给Glibc。这些大量的64K的内存块长时间存在于内存池中,导致了Glibc中缓存了大量的内存碎片不能释放回操作系统。比如:

64K

100K

64K

    假如应用层分配内存的顺序是64K100K64K,然后释放100K的内存块,Glibc会缓存这个100K的内存块,其中的两个64K内存块都在mempool中,一直不释放,如果下次再分配64K的内存,就会将100K的内存块拆分成64K36K的两个内存块,64K的内存块返回给应用层,并被mempool缓存,但剩下的36KGlibc缓存,再也不能被应用层分配了,因为应用层分配的最小内存为64K,这个36K的内存块就是内存碎片,这也是内存暴增的原因之一。

 

问题找到了,解决的办法可以参考如下几种:

  1. 禁用ptmallocmmap分配阈值动态调整机制。通过mallopt()设置M_TRIM_THRESHOLDM_MMAP_THRESHOLDM_TOP_PADM_MMAP_MAX中的任意一个,关闭mmap分配阈值动态调整机制,同时需要将mmap分配阈值设置为64K,大于64K的内存分配都使用mmap向系统分配,释放大于64K的内存将调用munmap释放回系统。但强烈建议不要这么做,这会大大降低ptmalloc的分配释放效率。因为系统调用mmap是串行的,操作系统需要对mmap分配内存加锁,而且操作系统对mmap的物理页强制清0很慢,请参看3.2.6选项配置相关描述。由于最初我们的系统的预分配优化做得不够好,关闭mmap的动态阈值调整机制后,chunkserverssd上的性能减少到原来的1/3,这种性能结果是无法让人接受的。
  2. 我们系统的关键问题出在全局内存池,它分配的内存是长生命周期的大内存块,通过前面的分析可知,对长生命周期的大内存块分配最好用mmap系统调用直接向操作系统分配,回收时用munmap返回给操作系统。比如内存池每次用mmap向操作系统分配8M或是更多的虚拟内存。如果非要用ptmallocmalloc函数分配内存,就得绕过ptmallocmmap分配阈值动态调整机制,mmap分配阈值在64位系统上的最大值为32M,如果分配的内存大于32M,可以保证malloc分配的内存肯定是用mmap向操作系统分配的,回收时free一定会返回给操作系统,而不会被ptmalloc缓存用于下一次分配。但是如果这样使用malloc分配的话,其实malloc就是mmap的简单封装,还不如直接使用mmap系统调用想操作系统分配内存来得简单,并且显式调用munmap回收分配的内存,根本不依赖ptmalloc的实现。
  3. 改写内存cache,使用free list管理所分配的内存块。使用预分配优化已有的代码,尽量在每个请求过程中少分配内存。并使用线程私有内存块来存放线程所使用的私有实例。这种解决办法也是暂时的。

从长远的设计来看,我们的系统也是分阶段执行的,每次网络请求都会分配2MB为单位内存,请求完成后释放请求锁分配的内存,内存池最适合这种情景的操作。我们的线程池至少需要包含对2MB和几种系统中常用分配大小的支持,采用与TCMalloc类似的无锁设计,使用线程私用变量的形式尽量减少分配时线程对锁的争用。或者直接使用TCMalloc,免去了很多的线程池设计考虑。        

猜你喜欢

转载自mqzhuang.iteye.com/blog/1014295