作為 C/C++ 開發(fā)人員,內存泄漏是最容易遇到的問題之一,這是由 C/C++ 語言的特性引起的。眾所周知,開源的時序數(shù)據(jù)庫(Time Series Database)TDengine OSS 就是使用 C 語言進行底層自研的,也因此,針對內存泄漏問題,我們的研發(fā)小伙伴也做了諸多研究和思考。在本篇文章中,我們將從 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 內存持續(xù)上漲。測試中 taosd 內存占用從 400MB 可以一直漲到 24GB+。期間,另有其他用戶也評論反饋遇到相同的問題,在內存小的情況下,最終 taosd 會 OOM。
問題定位
遇到這種疑似內存泄漏問題時,第一步應該先用工具跑,在使用常用工具 Valgrind、Address sanitizer 嘗試之后,結果都報告沒有內存泄漏。這種情況在之前 2.x 版本也曾發(fā)生過,當時研發(fā)人員懷疑 glibc 的內存管理器有問題(不完善),然后切換到 jemalloc 或 tcmalloc,但是不是真的是 glibc 有 BUG 或者內存空洞問題導致的?我們需要尋找證據(jù)。
問題分析
在開始動手之前我們先要搞清楚概念,到底什么是內存泄漏?我們都了解內存泄漏的最大害處是導致程序最終 OOM,在此之前能觀察到的現(xiàn)象是進程內存使用量持續(xù)上漲。那是不是只要進程 OOM 了或者內存持續(xù)上漲就是有內存泄漏?并不是。簡單來說,內存泄漏是指不再使用的內存沒有釋放,這必然導致內存持續(xù)上漲直至 OOM,但不是只有內存泄漏會導致內存持續(xù)上漲和 OOM,上面提到的內存空洞問題或者緩存也會導致同樣的后果。所以嚴格來說,上述 issue 遇到的是內存持續(xù)上漲或 OOM 問題,并不一定是內存泄漏。但是不管是哪一種情況造成的,后果都是嚴重的,研發(fā)人員都要找到問題并解決它。
常見的可能造成內存持續(xù)上漲的問題有內存泄漏、內存空洞、緩存三類,而我們常用的 Valgrind、Address sanitizer 能夠發(fā)現(xiàn)解決的都是內存泄漏問題,而對于內存空洞和緩存問題卻無法檢測,這就是為什么很多時候會有內存在漲但是工具檢測不到問題的情況發(fā)生。但想要說服用戶這是空洞問題也并不那么容易,單純的內存空洞問題通常只會導致內存占用多的問題,空洞部分是可以重復利用的,也就是說通常不會造成內存持續(xù)增長問題,只在一些極端使用場景下可能會出現(xiàn)持續(xù)增長的問題。如果工具可靠且可以排除內存空洞問題,那大概率就是緩存問題了,而 taosd 在單個查詢重復執(zhí)行的場景下又沒有明顯的緩存問題。理論分析又陷入困境,我們需要一種能發(fā)現(xiàn)解決這三類問題的方法和工具。
雖然是三類問題,但他們也有共同點,那就是都是因為內存的分配和釋放造成的,如果能夠找到并記錄每個內存分配和釋放的點就可以分析屬于什么狀況了:
- 分配后釋放了 – 沒有問題
- 分配后未釋放 – 需要根據(jù)代碼分析是內存泄漏還是緩存
既然有了思路,接下來就是思考如何實現(xiàn)了,核心問題是怎么找到并記錄每個內存分配和釋放的點?開發(fā)代碼可以記錄每一個 taosd 自己的內存分配和釋放,但是開發(fā)工作量不小短時間內難以完成,更重要的原因在于 taosd 的進程空間中除了我們自己開發(fā)的代碼外還有第三方庫包括 glibc 的代碼,雖然出問題的概率較小,但如果是我們的使用方式有問題也是存在出問題的可能的,這些代碼中出現(xiàn)的問題怎么辦?我的答案是向下找接口,即在系統(tǒng)調用層面捕捉內存的分配和釋放。
背景知識
- glibc 中的內存管理器 ptmalloc 通過 brk、mmap、munmap 3 個系統(tǒng)調用從 OS 分配和釋放內存,對于大塊內存每次都通過 mmap、munmap 直接分配和回收,對于小塊內存則是通過 brk 從堆上分配一個大片內存然后進行內部切分來分配、釋放、復用,因此默認情況下單個小塊內存的分配是不一定能從系統(tǒng)調用的追蹤中看到的。這里的“大塊”與“小塊”的邊界值大小默認是 128K,同時提供了 mallopt(M_MMAP_THRESHOLD,threshold_value)來改變這個邊界值。這就給我們提供了一種便利,只要將這個值調到足夠小就可以觀察到用戶空間所有的內存分配與釋放。
- strace 命令可以捕獲所有用戶空間程序發(fā)出的系統(tǒng)調用和其參數(shù)信息,帶來的便利就是可以觀察到所有內存分配與釋放的系統(tǒng)調用,同時對于日志信息可以被記錄觀察到。
定位步驟
- taosd 啟動時調用如下代碼強制所有內存分配與釋放都通過 mmap、munmap 進行,進而可以觀察到用戶所有內存的分配與釋放。
int ret = mallopt(M_MMAP_THRESHOLD, 0);
if (0 == ret) {
return TAOS_SYSTEM_ERROR(errno);
}
- 配置中打開 taosd 所有模塊的 DEBUG 日志開關,關閉異步日志,啟動 taosd 進程,啟動測試程序。
- shell 中運行下面的命令捕捉系統(tǒng)調用。
strace -TttFf -e write=0,1,2,3 -p `pidof taosd` -o strace_log.txt
- 在測試執(zhí)行完成后或觀察到明顯的內存增長后停止 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
- 通過自己開發(fā)的一個小工具從 map.txt 依次讀取每一行,然后在 unmap.txt 文件中依次尋找該地址是否存在,如果存在則該內存分配釋放沒有問題;如果不存在,則該地址(A)為內存泄漏或者一個緩存的地址。
- 在 strace_log.txt 中找到最后一次 mmap 分配的上一步找到的可疑地址 (A),通過線程號觀察該次內存分配的上下文信息(系統(tǒng)調用和日志信息),進而在代碼中找到對應的內存分配的地方。
- 通過代碼分析確認該次分配的內存在 strace 觀察的時間段內未釋放是否是正常的程序行為,如果是則可以劃分為緩存類別;如果不是則判斷為內存泄漏或異常緩存,修改后驗證直至內存不再增長。
說明
- 打開 taosd 所有模塊日志、關閉異步日志、跟蹤所有系統(tǒng)調用的目的都是為了在第 7 步有足夠的上下文信息判斷內存分配的代碼,但對于日志較少的模塊我們可能需要通過增加日志逐步縮小范圍來最終找到內存的分配點;
- 在第 4 步我們需要充足時間保證測試完整執(zhí)行完,進而保證最終找到可疑地址(A)不是因為觀察時間不足還未等到 munmap 的場景(排除干擾);
- 使用限制:只適用于 glibc 的內存管理器(Linux + glibc);
- 工具代碼如下,編譯后跟第 5 步生成的結果放在一個目錄直接運行即可(無需參數(shù)):
#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++;
}
}
}
定位結果
通過使用上面介紹的方法,我們最終定位到了兩個問題:
- 一處內存錯誤問題,按照上面的分類屬于非預期的緩存造成的:
atexit(cleanupRefPool);
說明:我們在創(chuàng)建每個查詢子任務時都直接調用了上面這個語句,它會每次緩存一個函數(shù)地址,最終在進程退出時又都全部釋放了,因此不屬于內存泄漏,Valgrind 和 Address sanitizer 都檢測不到,這是造成查詢內存一直增長的原因。
- 一處可優(yōu)化的緩存管理,不是內存增長的原因,但是針對特定使用場景緩存有優(yōu)化空間。
總結與后續(xù)
上述問題是一個從 3.0.0.0 版本開始就一直存在的“內存泄漏”問題,任何一個查詢都存在,直到 3.0.2.5 版本出來之后,我們才可以說 taosd 終于沒有“內存泄漏”問題了。本文通過一種不需要額外代碼開發(fā)的方法,在傳統(tǒng)的內存泄漏檢測工具能力范圍之外,一站式定位解決進程內存占用持續(xù)增長或 OOM 問題,讓徹底解決這類問題成為可能。此外面對這一類問題,目前 TDengine OSS 已經(jīng)在 taosd/taosc 增加在線開閉內存調試模式,可以隨時在現(xiàn)場定位內存增長問題,不需要安裝工具,不需要編譯 ASAN 版本,尤其適合解決 Valgrind/ASAN 發(fā)現(xiàn)不了的內存增長問題。



互聯(lián)網(wǎng).png)



-1.png)












伙伴.png)



