記憶體洩漏治理實戰:TDengine 研發團隊使用 Windbg 的經驗分享

TDengine發表於2024-02-29

記憶體洩漏是一種常見的問題,它會導致程式的記憶體佔用逐漸增加,最終導致系統資源耗盡或程式崩潰。AddressSanitizer (ASan) 和 Valgrind 是很好的記憶體檢測工具,TDengine 的 CI 過程就使用了 ASan 。不過這次記憶體洩漏問題發生在 Windows 下,我們 CI 暫時還沒有覆蓋到,因此 TDengine 研發選擇使用 Windbg 來解決問題。結果證明,在 Windows 下,使用 Windbg 也是一個不錯的選擇。

記憶體洩漏的常用檢測方法

記憶體洩漏通常會發生在以下情況下:

  • 程式未正確釋放已分配的記憶體
  • 程式中存在迴圈引用,導致垃圾收集器無法回收記憶體
  • 程式中存在記憶體洩漏的第三方庫或元件

記憶體洩漏的檢測方法主要包括以下幾種:

  1. 靜態程式碼分析工具:未釋放的指標或記憶體分配錯誤等問題,不能檢測在程式執行時動態分配記憶體的情況。
  2. 動態分析工具:可以使用記憶體分配和釋放跟蹤器來跟蹤程式中的記憶體分配和釋放操作,並檢測是否存在記憶體洩漏的情況。然而,使用某些工具(如Valgrind)可能會對程式的效能產生一定的影響。
  3. 偵錯程式:WinDbg 和 GDB。

優缺點:

  • 靜態程式碼分析工具可以在早期發現問題,但是它們不能檢測程式執行時動態分配記憶體的情況。
  • 動態分析工具可以在程式執行時檢測問題,但是它們可能會影響程式效能,並且在檢測大型應用程式時可能需要大量的時間和資源。不過在資源充足的測試環境中跑的話,就都不是問題了,比如 ASan 就幫我們發現過不少問題。
  • 偵錯程式可以在程式執行時檢測問題,並提供強大的分析工具。

實踐分析

基本原理

使用 Windbg 定位記憶體洩露,依賴 glags 元件記錄程式在執行期間所有申請和釋放的記憶體,同時記錄的還有申請記憶體時的呼叫棧資訊。這樣在程式執行期間,使用 umdh 元件進行兩次快照記錄,透過比較兩次快照資訊的差異,就可以發現兩次快照間隔時間段中申請卻並未釋放的記憶體申請資訊。如果有記憶體洩露,diff 結果最前邊一般就是洩漏點的呼叫棧資訊。當然,兩次快照期間,要儘量觸發記憶體洩露,才能更準確的定位。diff 結果中還會有少量正常的申請沒來得及釋放的呼叫資訊,不過 diff 結果中能看到呼叫次數,比較容易甄別。

問題介紹

taosdump 在 windows 匯入資料出錯:

build and install latest TDengine 3.0 branch on Windows
use "taosBenchmark -I stmt -y" to create a lot of tables and data (10000 * 10000).
use "taosdump -D test -o outputFile" to dump out
use "taos -s 'drop database test'" to drop database
use "taosdump -i inputFile" to dump in.

錯誤日誌:taosd “tsem_init failed, errno: 28”

Taosdump: dumpInAvroDataImpl() LN7039 taos_stmt_execute() failed! reason: Out of Memory, timestamp: 1500000009256

定位過程

配置 gflags

gflags 工具應該位於路徑:C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\gflags,如果沒有的話,可以直接前往 Microsoft 的官方網站下載安裝。

安裝完成後,在命令列執行 gflags.exe /i your_application.exe 可設定跟蹤目標,同時可以設定相關引數。雙擊執行也是可以的,Image File 對應 /i 引數,選擇啟動程式 your_application.exe 後先按 tab 鍵,然後選擇其他配置。

定位步驟

1. 啟動 your_application.exe(我要除錯的是 taosdump.exe,所以下邊是 taosdump.exe)

“C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\gflags” -i taosdump.exe +ust

2. 複製 pdb 檔案到 mysymbols 目錄,pdb 檔案儲存了編譯後的程式的除錯資訊,和可執行程式一起生成,可以在應用程式生成目錄中找到。


3. Set pdb 目錄

set _NT_SYMBOL_PATH=c:\mysymbols;srv*c:\mycache*


4. 生成第一次記憶體快照

"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\umdh" -pn:taosdump.exe -f:C:\xstest\umdhlog\taosdump11.log


5. 生成第二次記憶體快照

"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\umdh" -pn:taosdump.exe -f:C:\xstest\umdhlog\taosdump12.log


6. 生成快照比較結果(umdh)

"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\umdh"  C:\xstest\umdhlog\taosdump11.log C:\xstest\umdhlog\taosdump12.log -f:C:\xstest\umdhlog\taosdumpdiff11_12.log


分析與解決

結果檔案

因為 taosdump 程式啟動後直至退出都在做大量的業務工作,記憶體洩露很容易發生在兩次快照期間。 988040 – 6ecf0 表示”申請次數 – 釋放次數”, 很明顯發生了記憶體洩露,洩漏點在 buildRequest 函式的 sem_init 這裡。

+  919350 ( 988040 - 6ecf0)  201b0 allocs        BackTrace9CB6973F
+   1ea5c ( 201b0 -  1754)        BackTrace9CB6973F        allocations
        ntdll!RtlpAllocateHeapInternal+948D5
        taos!heap_alloc_dbg_internal+1F6 (minkernel\crts\ucrt\src\appcrt\heap\debug_heap.cpp, 359)
        taos!heap_alloc_dbg+4D (minkernel\crts\ucrt\src\appcrt\heap\debug_heap.cpp, 450)
        taos!_calloc_dbg+6C (minkernel\crts\ucrt\src\appcrt\heap\debug_heap.cpp, 518)
        taos!calloc+2E (minkernel\crts\ucrt\src\appcrt\heap\calloc.cpp, 30)
        taos!sem_init+5D (C:\workroom\TDengine\contrib\pthread\sem_init.c, 109)
        taos!buildRequest+209 (C:\workroom\TDengine\source\client\src\clientImpl.c, 192)
        taos!stmtCreateRequest+73 (C:\workroom\TDengine\source\client\src\clientStmt.c, 15)
        taos!stmtSetTbName+115 (C:\workroom\TDengine\source\client\src\clientStmt.c, 588)
        taos!taos_stmt_set_tbname+7F (C:\workroom\TDengine\source\client\src\clientMain.c, 1350)
        taosdump!dumpInAvroDataImpl+E25 (C:\workroom\TDengine\tools\taos-tools\src\taosdump.c, 6260)
        taosdump!dumpInOneAvroFile+3D2 (C:\workroom\TDengine\tools\taos-tools\src\taosdump.c, 7229)
        taosdump!dumpInAvroWorkThreadFp+20B (C:\workroom\TDengine\tools\taos-tools\src\taosdump.c, 7306)
        taosdump!ptw32_threadStart+CD (C:\workroom\TDengine\contrib\pthread\ptw32_threadStart.c, 233)
        taosdump!thread_start<unsigned int (__cdecl*)(void *),1>+9C (minkernel\crts\ucrt\src\appcrt\startup\thread.cpp, 97)
        KERNEL32!BaseThreadInitThunk+10
        ntdll!RtlUserThreadStart+2B
洩漏點修改

接下來檢視程式碼並修改,C 語言對記憶體的使用自由度很高,因此也比較麻煩。可以看到有些路徑遺漏了 tsem_destory 的呼叫。

總結

工欲善其事必先利其器,掌握更多的工具和手段,在解決問題時才能比較從容,Windbg 定位記憶體洩漏的方式非常簡單,但是很有效。不過需要注意,它依賴 pdb 檔案,因此,釋出應用程式時要記得保留 pdb 檔案。pdb 檔案包含了程式的符號資訊,能夠幫助我們在除錯過程中準確定位問題所在。

另外,從出問題的程式碼可以看出,這塊記憶體的管理方式還是比較容易出錯,RAII 機制能較好的避免資源洩露,C 語言中也可以透過模擬 RAII 來達到類似的效果,雖然沒有 C++ 那麼流暢,也許以後可以考慮最佳化一下。

RAII(Resource Acquisition Is Initialization)機制是一種重要的資源管理方式,它將資源的獲取和物件的生命週期關聯起來。透過在物件的建構函式中獲取資源,在解構函式中釋放資源,我們可以確保資源的正確管理,防止資源洩漏和記憶體洩漏等問題。RAII 機制在 C++ 等程式語言中得到廣泛應用,是一種有效的資源管理方式。

來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70014783/viewspace-3007684/,如需轉載,請註明出處,否則將追究法律責任。

相關文章