背景
glibc是linux下默认使用的标准C库。用户态程序,一般默认会使用其中的malloc/free接口进行内存的动态申请和释放,而其malloc使用的内存池管理方式则为ptmalloc,其他常见的开源内存池实现还有tcmalloc和jemalloc(相比于glibc ptmalloc,tcmalloc和jemalloc是提供了粒度更细小的内存池)。
数据面存在进程cfgdbd和dp,其中cfgdbd用于接收控制面配置,并将其存于堆内存中,再下发给转发面dp。当数据面进程重启时,控制面进程将会把所有配置再推送一遍到cfgdbd上(全同步),cfgdbd会生成一棵树形结构保存配置(全同步配置树),待控制面所有进程下完配置(全同步配置树构建完成),cfgdbd便会将这些配置再推送到dp,同时生成一棵配置树,等所有配置都下发完成,将释放全同步配置树。
问题
这天发现cfgdbd进程内存占用一直处于很高,将页面上的配置基本都删除后(配置树基本清空),cfgdbd内存占用依然很大。
排查过程
1.怀疑存在内存泄漏:
使用valgrind、libc_malloc/libc_free hook等方式复现检查,发现可以必现问题,但均未发现可疑的泄漏点
hook代码栗子如下:
// gcc -o fun2 fun2.c -ldl -g
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <link.h>
typedef void *(*malloc_t)(size_t size);
malloc_t malloc_f = NULL;
typedef void (*free_t)(void *ptr);
free_t free_f = NULL;
int enable_malloc_hook = 1;
int enable_free_hook = 1;
//解析地址
void* converToELF(void *addr) {
Dl_info info;
struct link_map *link;
dladdr1(addr, &info, (void **)&link, RTLD_DL_LINKMAP);
// printf("%p\n", (void *)(size_t)addr - link->l_addr);
return (void *)((size_t)addr - link->l_addr);
}
void *malloc(size_t size){
void *ptr = NULL;
char buffer[128] = {0};
if (enable_malloc_hook ){
enable_malloc_hook = 0;
ptr = malloc_f(size);
void *caller = __builtin_return_address(0);
sprintf(buffer, "./memory/%p.memory", ptr);
FILE *fp = fopen(buffer, "w");
// converToELF(caller)
fprintf(fp, "[+] caller: %p, addr: %p, size: %ld\n", converToELF(caller), ptr, size);
fflush(fp);
fclose(fp);
enable_malloc_hook = 1;
}
else {
ptr = malloc_f(size);
}
return ptr;
}
void free(void *ptr){
if (enable_free_hook ){
enable_free_hook = 0;
char buffer[128] = {0};
sprintf(buffer, "./memory/%p.memory", ptr);
if (unlink(buffer) < 0){
printf("double free: %p\n", ptr);
return;
}
free_f(ptr);
enable_free_hook = 1;
}
else {
free_f(ptr);
}
}
void init_hook(){
if (!malloc_f){
malloc_f = dlsym(RTLD_NEXT, "malloc");
}
if (!free_f){
free_f = dlsym(RTLD_NEXT, "free");
}
}
int main(){
init_hook();
void *p1 = malloc(5);
void *p2 = malloc(18);
void *p3 = malloc(15);
free(p1);
free(p3);
}
2.使用tcmalloc替换glibc:
LD_LIBRARY_PATH="/usr/lib/libtcmalloc.so:/sf/sdn/lib/:/sf/vn/lib/:/sf/lib/" /sf/sdn/bin/cfgdbd -V >> /sf/log/sdn/cfgdbd.out 2>&1
发现现象变成了非必现,于是往内存池实现方向上去思考。
3.使用malloc_trim(0)手动回收内存:
通过查阅资料,发现glibc实现的ptmalloc在某些场景下存在内存站岗问题(简单来说就是小块内存采用堆栈结构从低地址到高地址申请,若free时,高地址的内存并未被free,则低地址即使free后也不会被归还给系统,而是被glibc缓存了起来):
https://cloud.tencent.com/developer/article/1138651
https://wenfh2020.com/2021/04/08/glibc-memory-leak/
https://blog.csdn.net/u013259321/article/details/112031002
在环境上验证,全同步树是否站岗:
再验证配置树是否也站岗了:
至此确认了该问题原因:cfgdbd直接申请堆内存构建树形结构存放配置,此种做法本就与glibc的内存池实现不可完美兼容。
解决方案
1.尝试修改glibc malloc可配置的参数(默认未调用mallopt,glibc的参数会动态改变 ):
将M_TRIM_THRESHOLD直接调整成1K,M_MMAP_THRESHOLD不做修改
//全同步结束后,也还是有站岗,调malloc_trim(0)还能回收10个G
M_TRIM_THRESHOLD和M_MMAP_THRESHOLD都修改成1K
//全同步内存不站岗,但配置内存还会站岗
将M_TRIM_THRESHOLD设置为1,M_MMAPTHRESHOLD不修改
//全同步依然站岗,mp.top_pad非0,导致没法free一次就trim一次所有内存
尝试如下修改
//全同步依然站岗
2.换tcmalloc配合修改些参数,看是否能彻底解决该问题:
只是直接替换成tcmalloc
//存在站岗
配置下下tcmalloc的FLAGS_tcmalloc_release_rate
//无效,也没看到tcmalloc cpp实现哪里有跟它关联上
再试下tcmalloc的aggressive_memory_decommit
//有效,但有点激进了,相当于完全没有缓存(几乎可以等价于glibc每次free调用malloc_trim(0)),频繁申请/释放可能会引发其他问题(目前dp转发进程也用了tcmalloc,且其pageheap向系统申请/释放内存均需要等锁,怕影响到转发):https://www.infoq.cn/article/xwijC1Afv3CN5zNj2sDi
3.综上所述,还是考虑定时/定点调用malloc_trim(0)
最终采用代码里定点+阈值直接调malloc_trim(0)回收内存(归还时需上互斥锁,且需要进行系统调用,太频繁将大量浪费CPU资源)
//如果需要再做优雅点,就弄个free hook,free次数达到阈值,就调一次malloc_trim(0),且阈值可通过cli接口修改
总结
该问题的排查给日后内存异常占用问题的排查,提供了一个方向:
有此类内存占用异常的问题,可以先在现场gdb获取下malloc_state(),再使用malloc_trim(0)手动回收下内存,通过此操作优先排除glibc内存站岗问题,提高排查效率。
设计中同样需要考虑到该问题,如果使用非自研的内存池,则需要做好内存回收框架。