-
https://www.easyice.cn/archives/341
2019年 Elastic开发者大会的时候我曾经说过 glibc 导致的堆外内存过多问题:
如果应用程序每次分配内存的时候都通过系统调用 mmap,sbrk等来分配,效率会很低,所以glibc 中实现了一个内存池,应用程序使用内存的时候通过glibc的内存池来提供,早期的 glibc 版本中,只有一个内存池,称为 main arena,在多线程场景中,每次分配和释放需要进行加锁。后来为了降低锁的粒度,从glibc 2.10版本开始引入了 thread arena,线程在申请内存的时候,glibc 为他创建一个 thread arena,这个内存池的大小一般是64M,thread arena被不被某个线程独占,全部的 thread arena被加入到环形链表,被所有线程共享使用。
环境变量 MALLOC_ARENA_MAX 用来控制进程可以创建的 thread arena 数量上限(默认为 cpu core*8),在 Hadoop 中这个值设置为 4,军义兄发现要设置为1,否则控制不住,但是不清楚原因,按照常规的理解,既然这个参数可以生效,就应该准确地控制 arena 数量,和数值设置成多少没关系,查阅资料没有找到相关描述。近日一个 Hadoop 的 NameNode 占用内存比预期值多了40G,pmap 看一下,多出来的基本就是 64M 的 thread arena,查看进程当前的环境变量值:
1
2
3
|
grep MALLOC_ARENA_MAX /proc/$pid/environ
|
确认为4,于是分析一下 MALLOC_ARENA_MAX 控制不住的原因。
这种情况通过简单的测试程序可以复现,看了一下 glibc 的相关代码,基本搞清楚了来龙去脉。
MALLOC_ARENA_MAX 控制不住的原因
如果 MALLOC_ARENA_MAX 设置为 n,当 n 个 thread arena 的内存空间全被占满的时候,线程新申请内存的时候优先创建新的 arena,不受 n 的限制,并且是无上限的。测试过程如下:
创建10个线程,每个线程申请64M 内存,不能直接 malloc 64M,因为glibc对于大块内存的申请直接 mmap
1
2
3
4
5
6
7
8
9
10
11
12
|
void* threadFunc(void* arg) {
for (int i = 0; i <64*1024*1024; i+=1024){
char* addr = (char*) malloc(1024);
}
}
int main() {
for (i = 0; i < 10; i++) {
ret = pthread_create(&t1, NULL, threadFunc, NULL);
}
}
|
编译执行这个程序:
1
2
3
|
MALLOC_ARENA_MAX=4 ./per_thread_arena
|
然后 pmap 查看进程内存空间:
1
2
3
|
pmap -x `pidof per_thread_arena` |sort -nrk3
|
标红的区域为进程创建的 thread arena,当配置的 n 个 thread arena空间不足时,glibc 有两种处理方式:
- 创建新的 thread arena
- 从 main arena 分配
绝大部分情况下,会创建新的 thread arena。
从实现上来说,arena_lookup查找分配区和 arena_lock锁分配区的时候并不关心该分配区的空间是否足够,如果 arena_lock 到的分配区空间不足,就创建新的thread arena
函数:sysmalloc
new_heap 创建新的 thread arena 时并不检查MALLOC_ARENA_MAX的配置值。
MALLOC_ARENA_MAX 可以生效的情况
与上面的结论相同,只有在thread arena没有被占满的情况下。为 MALLOC_ARENA_MAX设置的阈值可以控制住设定的数量。
将上面的测试程序修改为每个线程申请4M 内存,其他不变:
1
2
3
|
for (int i = 0; i <4*1024*1024; i+=1024){
|
运行时参数不变,pmap 查看进程内存情况,共创建了3个 thread arena
为什么 MALLOC_ARENA_MAX 设置为1 能够控制住
当设置为1,相当于禁用了 thread arena,arena_lookup每次都会返回 main arena, 不会创建任何 thread arena
总结
当 glibc 的内存池空间不足时,优先创建thread arena进行分配。但是在内存释放的时候,又需要等待 top chunk释放掉,才能释放一个连续的空间给操作系统,造成 thread arena 存在非常多的一个主要原因是存在很多生命周期比较长的对象,如果这些对象能够及时释放,虽然进程可能会在短时间创建许多 thread arena,但实际上并不占据 RES,所以根本问题还是那些具有长生命周期的对象。
当只使用 main arena 的情况下,虽然具有长生命周期的对象不变,但是内存池中的空间被重用的几率比多个 thread arena 更高,进程占据的的 RES 要相对少一些。
参考
https://paper.seebug.org/papers/Archive/refs/heap/glibc内存管理ptmalloc源代码分析.pdf
http://pwn4.fun/2016/04/11/深入理解glibc-malloc/
https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/