百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

用 TDengine 3.0 碰到“内存泄露”?定位问题原因很关键

wptr33 2025-01-23 21:52 51 浏览

作为C/C++开发人员,内存泄漏是最容易遇到的问题之一,这是由C/C++语言的特性引起的。众所周知,开源的时序数据库(Time Series Database)TDengine OSS 就是使用C语言进行底层自研的,也因此,针对内存泄露问题,我们的研发小伙伴也做了诸多研究和思考。在本篇文章中,我们将从 GitHub 上的一个关于内存泄漏的 issue 入手,和大家探讨下导致内存泄漏的原因,以及如何避免和定位内存泄漏。

issue 链接:
https://github.com/taosdata/TDengine/issues/18276

从上述 issue 的详细描述可以看到,这是一个疑似内存泄漏问题,该用户使用 TDengine OSS 从 3.0.1.6 版本开始一直升级测到 3.0.2.2 版本,内存泄漏问题一直存在。该问题简化总结即:在只有一个简单查询(例如 select count(*) from 子表)且不断重复查询的情况下,taosd 内存持续上涨。测试中 taosd 内存占用从 400MB 可以一直涨到 24GB+。期间,另有其他用户也评论反馈遇到相同的问题,在内存小的情况下,最终 taosd 会 OOM。

问题定位

遇到这种疑似内存泄漏问题时,第一步应该先用工具跑,在使用常用工具 Valgrind、Address sanitizer 尝试之后,结果都报告没有内存泄漏。这种情况在之前 2.x 版本也曾发生过,当时研发人员怀疑 glibc 的内存管理器有问题(不完善),然后切换到 jemalloc 或 tcmalloc,但是不是真的是 glibc 有 BUG 或者内存空洞问题导致的?我们需要寻找证据。

问题分析

在开始动手之前我们先要搞清楚概念,到底什么是内存泄漏?我们都了解内存泄漏的最大害处是导致程序最终 OOM,在此之前能观察到的现象是进程内存使用量持续上涨。那是不是只要进程 OOM 了或者内存持续上涨就是有内存泄漏?并不是。简单来说,内存泄漏是指不再使用的内存没有释放,这必然导致内存持续上涨直至 OOM,但不是只有内存泄漏会导致内存持续上涨和 OOM,上面提到的内存空洞问题或者缓存也会导致同样的后果。所以严格来说,上述 issue 遇到的是内存持续上涨或 OOM 问题,并不一定是内存泄漏。但是不管是哪一种情况造成的,后果都是严重的,研发人员都要找到问题并解决它。

常见的可能造成内存持续上涨的问题有内存泄漏、内存空洞、缓存三类,而我们常用的 Valgrind、Address sanitizer 能够发现解决的都是内存泄漏问题,而对于内存空洞和缓存问题却无法检测,这就是为什么很多时候会有内存在涨但是工具检测不到问题的情况发生。但想要说服用户这是空洞问题也并不那么容易,单纯的内存空洞问题通常只会导致内存占用多的问题,空洞部分是可以重复利用的,也就是说通常不会造成内存持续增长问题,只在一些极端使用场景下可能会出现持续增长的问题。如果工具可靠且可以排除内存空洞问题,那大概率就是缓存问题了,而 taosd 在单个查询重复执行的场景下又没有明显的缓存问题。理论分析又陷入困境,我们需要一种能发现解决这三类问题的方法和工具。

虽然是三类问题,但他们也有共同点,那就是都是因为内存的分配和释放造成的,如果能够找到并记录每个内存分配和释放的点就可以分析属于什么状况了:

  • 分配后释放了 – 没有问题
  • 分配后未释放 – 需要根据代码分析是内存泄漏还是缓存

既然有了思路,接下来就是思考如何实现了,核心问题是怎么找到并记录每个内存分配和释放的点?开发代码可以记录每一个 taosd 自己的内存分配和释放,但是开发工作量不小短时间内难以完成,更重要的原因在于 taosd 的进程空间中除了我们自己开发的代码外还有第三方库包括 glibc 的代码,虽然出问题的概率较小,但如果是我们的使用方式有问题也是存在出问题的可能的,这些代码中出现的问题怎么办?我的答案是向下找接口,即在系统调用层面捕捉内存的分配和释放

背景知识

  • 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 
1230673 12:56:10.273547 futex(0x7ff766f4d01c, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 3, NULL, FUTEX_BITSET_MATCH_ANY 
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 
1230749 12:56:10.273669 write(3, "01/13 12:56:10.271877 01230749 U"..., 83 
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 步生成的结果放在一个目录直接运行即可(无需参数):
#include "stdlib.h"
#include "stdio.h"
#include 
#include 
#include 

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=100)
         //  break;
     }
     if (m > (minIdx+10000)) {
        minIdx++;
     }
  }
}

定位结果

通过使用上面介绍的方法,我们最终定位到了两个问题:

  • 一处内存错误问题,按照上面的分类属于非预期的缓存造成的:
  atexit(cleanupRefPool);

说明:我们在创建每个查询子任务时都直接调用了上面这个语句,它会每次缓存一个函数地址,最终在进程退出时又都全部释放了,因此不属于内存泄漏,Valgrind 和 Address sanitizer 都检测不到,这是造成查询内存一直增长的原因。

  • 一处可优化的缓存管理,不是内存增长的原因,但是针对特定使用场景缓存有优化空间。

总结与后续

上述问题是一个从 3.0.0.0 版本开始就一直存在的“内存泄漏”问题,任何一个查询都存在,直到 3.0.2.5 版本出来之后,我们才可以说 taosd 终于没有“内存泄漏”问题了。本文通过一种不需要额外代码开发的方法,在传统的内存泄漏检测工具能力范围之外,一站式定位解决进程内存占用持续增长或 OOM 问题,让彻底解决这类问题成为可能。此外面对这一类问题,目前 TDengine OSS 已经在 taosd/taosc 增加在线开闭内存调试模式,可以随时在现场定位内存增长问题,不需要安装工具,不需要编译 ASAN 版本,尤其适合解决 Valgrind/ASAN 发现不了的内存增长问题。

相关推荐

开发者必看的八大Material Design开源项目

MaterialDesign是介于拟物和扁平之间的一种设计风格,自从它发布以来,便引起了很多开发者的关注,在这里小编介绍在Android开发者当中里最受青睐的八个MaterialDesign开源项...

另类插这么可爱,一定是…(另类t恤)

IT之家(www.ithome.com):另类插图:这么可爱,一定是…OSXMavericks和Yosemite打破了苹果对Mac操作系统传统的命名方式,使用加州的某些标志性景点来替换猫...

Android常用ADB命令(安卓adb工具是什么)

杀死应用①根据包名获取APP的PIDadbshellps|grep应用包名②执行kill命令...

微软Mac版PowerPoint测试Reading Order Pane功能

IT之家5月20日消息,微软公司昨日(5月19日)发布博文,邀请Microsoft365Insiders成员,测试macOS新版PowerPoint演示文稿应用,重点引入...

Visual Studio跨平台开发实战(4):Xamarin Android控制项介绍

前言不同于iOS,Xamarin在VisualStudio中针对Android,可以直接设计使用者界面.在本篇教学文章中,笔者会针对Android的专案目录结构以及基本控制项进行介绍,包...

用云存储30分钟快速搭建APP,你信吗?

背景不管你承认与否,移动互联的时代已经到来,这是一个移动互联的时代,手机已经是当今世界上引领潮流的趋势,大型的全球化企业和中小企业都把APP程序开发纳入到他们的企业发展策略当中。但随着手机APP上传的...

谷歌P图神器来了!不用学不用教,输入一句话,分分钟给结果

Pine发自凹非寺量子位|公众号QbitAI当你拍照片时,“模特不好好配合”怎么办?...

iOS文本编辑控件UITextField和UITextVie

记录一个菜鸟的IOS学习之旅,如能帮助正在学习的你,亦枫不胜荣幸;如路过的大神如指教几句,亦枫感激涕淋!细心的朋友可能已经注意到了,IOS学习之旅系列教程在本篇公众号的文章中,封面已经换成美女图片了,...

Android入门图文教程集锦(android 入门教程)

Android入门视频教程集锦AndroidStudio错误gradientandroid:endXattributenotfound...

如何使用Android自定义复合视图(如何使用android自定义复合视图)

在最近的一个客户应用中,我遇到了一个需求,根据选定的值来生成指定数量的编辑框字段,这样用户可以输入人物信息。最初我的想法是把这些逻辑放到Fragment中,只是根据选中值的变化来向线性布局容器中增加编...

原生安卓开发app的框架frida常用关键代码定位

前言有时候可能会对APP进行字符串加密等操作,这样的话你的变量名等一些都被混淆了,看代码就可能无从下手...

教程10 | 三分钟搞定一个智能输入法程序

一案例描述1、考核知识点网格布局线性布局样式和主题Toast2、练习目标掌握网格布局的使用掌握Toast的使用掌握线性布局的使用...

(Android 8.1) 功能与新特性(android的功能)

和你一起终身学习,这里是程序员AndroidAndroid8.1(API级别27)为用户和开发人员引入了各种新特性和功能。本文档重点介绍了开发人员的新功能。通过本章阅读,您将获取到以下内容:Andr...

怎样设置EditText内部文字被锁定不可删除和修改

在做项目的时候,我曾经遇到过这样的要求,就是跟百度贴吧客户端上的一样,在回复帖子的时候,在EditText中显示回复人的名字,而且这个名字不可以修改和删除,说白了就是不可操作,只能在后面输入内容。在E...

如何阻止 Android 活动启动时 EditText 获得焦点

技术背景在Android开发中,当活动启动时,EditText有时会自动获得焦点并弹出虚拟键盘,这可能不是用户期望的行为。为了提升用户体验,我们需要阻止...