无码人妻精品一区二区三18禁,影音先锋男人AV橹橹色,污污污污污污www网站免费,日韩成人av无码一区二区三区,欧美性受xxxx狂喷水

用 TDengine 3.0 碰到“內存泄漏”?定位問題原因很關鍵

爾悅

2023-09-19 / ,

作為 C/C++ 開發人員,內存泄漏是最容易遇到的問題之一,這是由 C/C++ 語言的特性引起的。眾所周知,開源的時序數據庫(Time Series Database)TDengine OSS 就是使用 C 語(yu)言進行(xing)底層自(zi)研的,也因此(ci),針(zhen)對內(nei)存(cun)泄漏(lou)(lou)問題,我們(men)的研發(fa)小伙伴也做了諸(zhu)多研究和(he)思考。在本篇文章中,我們(men)將從 GitHub 上(shang)的一個關(guan)于內(nei)存(cun)泄漏(lou)(lou)的 issue 入(ru)手,和(he)大(da)家探討(tao)下導致內(nei)存(cun)泄漏(lou)(lou)的原(yuan)因,以及(ji)如何避免和(he)定位內(nei)存(cun)泄漏(lou)(lou)。

issue 鏈接:

從(cong)上述 issue 的(de)(de)詳細描述可以(yi)(yi)看到(dao)(dao),這是一個疑(yi)似內(nei)存(cun)泄漏(lou)問(wen)題(ti),該用戶使用 TDengine OSS 從(cong) 3.0.1.6 版(ban)本開(kai)始一直升級測到(dao)(dao) 3.0.2.2 版(ban)本,內(nei)存(cun)泄漏(lou)問(wen)題(ti)一直存(cun)在。該問(wen)題(ti)簡化總結即:在只有一個簡單查詢(例如 select count(*) from 子表)且不斷重復查詢的(de)(de)情況下,taosd 內(nei)存(cun)持續上漲。測試中 taosd 內(nei)存(cun)占用從(cong) 400MB 可以(yi)(yi)一直漲到(dao)(dao) 24GB+。期間,另有其他(ta)用戶也評論反饋遇到(dao)(dao)相同的(de)(de)問(wen)題(ti),在內(nei)存(cun)小(xiao)的(de)(de)情況下,最終(zhong) taosd 會 OOM。

問題定位

遇到這種疑(yi)似內(nei)(nei)存泄(xie)漏(lou)問(wen)題時,第一步應(ying)該先用工具(ju)跑,在使用常用工具(ju) Valgrind、Address sanitizer 嘗(chang)試之后,結果都報告沒有(you)內(nei)(nei)存泄(xie)漏(lou)。這種情況在之前 2.x 版本也曾發生過,當時研發人(ren)員懷疑(yi) glibc 的內(nei)(nei)存管理器有(you)問(wen)題(不(bu)(bu)完善(shan)),然后切換到 jemalloc 或(huo) tcmalloc,但是(shi)不(bu)(bu)是(shi)真的是(shi) glibc 有(you) BUG 或(huo)者內(nei)(nei)存空(kong)洞問(wen)題導(dao)致的?我(wo)們(men)需要尋(xun)找證據。

問題分析

在開始動手(shou)之(zhi)前(qian)(qian)我(wo)們先要搞(gao)清楚概(gai)念,到(dao)(dao)底什么(me)是(shi)(shi)內(nei)(nei)(nei)(nei)存(cun)泄(xie)(xie)(xie)漏(lou)(lou)(lou)(lou)?我(wo)們都(dou)了(le)解內(nei)(nei)(nei)(nei)存(cun)泄(xie)(xie)(xie)漏(lou)(lou)(lou)(lou)的最大害處(chu)是(shi)(shi)導(dao)(dao)致(zhi)程(cheng)序最終 OOM,在此之(zhi)前(qian)(qian)能觀察(cha)到(dao)(dao)的現象是(shi)(shi)進(jin)程(cheng)內(nei)(nei)(nei)(nei)存(cun)使(shi)用(yong)量持(chi)(chi)續(xu)上(shang)(shang)漲。那是(shi)(shi)不(bu)是(shi)(shi)只要進(jin)程(cheng) OOM 了(le)或(huo)者內(nei)(nei)(nei)(nei)存(cun)持(chi)(chi)續(xu)上(shang)(shang)漲就是(shi)(shi)有內(nei)(nei)(nei)(nei)存(cun)泄(xie)(xie)(xie)漏(lou)(lou)(lou)(lou)?并(bing)(bing)不(bu)是(shi)(shi)。簡單來說,內(nei)(nei)(nei)(nei)存(cun)泄(xie)(xie)(xie)漏(lou)(lou)(lou)(lou)是(shi)(shi)指不(bu)再使(shi)用(yong)的內(nei)(nei)(nei)(nei)存(cun)沒(mei)有釋放(fang),這(zhe)必(bi)然導(dao)(dao)致(zhi)內(nei)(nei)(nei)(nei)存(cun)持(chi)(chi)續(xu)上(shang)(shang)漲直至 OOM,但(dan)不(bu)是(shi)(shi)只有內(nei)(nei)(nei)(nei)存(cun)泄(xie)(xie)(xie)漏(lou)(lou)(lou)(lou)會(hui)導(dao)(dao)致(zhi)內(nei)(nei)(nei)(nei)存(cun)持(chi)(chi)續(xu)上(shang)(shang)漲和(he) OOM,上(shang)(shang)面提到(dao)(dao)的內(nei)(nei)(nei)(nei)存(cun)空洞問(wen)(wen)題或(huo)者緩存(cun)也會(hui)導(dao)(dao)致(zhi)同樣的后(hou)果。所以(yi)嚴格(ge)來說,上(shang)(shang)述(shu) issue 遇到(dao)(dao)的是(shi)(shi)內(nei)(nei)(nei)(nei)存(cun)持(chi)(chi)續(xu)上(shang)(shang)漲或(huo) OOM 問(wen)(wen)題,并(bing)(bing)不(bu)一定是(shi)(shi)內(nei)(nei)(nei)(nei)存(cun)泄(xie)(xie)(xie)漏(lou)(lou)(lou)(lou)。但(dan)是(shi)(shi)不(bu)管(guan)是(shi)(shi)哪一種情況(kuang)造(zao)成的,后(hou)果都(dou)是(shi)(shi)嚴重的,研發(fa)人員都(dou)要找(zhao)到(dao)(dao)問(wen)(wen)題并(bing)(bing)解決(jue)它。

常見的可能造成內存持續上漲的問題有內存泄漏、內存空洞、緩存三類,而我們常用的 Valgrind、Address sanitizer 能夠發現解決的都是內存泄漏問題,而對于內存空洞和緩存問題卻無法檢測,這就是為什么很多時候會有內存在漲但是工具檢測不到問題的情況發生。但想要說服用(yong)(yong)(yong)戶這(zhe)是(shi)空洞問(wen)(wen)題(ti)(ti)也并不那么容(rong)易,單純(chun)的(de)(de)內存(cun)(cun)(cun)(cun)(cun)空洞問(wen)(wen)題(ti)(ti)通常只會導致內存(cun)(cun)(cun)(cun)(cun)占(zhan)用(yong)(yong)(yong)多的(de)(de)問(wen)(wen)題(ti)(ti),空洞部分是(shi)可(ke)(ke)以重(zhong)復(fu)利用(yong)(yong)(yong)的(de)(de),也就是(shi)說通常不會造(zao)成內存(cun)(cun)(cun)(cun)(cun)持(chi)續增長(chang)問(wen)(wen)題(ti)(ti),只在(zai)一(yi)些極端(duan)使用(yong)(yong)(yong)場景(jing)下(xia)可(ke)(ke)能(neng)會出現(xian)持(chi)續增長(chang)的(de)(de)問(wen)(wen)題(ti)(ti)。如(ru)果(guo)工(gong)具(ju)可(ke)(ke)靠且可(ke)(ke)以排除內存(cun)(cun)(cun)(cun)(cun)空洞問(wen)(wen)題(ti)(ti),那大概率就是(shi)緩存(cun)(cun)(cun)(cun)(cun)問(wen)(wen)題(ti)(ti)了,而 taosd 在(zai)單個查詢重(zhong)復(fu)執行的(de)(de)場景(jing)下(xia)又(you)沒(mei)有明(ming)顯的(de)(de)緩存(cun)(cun)(cun)(cun)(cun)問(wen)(wen)題(ti)(ti)。理論分析又(you)陷入困境,我(wo)們需要一(yi)種能(neng)發現(xian)解決這(zhe)三類(lei)問(wen)(wen)題(ti)(ti)的(de)(de)方(fang)法和工(gong)具(ju)。

雖然是三類問題,但他們也有共同點,那就是都是因為內存的分配和釋放造成的,如果能夠找到并記錄每個內存分配和釋放的點就可以(yi)分析(xi)屬于什么狀況了:

  • 分配后釋放了 – 沒有問題
  • 分配后未釋放 – 需要根據代碼分析是內存泄漏還是緩存

既然有了思路,接下來就是思考如何實現了,核心問題是怎么找到并記錄每個內存分配和釋放的點?開發代碼可以記錄每一個 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 <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 步生成的結果放在一個目錄直接運行即可(無需參數):
#include "stdlib.h"
#include "stdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

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++;
     }
  }
}

定位結果

通(tong)過使用上面介紹的方法,我(wo)們最終定位到了(le)兩個問題:

  • 一處內存錯誤問題,按照上面的分類屬于非預期的緩存造成的:
  atexit(cleanupRefPool);

說明:我們在創建每個(ge)(ge)查(cha)詢子任務時都直(zhi)接調用了上面這個(ge)(ge)語句,它會每次緩存(cun)一個(ge)(ge)函數地址,最終在進(jin)程(cheng)退(tui)出時又都全部釋放(fang)了,因(yin)此(ci)不屬于(yu)內(nei)存(cun)泄漏,Valgrind 和 Address sanitizer 都檢測不到,這是造成查(cha)詢內(nei)存(cun)一直(zhi)增長的原因(yin)。

  • 一處可優化的緩存管理,不是內存增長的原因,但是針對特定使用場景緩存有優化空間。

總結與后續

上述問題是一個從 3.0.0.0 版本開始就一直存在的“內存泄漏”問題,任何一個查詢都存在,直到 3.0.2.5 版本出來之后,我們才可以說 taosd 終于沒有“內存泄漏”問題了。本文通過一種不需要額外代碼開發的方法,在傳統的內存泄漏檢測工具能力范圍之外,一站式定位解決進程內存占用持續增長或 OOM 問題,讓徹底解決這類問題成為可能。此外面對這一類問題,目前 TDengine OSS 已經在 taosd/taosc 增加在線開閉內存調試模式,可以隨時在現場定位內存增長問題,不需要安裝工具,不需要編譯 ASAN 版本,尤其適合解決 Valgrind/ASAN 發現不了的內存增長問題。