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

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

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

作为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 发现不了的内存增长问题。

相关推荐

Java常用工具类技术文档(java常用util工具类)

一、概述Java工具类(UtilityClasses)是封装了通用功能的静态方法集合,能够简化代码、提高开发效率。本文整理Java原生及常用第三方库(如ApacheCommons、GoogleG...

建议收藏!深入理解Java虚拟机:JVM垃圾回收算法+垃圾收集器

02JVM垃圾回收算法2.1什么是垃圾回收?...

Java 开发者线上问题排查常用的 15 个 Linux 命令

作为Java开发者,线上环境的问题排查是日常工作的重要组成部分。熟练掌握Linux命令能大幅提升排查效率,快速定位进程异常、日志错误、性能瓶颈等核心问题。本文结合Java应用特点,整理1...

Java-Maven详解(maven for java)

一、什么是Maven?ApacheMaven是一个软件...

java 文件操作(I/O流)(java文件流写入文件)

一、文件操作技术演进二、核心类对比分析...

如何使用Java API操作HDFS系统?(java编程操作hdfs能完成的功能有)

1.搭建项目环境打开Eclipse选择FileàNewàMavenProject创建Maven工程,选择“Createasimpleproject”选项,点击【Next】按钮,会进入“New...

那些被&quot;删除&quot;却仍占用空间的文件

在服务器运维过程中,磁盘空间不足是一个常见问题。而有时候,即使清理了大量文件,系统仍然报告磁盘几乎已满,这种情况尤为令人困惑。本文将通过一个实际案例,分享如何排查和解决Linux服务器上的"幽...

SpringBoot的Web应用开发——Web缓存利器Redis的应用!

 Web缓存利器Redis的应用Redis是目前使用非常广泛的开源的内存数据库,是一个高性能的keyvalue数据库,它支持多种数据结构,常用做缓存、消息代理和配置中心。本节将简单介绍Redis的使...

如何使用C#中的Lambda表达式操作Redis Hash结构,简化缓存中对象属性的读写操作

...

Redis 常用命令大全(redis常用命令及详解)

Redis常用命令全解析在当今的数据处理与存储领域,Redis凭借其高性能、丰富的数据结构等特性,成为了众多开发者和企业的首选内存数据库。下面将为大家详细介绍Redis的常用命令。键(Key)...

Redis+Lua脚本防超卖是万能解?这3个致命漏洞你可能没发现!

在高并发秒杀场景中,Redis+Lua脚本常被视为防止超卖的“银弹”。然而,许多开发者因对其底层逻辑理解不足,踩中了致命漏洞却不自知。本文通过真实案例剖析三个隐藏极深的问题,并提供完整解决方案,助你避...

10w qps缓存数据库——Redis(缓存技术 redis)

一、Redis数据库介绍:Redis:非关系型缓存数据库...

Redis安装及核心数据结构(redis一般安装在哪)

Redis安装官方下载地址:http://redis.io/downloadhttp://download.redis.io/releases/...

Python Redis数据库新玩法:从零到高手掌握操作技巧

介绍Redis(RemoteDictionaryServer)是一种高性能的开源内存数据库,它支持多种数据结构,如字符串、哈希、列表、集合、有序集合等,并提供了丰富的操作命令。Redis具有快速、...

redis知识总结(基础篇,可复习,可学习)

最近redis差不多看完了,前面学的也忘了好多,所以正好写篇博客复习复习。此篇介绍的是redis的基础篇,希望这篇能帮到各位大佬。...