issue 链接:
从上述 issue 的详细描述可以看到,这是一个疑似内存泄漏问题,该用户使用 TDengine OSS 从 3.0.1.6 版本开始一直升级测到 3.0.2.2 版本,内存泄漏问题一直存在。该问题简化总结即:在只有一个简单查询(例如 select count(*) from 子表)且不断重复查询的情况下,taosd 内存持续上涨。测试中 taosd 内存占用从 400MB 可以一直涨到 24GB+。期间,另有其他用户也评论反馈遇到相同的问题,在内存小的情况下,最终 taosd 会 OOM。
01
问题定位
问题分析
分配后释放了 - 没有问题
-
分配后未释放 - 需要根据代码分析是内存泄漏还是缓存
背景知识
-
glibc 中的内存管理器 ptmalloc 通过 brk、mmap、munmap 3 个系统调用从 OS 分配和释放内存,对于大块内存每次都通过 mmap、munmap 直接分配和回收,对于小块内存则是通过 brk 从堆上分配一个大片内存然后进行内部切分来分配、释放、复用,因此默认情况下单个小块内存的分配是不一定能从系统调用的追踪中看到的。这里的“大块”与“小块”的边界值大小默认是 128K,同时提供了 mallopt(M_MMAP_THRESHOLD,threshold_value)来改变这个边界值。这就给我们提供了一种便利,只要将这个值调到足够小就可以观察到用户空间所有的内存分配与释放。 -
strace 命令可以捕获所有用户空间程序发出的系统调用和其参数信息,带来的便利就是可以观察到所有内存分配与释放的系统调用,同时对于日志信息可以被记录观察到。
定位步骤
-
taosd 启动时调用如下代码强制所有内存分配与释放都通过 mmap、munmap 进行,进而可以观察到用户所有内存的分配与释放。
int ret = mallopt(M_MMAP_THRESHOLD, 0);
if (0 == ret) {
return TAOS_SYSTEM_ERROR(errno);
}
-
配置中打开 taosd 所有模块的 DEBUG 日志开关,关闭异步日志,启动 taosd 进程,启动测试程序。 -
shell 中运行下面的命令捕捉系统调用。
strace -TttFf -e write=0,1,2,3 -p `pidof taosd` -o strace_log.txt
在测试执行完成后或观察到明显的内存增长后停止 strace 命令,strace_log.txt 内容示例如下:
1230673 12:56:10.273506 <... futex resumed>) = 0 <0.001681>
1230741 12:56:10.273535 write(3, "01/13 12:56:10.273516 01230741 Q"..., 129 <unfinished ...>
1230673 12:56:10.273547 futex(0x7ff766f4d01c, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 3, NULL, FUTEX_BITSET_MATCH_ANY <unfinished ...>
1230741 12:56:10.273566 <... write resumed>) = 129 <0.000022>
| 00000 30 31 2f 31 33 20 31 32 3a 35 36 3a 31 30 2e 32 01/13 12:56:10.2 |
| 00010 37 33 35 31 36 20 30 31 32 33 30 37 34 31 20 51 73516 01230741 Q |
| 00020 52 59 20 51 49 44 3a 30 78 65 33 39 37 66 65 37 RY QID:0xe397fe7 |
| 00030 63 33 65 30 38 38 36 63 30 2c 54 49 44 3a 30 78 c3e0886c0,TID:0x |
| 00040 63 33 32 34 2c 45 49 44 3a 30 20 74 61 73 6b 20 c324,EID:0 task |
| 00050 73 74 61 74 75 73 20 75 70 64 61 74 65 64 20 66 status updated f |
| 00060 72 6f 6d 20 45 58 45 43 55 54 49 4e 47 20 74 6f rom EXECUTING to |
| 00070 20 50 41 52 54 49 41 4c 5f 53 55 43 43 45 45 44 PARTIAL_SUCCEED |
| 00080 0a . |
1230741 12:56:10.273603 futex(0x7ff766f4d01c, FUTEX_WAKE_PRIVATE, 1) = 1 <0.000027>
1230749 12:56:10.273644 <... futex resumed>) = 0 <0.001744>
1230741 12:56:10.273655 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0 <unfinished ...>
1230749 12:56:10.273669 write(3, "01/13 12:56:10.271877 01230749 U"..., 83 <unfinished ...>
1230741 12:56:10.273681 <... mmap resumed>) = 0x7ff50f4c8000 <0.000020>
-
通过下面的 shell 命令从 strace 生成的文件中提取所有的内存分配地址与释放地址,map.txt 文件中的每行内容为一个内存分配的地址,unmap.txt 文件中的每行内容为一个内存释放的地址。
egrep "mmap|mremap" strace_log.txt |grep -v unfinished|awk -F "=" '{print $2}'|awk '{print $1}'>map.txt
egrep "munmap|mremap" strace_log.txt |grep -v resumed| awk -F "(" '{print $2}'|awk -F "," '{print $1}'>unmap.txt
通过自己开发的一个小工具从 map.txt 依次读取每一行,然后在 unmap.txt 文件中依次寻找该地址是否存在,如果存在则该内存分配释放没有问题;如果不存在,则该地址(A)为内存泄漏或者一个缓存的地址。
-
在 strace_log.txt 中找到最后一次 mmap 分配的上一步找到的可疑地址 (A),通过线程号观察该次内存分配的上下文信息(系统调用和日志信息),进而在代码中找到对应的内存分配的地方。 通过代码分析确认该次分配的内存在 strace 观察的时间段内未释放是否是正常的程序行为,如果是则可以划分为缓存类别;如果不是则判断为内存泄漏或异常缓存,修改后验证直至内存不再增长。
说明
-
打开 taosd 所有模块日志、关闭异步日志、跟踪所有系统调用的目的都是为了在第 7 步有足够的上下文信息判断内存分配的代码,但对于日志较少的模块我们可能需要通过增加日志逐步缩小范围来最终找到内存的分配点; -
在第 4 步我们需要充足时间保证测试完整执行完,进而保证最终找到可疑地址(A)不是因为观察时间不足还未等到 munmap 的场景(排除干扰); -
使用限制:只适用于 glibc 的内存管理器(Linux + glibc); -
工具代码如下,编译后跟第 5 步生成的结果放在一个目录直接运行即可(无需参数):
char in1[16] = {0};
char in2[500*1048576][16] = {0};
main()
{
FILE* fd1=fopen("map.txt", "r");
FILE* fd2=fopen("unmap.txt", "r");
int i = 0, n = 0, found = 0,m=0, minIdx = 0, non0 = 0;
while(fgets(in2[i], sizeof(in2[0]), fd2) != NULL)
{
if (in2[i][14] = '\n') {
in2[i][14] = 0;
}
i++;
}
printf("%d rcords in unmap.txt read\n", i);
while(fgets(in1, sizeof(in1), fd1) != NULL)
{
if (in1[14] = '\n') {
in1[14] = 0;
}
m++;
non0 = 0;
for(n=minIdx;n<i;n++)
{
if(in2[n][0]==0) {
if (0 == non0) {
minIdx++;
}
continue;
}
non0 = 1;
if((in1[0]==in2[n][0]) && (0==strcmp(in1, in2[n])))
{
in2[n][0]=0;
break;
}
}
if (n==i)
{
found++;
printf("%dth found, %s, it's the %dth in map.txt\n", found, in1, m);
//if(found>=100)
// break;
}
if (m > (minIdx+10000)) {
minIdx++;
}
}
}
02
定位结果
通过使用上面介绍的方法,我们最终定位到了两个问题:
一处内存错误问题,按照上面的分类属于非预期的缓存造成的:
atexit(cleanupRefPool);
一处可优化的缓存管理,不是内存增长的原因,但是针对特定使用场景缓存有优化空间。
03
总结与后续
往
期
推
荐
本文分享自微信公众号 - TDengine(taosdata_news)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。