万字长文!解读 Curve 资源占用之内存管理
chunkserver 上内存无法释放
mds 出现内存缓慢增长的现象
内存问题在开发阶段大多很难发现,测试阶段大压力稳定性测试(持续跑7*24小时以上)、异常测试往往比较容易出问题,当然这还需要我们在测试阶段足够仔细,除了关注 io 相关指标外,还要关注服务端内存/ CPU /网卡等资源使用情况以及采集的 metric 是否符合预期。比如上述问题 mds 内存缓慢增长 ,如果只关注 io 是否正常,在测试阶段是无法发现的。内存问题出现后定位也不容易,尤其在软件规模较大的情况下。
本文主要是从开发者的角度来谈 Curve 中的内存管理,不会过度强调内存管理理论,目的是把我们在软件开发过程中对 Linux 内存管理的认知、内存问题分析的一些方法分享给大家。本文会从以下几个方面展开:
内存布局。结合 Curve 软件说明内存布局。 内存分配策略。说明内存分配器的必要性,以及需要解决的问题和具有的特点,然后通过举例说明其中一个内存分配器的内存管理方法。
Curve 的内存管理。介绍当前 Curve 软件内存分配器的选择及原因。
Curve 是云原生计算基金会 (CNCF) Sandbox 项目,由网易主导开源的高性能、易运维、云原生的分布式存储系统,由块存储 CurveBS 和文件系统 CurveFS 两部分组成。
在说内存管理之前,首先简要介绍下内存布局相关知识。软件在运行时需要占用一定量的内存用来存放一些数据,但进程并不直接与存放数据的物理内存打交道,而是直接操作虚拟内存。物理内存是真实的存在,就是内存条;虚拟内存为进程隐藏了物理内存这一概念,为进程提供了简洁易用的接口和更加复杂的功能。本文说的内存管理是指虚拟内存管理。为什么需要抽象一层虚拟内存?虚拟内存和物理内存是如何映射管理的?物理寻址是怎么的?这些虚拟内存更下层的问题不在本文讨论范围。
Linux 为每个进程维护了一个单独的虚拟地址空间,包括两个部分进程虚拟存储器(用户空间)和内核虚拟存储器(内核空间),本文主要讨论进程可操作的用户空间,形式如下图。
现在我们使用
pmap 查看运行中的 curve-mds 虚拟空间的分布。
pmap 用于查看进程的内存映像信息,该命令读取的是
/proc/[pid]/maps 中的信息。
// pmap -X {进程id} 查看进程内存分布
sudo pmap -X 2804620
// pmap 获取的 curve-mds 内存分布有很多项
Address Perm Offset Device Inode Size Rss Pss Referenced Anonymous ShmemPmdMapped Shared_Hugetlb Private_Hugetlb Swap SwapPss Locked Mapping
// 为了方便展示这里把从 Pss 后面的数值删除了, 中间部分地址做了省略
2804620: /usr/bin/curve-mds -confPath=/etc/curve/mds.conf -mdsAddr=127.0.0.1:6666 -log_dir=/data/log/curve/mds -graceful_quit_on_sigterm=true -stderrthreshold=3
Address Perm Offset Device Inode Size Rss Pss Mapping
c000000000 rw-p 00000000 00:00 0 65536 1852 1852
559f0e2b9000 r-xp 00000000 41:42 37763836 9112 6296 6296 curve-mds
559f0eb9f000 r--p 008e5000 41:42 37763836 136 136 136 curve-mds
559f0ebc1000 rw-p 00907000 41:42 37763836 4 4 4 curve-mds
559f0ebc2000 rw-p 00000000 00:00 0 10040 4244 4244
559f1110a000 rw-p 00000000 00:00 0 2912 2596 2596 [heap]
7f6124000000 rw-p 00000000 00:00 0 156 156 156
7f6124027000 ---p 00000000 00:00 0 65380 0 0
7f612b7ff000 ---p 00000000 00:00 0 4 0 0
7f612b800000 rw-p 00000000 00:00 0 8192 8 8
7f612c000000 rw-p 00000000 00:00 0 132 4 4
7f612c021000 ---p 00000000 00:00 0 65404 0 0
.....
7f6188cff000 ---p 0026c000 41:42 37750237 2044 0 0
7f61895b7000 r-xp 00000000 41:42 50201214 96 96 0 libpthread-2.24.so
7f61895cf000 ---p 00018000 41:42 50201214 2044 0 0 libpthread-2.24.so
7f61897ce000 r--p 00017000 41:42 50201214 4 4 4 libpthread-2.24.so
7f61897cf000 rw-p 00018000 41:42 50201214 4 4 4 libpthread-2.24.so
7f61897d0000 rw-p 00000000 00:00 0 16 4 4
7f61897d4000 r-xp 00000000 41:42 50200647 16 16 0 libuuid.so.1.3.0
7f61897d8000 ---p 00004000 41:42 50200647 2044 0 0 libuuid.so.1.3.0
7f61899d7000 r--p 00003000 41:42 50200647 4 4 4 libuuid.so.1.3.0
7f61899d8000 rw-p 00004000 41:42 50200647 4 4 4 libuuid.so.1.3.0
7f61899d9000 r-xp 00000000 41:42 37617895 9672 8904 8904 libetcdclient.so
7f618a34b000 ---p 00972000 41:42 37617895 2048 0 0 libetcdclient.so
7f618a54b000 r--p 00972000 41:42 37617895 6556 5664 5664 libetcdclient.so
7f618abb2000 rw-p 00fd9000 41:42 37617895 292 252 252 libetcdclient.so
7f618abfb000 rw-p 00000000 00:00 0 140 60 60
7f618ac1e000 r-xp 00000000 41:42 50201195 140 136 0 ld-2.24.so
7f618ac4a000 rw-p 00000000 00:00 0 1964 1236 1236
7f618ae41000 r--p 00023000 41:42 50201195 4 4 4 ld-2.24.so
7f618ae42000 rw-p 00024000 41:42 50201195 4 4 4 ld-2.24.so
7f618ae43000 rw-p 00000000 00:00 0 4 4 4
7fffffd19000 rw-p 00000000 00:00 0 132 24 24 [stack]
7fffffdec000 r--p 00000000 00:00 0 8 0 0 [vvar]
7fffffdee000 r-xp 00000000 00:00 0 8 4 0 [vdso]
ffffffffff600000 r-xp 00000000 00:00 0 4 0 0 [vsyscall]
======= ===== =====
1709344 42800 37113
上面输出中进程实际占用的空间是从 0x559f0e2b9000 开始,
上面输出中进程实际占用的空间是从 0x559f0e2b9000 开始,不是内存分布图上画的 0x40000000。这是因为地址空间分布随机化(ASLR),它的作用是随机生成进程地址空间(例如栈、库或者堆)的关键部分的起始地址,目的是增强系统安全性、避免恶意程序对已知地址攻击。Linux 中 /proc/sys/kernel/randomize_va_space 的值为 1 或 2 表示地址空间随机化已开启,数值1、2的区别在于随机化的关键部分不同;0表示关闭。 接下来 0x559f0e2b9000 0x559f0eb9f000 0x559f0ebc1000 三个地址起始对应的文件都是curve-mds ,但是对该文件的拥有不同的权限,各字母代表的权限 r-读 w-写 x-可执行 p-私有 s-共享 。curve-mds 是 elf 类型文件,从内容的角度看,它包含代码段、数据段、BSS段等;从装载到内存角度看,操作系统不关心各段所包含的内容,只关心跟装载相关的问题,主要是权限,所以操作系统会把相同权限的段合并在一起去加载,就是我们这里看到的以代码段为代表的权限为可读可执行的段、以只读数据为代表的权限为只读的段、以数据段和 BSS 段为代表的权限为可读可写的段。 再往下 0x559f1110a000 开始,对应上图的运行时堆,运行时动态分配的内存会在这上面进行 。我们发现也是在.bss 段的结束位置进行了随机偏移。 接着 0x7f6124000000 开始,对应的是上图 mmap 内存映射区域,这一区域包含动态库、用户申请的大片内存等。到这里我们可以看到 Heap 和 Memory Mapping Region 都可以用于程序中使用 malloc 动态分配的内存,在下一节内存分配策略中会有展开,也是本文关注重点。 接着 0x7fffffd19000 开始是栈空间,一般有数兆字节。 最后 vvar、vdso、vsyscall 区域是为了实现虚拟函数调用以加速部分系统调用,使得程序可以不进入内核态1直接调用系统调用。这里不具体展开。
brk 分配的区域对应堆 heap
mmap 分配的区域对应 Memory Mapping Region
如果让每个开发者在软件开发时都直接使用系统调 brk 和 mmap 用去分配释放内存,那开发效率将会变得很低,而且也很容易出错。一般来说我们在开发中都会直接使用内存管理库,当前主流的内存管理器有三种:ptmalloc tcmalloc jemalloc , 都提供 malloc, free 接口,glibc 默认使用ptmalloc。这些库的作用是管理它通过系统调用获得的内存区域,一般来说一个优秀的通用内存分配器应该具有以下特征:
额外的空间损耗量尽量少。比如应用程序只需要5k内存,结果分配器给他分配了10k,会造成空间的浪费。
分配的速度尽可能快。 尽量避免内存碎片。下面我们结合图来直观的感受下内存碎片。 通用性、兼容性、可移植性、易调试。
malloc(30k) 通过系统调用 brk 扩展堆顶的方式分配内存。 malloc(20k) 通过系统调用 brk 继续扩展堆顶。 malloc(200k) 默认情况下请求内存大于 128K (由 M_MMAP_THRESHOLD 确定,默认大小为128K,可以调整),就利用系统调用 mmap分配内存。 free(30k) 这部分空间并没有归还给系统,而是 ptmalloc 管理着。由1、2两步的 malloc 可以看出,我们分配空间的时候调用 brk 进行堆顶扩展,那归还空间给系统是相反操作即收缩堆顶。这里由于第二步 malloc(20k) 的空间并未释放,所以此时堆顶无法收缩。这部分空间是可以被再分配的,比如此时 malloc(10k),那可以从这里分配 10k 空间,而不需要通过 brk 去申请。考虑这样一种情况,堆顶的空间一直被占用,堆顶向下的空间有部分被应用程序释放但由于空间不够没有再被使用,就会形成 内存碎片。 free(20k) 这部分空间应用程序释放后,ptmalloc 会把刚才的 20k 和 30k 的区域合并,如果堆顶空闲超过 M_TRIM_THREASHOLD ,会把这块区域收缩归还给操作系统。 free(200k) mmap分配出来的空间会直接归还给系统。
那对于多线程程序,ptmalloc 又是怎么区分配的?多线程情况下需要处理各线程间的竞争,如果还是按照之前的方式,小于 HEAP_MAX_SIZE ( 64 位系统默认大小为 64M )的空间使用 brk 扩展堆顶, 大于 HEAP_MAX_SIZE 的空间使用 mmap 申请,那对于线程数量较多的程序,如果每个线程上存在比较频繁的内存分配操作,竞争会很激烈。ptmalloc 的方法是使用多个分配区域,包含两种类型分配区:主分配区 和 动态分配区。
主分配区:会在 Heap 和 Memory Mapping Region 这两个区域分配内存;
动态分配区:在 Memory Mapping Region 区域分配内存,在 64 位系统中默认每次申请的大小位。Main 线程和先执行 malloc 的线程使用不同的动态分配区,动态分配区的数量一旦增加就不会减少了。动态分配区的数量对于 32 位系统最多是 ( 2 number of cores + 1 ) 个,对于 64 位系统最多是( 8 number of cores + 1 )个。
// 共有三个线程
// 主线程:分配一次 4k 空间
// 线程1: 分配 100 次 4k 空间
// 线程2: 分配 100 次 4k 空间
void* threadFunc(void* id) {
std::vector<char *> malloclist;
for (int i = 0; i < 100; i++) {
malloclist.emplace_back((char*) malloc(1024 * 4));
}
sleep(300); // 这里等待是为查看内存分布
}
int main() {
pthread_t t1,t2;
int id1 = 1;
int id2 = 2;
void* s;
int ret;
char* addr;
addr = (char*) malloc(4 * 1024);
pthread_create(&t1, NULL, threadFunc, (void *) &id1);
pthread_create(&t2, NULL, threadFunc, (void *) &id2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
我们用 pmap 查看下该程序的内存分布情况:
741545: ./memory_test
Address Perm Offset Device Inode Size Rss Pss Mapping
56127705a000 r-xp 00000000 08:02 62259273 4 4 4 memory_test
56127725a000 r--p 00000000 08:02 62259273 4 4 4 memory_test
56127725b000 rw-p 00001000 08:02 62259273 4 4 4 memory_test
5612784b9000 rw-p 00000000 00:00 0 132 8 8 [heap]
**7f0df0000000 rw-p 00000000 00:00 0 404 404 404
7f0df0065000 ---p 00000000 00:00 0 65132 0 0
7f0df8000000 rw-p 00000000 00:00 0 404 404 404
7f0df8065000 ---p 00000000 00:00 0 65132 0 0**
7f0dff467000 ---p 00000000 00:00 0 4 0 0
7f0dff468000 rw-p 00000000 00:00 0 8192 8 8
7f0dffc68000 ---p 00000000 00:00 0 4 0 0
7f0dffc69000 rw-p 00000000 00:00 0 8192 8 8
7f0e00469000 r-xp 00000000 08:02 50856517 1620 1052 9 libc-2.24.so
7f0e005fe000 ---p 00195000 08:02 50856517 2048 0 0 libc-2.24.so
7f0e007fe000 r--p 00195000 08:02 50856517 16 16 16 libc-2.24.so
7f0e00802000 rw-p 00199000 08:02 50856517 8 8 8 libc-2.24.so
7f0e00804000 rw-p 00000000 00:00 0 16 12 12
7f0e00808000 r-xp 00000000 08:02 50856539 96 96 1 libpthread-2.24.so
7f0e00820000 ---p 00018000 08:02 50856539 2044 0 0 libpthread-2.24.so
7f0e00a1f000 r--p 00017000 08:02 50856539 4 4 4 libpthread-2.24.so
7f0e00a20000 rw-p 00018000 08:02 50856539 4 4 4 libpthread-2.24.so
7f0e00a21000 rw-p 00000000 00:00 0 16 4 4
7f0e00a25000 r-xp 00000000 08:02 50856513 140 140 1 ld-2.24.so
7f0e00c31000 rw-p 00000000 00:00 0 16 16 16
7f0e00c48000 r--p 00023000 08:02 50856513 4 4 4 ld-2.24.so
7f0e00c49000 rw-p 00024000 08:02 50856513 4 4 4 ld-2.24.so
7f0e00c4a000 rw-p 00000000 00:00 0 4 4 4
7ffe340be000 rw-p 00000000 00:00 0 132 12 12 [stack]
7ffe3415c000 r--p 00000000 00:00 0 8 0 0 [vvar]
7ffe3415e000 r-xp 00000000 00:00 0 8 4 0 [vdso]
ffffffffff600000 r-xp 00000000 00:00 0 4 0 0 [vsyscall]
====== ==== ===
153800 2224 943
关注上面加粗的部分,蓝色区域加起来是 65536K,其中有 404K 是 rw-p (可读可写)权限,65132K 是 —-p (不可读写)权限;黄色区域类似。两个线程分配的时 ptmalloc 分别给了 动态分区,并且每次申请 64M 内存,再从这 64M 中切分出一部分给应用程序。
这里还有一个有意思的现象:我们用 strace -f -e "brk, mmap, munmap" -p {pid} 去跟踪程序查看下 malloc 中的系统调用:
mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f624a169000
strace: Process 774601 attached
[pid 774018] mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f6249968000
[pid 774601] mmap(NULL, 134217728, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x7f6241968000
[pid 774601] munmap(0x7f6241968000, 40468480strace: Process 774602 attached
) = 0
[pid 774601] munmap(0x7f6248000000, 26640384) = 0
[pid 774602] mmap(NULL, 134217728, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x7f623c000000
[pid 774602] munmap(0x7f6240000000, 67108864) = 0
这里主线程 [774018] 要求分配了 8M+4k 空间;线程1 [774601] 先 mmap 了 128M 空间,再归还了 0x7f6241968000 为起始地址的 40468480 字节 和 0x7f6248000000 为起始地址的 26640384 字节,那剩余的部分是 0x7F6244000000 ~ 0x7F6248000000。先申请再归还是为了让分配的这部分内存的起止地址是字节对齐的。
Curve 的内存管理
Curve 中选择了两种分配器:ptmalloc 和 jemalloc 。其中 MDS 使用默认的 ptmalloc,Chunkserver 和 Client 端使用 jemalloc。
本文开头提到的两个问题在这里进行说明。首先是MDS内存缓慢增长,现象是每天增长 3G。这个问题分析的过程如下:
1、首先是使用 pmap 查看内存分布。 我们用 pmap 查看内存缓慢增长的 curve-mds 内存分配情况,发现在 Memory Mapping Region 存在着大量分配的 64M 内存,且观察一段时间后都不释放还在一直分配。从这里怀疑存在内存泄露。
2815659: /usr/bin/curve-mds -confPath=/etc/curve/mds.conf -mdsAddr=*.*.*.*:6666 -log_dir=/data/log/curve/mds -graceful_quit_on_sigterm=true -stderrthreshold=3
Address Perm Offset Device Inode Size Rss Pss Referenced Anonymous ShmemPmdMapped Shared_Hugetlb Private_Hugetlb Swap SwapPss Locked Mapping
c000000000 rw-p 00000000 00:00 0 8192 4988 4988 4988 4988 0 0 0 0 0 0
c000800000 rw-p 00000000 00:00 0 57344 0 0 0 0 0 0 0 0 0 0
557c5abb6000 r-xp 00000000 41:42 55845493 9112 6488 6488 6488 0 0 0 0 0 0 0 /usr/bin/curve-mds
557c5b49c000 r--p 008e5000 41:42 55845493 136 136 136 136 136 0 0 0 0 0 0 /usr/bin/curve-mds
557c5b4be000 rw-p 00907000 41:42 55845493 4 4 4 4 4 0 0 0 0 0 0 /usr/bin/curve-mds
557c5b4bf000 rw-p 00000000 00:00 0 10040 2224 2224 2224 2224 0 0 0 0 0 0
557c5cce2000 rw-p 00000000 00:00 0 5604 5252 5252 5252 5252 0 0 0 0 0 0 [heap]
7f837f7ff000 ---p 00000000 00:00 0 4 0 0 0 0 0 0 0 0 0 0
7f837f800000 rw-p 00000000 00:00 0 8192 8 8 8 8 0 0 0 0 0 0
7f8380000000 rw-p 00000000 00:00 0 132 12 12 12 12 0 0 0 0 0 0
......
7fbcf8000000 rw-p 00000000 00:00 0 65536 65536 65536 65536 65536 0 0 0 0 0 0
7fbcfc000000 rw-p 00000000 00:00 0 65536 65536 65536 65528 65536 0 0 0 0 0 0
7fbd04000000 rw-p 00000000 00:00 0 65536 65536 65536 65520 65536 0 0 0 0 0 0
7fbd08000000 rw-p 00000000 00:00 0 65536 65536 65536 65536 65536 0 0 0 0 0 0
7fbd0c000000 rw-p 00000000 00:00 0 65536 65536 65536 65528 65536 0 0 0 0 0 0
7fbd10000000 rw-p 00000000 00:00 0 65536 65536 65536 65524 65536 0 0 0 0 0 0
7fbd14000000 rw-p 00000000 00:00 0 65536 65536 65536 65532 65536 0 0 0 0 0 0
7fbd18000000 rw-p 00000000 00:00 0 65536 65536 65536 65536 65536 0 0 0 0 0 0
7fbd1c000000 rw-p 00000000 00:00 0 65536 65536 65536 65524 65536 0 0 0 0 0 0
7fbd20000000 rw-p 00000000 00:00 0 65536 65536 65536 65524 65536 0 0 0 0 0 0
7fbd24000000 rw-p 00000000 00:00 0 65536 65536 65536 65512 65536 0 0 0 0 0 0
7fbd28000000 rw-p 00000000 00:00 0 65536 65536 65536 65520 65536 0 0 0 0 0 0
7fbd2c000000 rw-p 00000000 00:00 0 65536 65536 65536 65520 65536 0 0 0 0 0 0
7fbd30000000 rw-p 00000000 00:00 0 65536 65536 65536 65516 65536 0 0 0 0 0 0
......
======= ====== ====== ========== ========= ============== ============== =============== ==== ======= ======
7814504 272928 263610 272928 248772 0 0 0 0 0 0 KB
然后查看应用上请求的压力情况。 查看MDS 上相关的业务 metric,发现 MDS 上的压力都很小,一些控制面 rpc 的 iops 在几百左右,不应该是业务压力较大导致的。
接下来查看 curve-mds 部分 64M 内存上的数据。 使用 gdb -p {pid} attach 跟踪线程, dump memory mem.bin {addr1} {addr2} 获取指定地址段的内存,然后查看这部分内存内容,基本确定几个怀疑点。以下是 dump 出来的部分内容,很多 01 开头的字符串,还有一些metric 信息等信息,基本可以分为几块内容。
根据这几个点去排查代码,看是否有内存泄露。 其中有大量 01、02开头的字符串是我们对文件信息 FileInfo 和 映射信息 SegmentInfo 转化为 key-value 进行持久化中 key 的编码。那会分配内存的就是 GetFileInfo、GetSegmentInfo、ListSegmentInfo 等接口,排查这些接口后发现一处内存泄露。MDS 中使用的 Etcd 存储元数据,其中用 cgo 封装了 go 版本的 etcdclient。go 中的数据传递给 C 时,这部分内存是需要由 C 管理释放了,下面有一处写代码时忘记释放了。
内存泄露还有一个比较好用的工具:Vargrind 。在初期定位的时候尝试过,由于缺乏经验,没有看出相应的问题。后面复盘的时候我们又尝试抓了一次,加入了比较多的参数,命令如下:
valgrind --tool=memcheck --leak-check=full --show-reachable=yes
--trace-children=yes --track-origins=yes /usr/bin/curve-mds
-confPath=/etc/curve/mds.conf -mdsAddr=*.*.*.*:6666
-log_dir=/data/log/curve/mds -graceful_quit_on_sigterm=true -stderrthreshold=3
结果很长,截取了部分,在标黄区域有比较明显的提示:
==1559781== 13,440 bytes in 40 blocks are possibly lost in loss record 2,296 of 2,367
==1559781== at 0x4C2DBC5: calloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==1559781== by 0x4011E31: allocate_dtv (dl-tls.c:322)
==1559781== by 0x40127BD: _dl_allocate_tls (dl-tls.c:539)
==1559781== by 0x628A189: allocate_stack (allocatestack.c:584)
==1559781== by 0x628A189: pthread_create@ .2.5 (pthread_create.c:663)
==1559781== by 0x54F88D: bthread::TaskControl::add_workers(int) (in /usr/bin/curve-mds)
==1559781== by 0x54147B: bthread_setconcurrency (in /usr/bin/curve-mds)
==1559781== by 0x35A0C9: brpc::Server::Init(brpc::ServerOptions const*) (in /usr/bin/curve-mds)
==1559781== by 0x35AAD7: brpc::Server::StartInternal(in_addr const&, brpc::PortRange const&, brpc::ServerOptions const*) (in /usr/bin/curve-mds)
==1559781== by 0x35BCBC: brpc::Server::Start(butil::EndPoint const&, brpc::ServerOptions const*) (in /usr/bin/curve-mds)
==1559781== by 0x35BD60: brpc::Server::Start(char const*, brpc::ServerOptions const*) (in /usr/bin/curve-mds)
==1559781== by 0x19442B: curve::mds::MDS::StartServer() (in /usr/bin/curve-mds)
==1559781== by 0x194A61: curve::mds::MDS::Run() (in /usr/bin/curve-mds)
==1559781==
**==1559781== 85,608 bytes in 4,125 blocks are definitely lost in loss record 2,333 of 2,367
==1559781== at 0x4C2BBAF: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==1559781== by 0x56D6863: _cgo_728933f4f8ea_Cfunc__Cmalloc (_cgo_export.c:502)
==1559781== by 0x51A9817: runtime.asmcgocall (/home/xuchaojie/github/curve/thirdparties/etcdclient/tmp/go/src/runtime/asm_amd64.s:635)
==1559781== by 0xC00000077F: ???
==1559781== by 0x300000005: ???**
==1559781==
==1559781== LEAK SUMMARY:
==1559781== definitely lost: 85,655 bytes in 4,128 blocks
==1559781== indirectly lost: 0 bytes in 0 blocks
==1559781== possibly lost: 94,392 bytes in 125 blocks
==1559781== still reachable: 27,012,904 bytes in 13,518 blocks
==1559781== of which reachable via heuristic:
==1559781== newarray : 3,136 bytes in 2 blocks
==1559781== multipleinheritance: 1,616 bytes in 1 blocks
==1559781== suppressed: 0 bytes in 0 blocks
==1559781== Reachable blocks (those to which a pointer was found) are not shown.
==1559781== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==1559781==
==1559781== For counts of detected and suppressed errors, rerun with: -v
==1559781== ERROR SUMMARY: 10331 errors from 988 contexts (suppressed: 0 from 0)
Chunkserver 端不是开始就使用 jemalloc 的,最初也是用的默认的 ptmalloc。换成 jemalloc 是本文开始提到的 Chunkserver 在测试过程中出现内存无法释放的问题,这个问题的现象是:chunkserver的内存在 2 个 小时内增长很快,一共增长了 50G 左右,但后面并未释放。这个问题分析的过程如下:
首先分析内存中数据来源。 这一点跟 MDS 不同,MDS 上都是控制面的请求以及一些元数据的缓存。而Chunkserver 上的内存增长一般来自两个地方:一是用户发送的请求,二是 copyset 的 leader 和 follower 之间同步数据。这两个都会涉及到 brpc 模块。brpc 的内存管理有两个模块 IOBuf 和 ResourcePool。IOBuf 中的空间一般用于存放用户数据,ResourcePool 管理 socket、bthread_id 等对象,管理的内存对象单位是 64K 。这两个模块详细的数据结构这里就不展开说明了,感兴趣的小伙伴可以阅读 brpc 文档:brpc-innternal, ResourcePool相关的可以看下源码。
查看对应模块的一些趋势指标。 观察这两个模块的metric,发现 IOBuf 和 ResourcePool 这段时间内占用的内存都有相同的增长趋势。IOBuf 后面将占用的内存归还给 ptmalloc, ResourcePool 中管理的内存不会归还给 ptmalloc 而是自己管理。
分析验证。结合第二节的内存分配策略,如果堆顶的空间一直被占用,那堆顶向下的空间也是无法被释放的。仍然可以使用 pmap 查看当前堆上内存的大小以及内存的权限(是否有很多 —-p 权限的内存)来确定猜想。因此后面 Chunkserver 使用了 jemalloc。这里可以看到在多线程情况下,如果一部分内存被应用长期持有,使用 ptmalloc 也许就会遇到内存无法释放的问题。
这里对 MDS 和 Chunkserver 出现的两个问题进行了总结,一方面想说明 Curve 选择不同内存分配器的原因。对于一个从 0 到 1 的项目,代码开发之初选择内存分配器不是一个特别重要的事情,如果有比较多的开发和解决类似内存问题的经验,开始做出评估是好的;但如果没有,可以先选择一个,出现问题或者到需要优化内存性能的时候再去分析;另外一方面希望可以给遇到类似内存问题的小伙伴一些定位的思路~
参考①:
ptmalloc,tcmalloc,jemalloc对比分析
https://www.cyningsun.com/07-07-2018/memory-allocator-contrasts.html
参考②:
十问linux虚拟内存管理(glibc)
https://zhuanlan.zhihu.com/p/26137521?utm_source=wechat_session&utm_medium=social&utm_oi=552541092948684800&utm_content=sec
参考③:
Go语言使用cgo时的内存管理笔记
https://www.pengrl.com/p/29054/
参考④:
深入理解计算机系统 [美] 兰德尔 E.布莱恩特(Randal E.·Bryant) 著,龚奕利,贺莲 译