導讀:Apache Doris 使用 C++ 語言實作了執行引擎,C++ 開發程序中,影響開發效率的一個重要因素是指標的使用,包括非法訪問、泄露、強制型別轉換等,本文將會通過對 Sanitizer 和 Core Dump 分析工具的介紹來為大家分享:如何快速定位 Apache Doris 中的 C++ 問題,幫助開發者提升開發效率并掌握更高效的開發技巧,
?作者|Apache Doris Committer楊勇強
Apache Doris 是一款高性能 MPP 分析型資料庫,出于性能的考慮,Apache Doris 使用了 C++ 語言實作了執行引擎,在 C++ 開發程序中,影響開發效率的一個重要因素是指標的使用,包括非法訪問、泄露、強制型別轉換等,Google Sanitizer 是由 Google 設計的用于動態代碼分析的工具,在 Apache Doris 開發程序中遭遇指標使用引起的記憶體問題時,正是因為有了 Sanitizer,使得問題解決效率可以得到數量級的提升,除此以外,當出現一些記憶體越界或非法訪問的情況導致 BE 行程 Crash 時,Core Dump 檔案是非常有效的定位和復現問題的途徑,因此一款高效分析 CoreDump 的工具也會進一步幫助更加快捷定位問題,
本文將會通過對 Sanitizer 和 Core Dump 分析工具的介紹來為大家分享:如何快速定位 Apache Doris 中的 C++ 問題,幫助開發者提升開發效率并掌握更高效的開發技巧,
Sanitizer 介紹
定位 C++ 程式記憶體問題常用的工具有兩個,Valgrind 和 Sanitizer,
二者的對比可以參考:https://developers.redhat.com/blog/2021/05/05/memory-error-checking-in-c-and-c-comparing-sanitizers-and-valgrind
其中 Valgrind 通過運行時軟體翻譯二進制指令的執行獲取相關的資訊,所以 Valgrind 會非常大幅度的降低程式性能,這就導致在一些大型專案比如 Apache Doris 使用 Valgrind 定位記憶體問題效率會很低,
而 Sanitizer 則是通過編譯時插入代碼來捕獲相關的資訊,性能下降幅度比 Valgrind 小很多,使得能夠在單測以及其它測驗環境默認使用 Saintizer,
Sanitizer 的演算法可以參考:https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm
在 Apache Doris 中,我們通常使用 Sanirizer 來定位記憶體問題,LLVM 以及 GNU C++ 有多個 Sanitizer:
- AddressSanitizer(ASan)可以發現記憶體錯誤問題,比如 use after free,heap buffer overflow,stack buffer overflow,global buffer overflow,use after return,use after scope,memory leak,super large memory allocation;
- AddressSanitizerLeakSanitizer (LSan)可以發現記憶體泄露;
- MemorySanitizer(MSan)可以發現未初始化的記憶體使用;
- UndefinedBehaviorSanitizer (UBSan)可以發現未定義的行為,比如越界陣列訪問、數值溢位等;
- ThreadSanitizer (TSan)可以發現執行緒的競爭行為;
其中 AddressSanitizer, AddressSanitizerLeakSanitizer 以及 UndefinedBehaviorSanitizer 對于解決指標相關的問題最為有效,
Sanitizer 不但能夠發現錯誤,而且能夠給出錯誤源頭以及代碼位置,這就使得問題的解決效率很高,通過一些例子來說明 Sanitizer 的易用程度,
可以參考此處使用 Sanitizer:https://github.com/apache/doris/blob/master/be/CMakeLists.txt
Sanitizer 和 Core Dump 配合定位問題非常高效,默認 Sanitizer 不生成 Core Dump 檔案,可以使用如下環境變數生成 Core Dump檔案,建議默認打開,
可以參考:https://github.com/apache/doris/blob/master/bin/start_be.sh
export ASAN_OPTIONS=symbolize=1:abort_on_error=1:disable_coredump=0:unmap_shadow_on_exit=1
使用如下環境變數讓 UBSan 生成代碼堆疊,默認不生成,
export UBSAN_OPTIONS=print_stacktrace=1
有時候需要顯示指定 Symbolizer 二進制的位置,這樣 Sanitizer 就能夠直接生成可讀的代碼堆疊,
export ASAN_SYMBOLIZER_PATH=your path of llvm-symbolizer
Sanitizer 使用舉例
Use after free
User after free 是指訪問釋放的記憶體,針對 use after free 錯誤,AddressSanitizer 能夠報出使用釋放地址的代碼堆疊,地址分配的代碼堆疊,地址釋放的代碼堆疊,比如:https://github.com/apache/doris/issues/9525中,使用釋放地址的代碼堆疊如下:
82849==ERROR: AddressSanitizer: heap-use-after-free on address 0x60300074c420 at pc 0x56510f61a4f0 bp 0x7f48079d89a0 sp 0x7f48079d8990
READ of size 1 at 0x60300074c420 thread T94 (MemTableFlushTh)
#0 0x56510f61a4ef in doris::faststring::append(void const*, unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/util/faststring.h:120
// 更詳細的代碼堆疊請前往https://github.com/apache/doris/issues/9525查看
此地址初次分配的代碼堆疊如下:
previously allocated by thread T94 (MemTableFlushTh) here:
#0 0x56510e9b74b7 in __interceptor_malloc (/mnt/ssd01/tjp/regression_test/be/lib/palo_be+0x536a4b7)
#1 0x56510ee77745 in Allocator<false, false>::alloc_no_track(unsigned long, unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/vec/common/allocator.h:223
#2 0x56510ee68520 in Allocator<false, false>::alloc(unsigned long, unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/vec/common/allocator.h:104
地址釋放的代碼堆疊如下:
0x60300074c420 is located 16 bytes inside of 32-byte region [0x60300074c410,0x60300074c430)
freed by thread T94 (MemTableFlushTh) here:
#0 0x56510e9b7868 in realloc (/mnt/ssd01/tjp/regression_test/be/lib/palo_be+0x536a868)
#1 0x56510ee8b913 in Allocator<false, false>::realloc(void*, unsigned long, unsigned long, unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/vec/common/allocator.h:125
#2 0x56510ee814bb in void doris::vectorized::PODArrayBase<1ul, 4096ul, Allocator<false, false>, 15ul, 16ul>::realloc<>(unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/vec/common/pod_array.h:147
有了詳細的非法訪問地址代碼堆疊、分配代碼堆疊、釋放代碼堆疊,問題定位就會非常容易,
說明:限于文章篇幅,示例中的堆疊展示不全,完整代碼堆疊可以前往對應 Issue 中進行查看,
heap buffer overflow
AddressSanitizer 能夠報出 heap buffer overflow 的代碼堆疊,
比如https://github.com/apache/doris/issues/5951 里的,結合運行時生成的 Core Dump 檔案就可以快速定位問題,
==3930==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60c000000878 at pc 0x000000ae00ce bp 0x7ffeb16aa660 sp 0x7ffeb16aa658
READ of size 8 at 0x60c000000878 thread T0
#0 0xae00cd in doris::StringFunctions::substring(doris_udf::FunctionContext*, doris_udf::StringVal const&, doris_udf::IntVal const&, doris_udf::IntVal const&) ../src/exprs/string_functions.cpp:98
memory leak
AddressSanitizer 能夠報出哪里分配的記憶體沒有被釋放,就可以快速的分析出泄露原因,
==1504733==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 688128 byte(s) in 168 object(s) allocated from:
#0 0x560d5db51aac in __interceptor_posix_memalign (/mnt/ssd01/doris-master/VEC_ASAN/be/lib/doris_be+0x9227aac)
#1 0x560d5fbb3813 in doris::CoreDataBlock::operator new(unsigned long) /home/zcp/repo_center/doris_master/be/src/util/core_local.cpp:35
#2 0x560d5fbb65ed in doris::CoreDataAllocatorImpl<8ul>::get_or_create(unsigned long) /home/zcp/repo_center/doris_master/be/src/util/core_local.cpp:58
#3 0x560d5e71a28d in doris::CoreLocalValue::CoreLocalValue(long)
https://github.com/apache/doris/issues/10926
https://github.com/apache/doris/pull/3326
例外分配
分配過大的記憶體 AddressSanitizer 會報出 OOM 錯誤,根據堆疊以及 Core Dump 檔案可以分析出何處分配了過大記憶體,堆疊舉例如下:
Fix PR 見:https://github.com/apache/doris/pull/10289
UBSan 能夠高效發現強制型別轉換的錯誤,如下方 Issue 鏈接中描述,它能夠精確的描述出強制型別轉換帶來錯誤的代碼,如果不能在第一現場發現這種錯誤,后續因為指標錯誤使用,會比較難定位,
Issue:https://github.com/apache/doris/issues/9105
UndefinedBehaviorSanitizer 也比 AddressSanitizer 及其它的更容易發現死鎖,
比如:https://github.com/apache/doris/issues/10309
程式維護記憶體 Pool 時 AddressSanitizer 的使用
AddressSanitizer 是編譯器針對記憶體分配、釋放、訪問 生成額外代碼來實作記憶體問題分析的,如果程式維護了自己的記憶體 Pool,AddressSanitizer 就不能發現 Pool 中記憶體非法訪問的問題,這種情況下需要做一些額外的作業來使得 AddressSanitizer 盡可能作業,主要是使用 ASAN_POISON_MEMORY_REGION 和 ASAN_UNPOISON_MEMORY_REGION 管理記憶體是否可以訪問,這種方法使用比較難,因為 AddressSanitizer 內部有地址對齊等的處理,出于性能以及記憶體釋放等原因,Apache Doris 也維護了記憶體分配 Pool ,這種方法不能確保 AddressSanitizer 能夠發現所有問題,
可以參考:https://github.com/apache/doris/pull/8148
當程式維護自己的記憶體池時,按照 https://github.com/apache/dorisw/pull/8148 中方法,use after free 錯誤會變成 use after poison,但是 use after poison 不能夠給出地址失效的堆疊(https://github.com/google/sanitizers/issues/191),從而導致問題的定位分析仍然很困難,
因此建議程式維護的記憶體 Pool 可以通過選項關閉,這樣在測驗環境就可以使用 AddressSanitizer 高效地定位記憶體問題,
Core dump 分析工具
分析 C++ 程式生成的 Core Dump 檔案經常遇到的問題就是怎么列印出 STL 容器中的值以及 Boost 中容器的值,有如下三個工具可以高效的查看 STL 和 Boost 中容器的值,
STL-View
可以將此檔案 https://github.com/dataroaring/tools/blob/main/gdb/dbinit_stl_views-1.03.txt 放置到~/.gdbinit中使用 STL-View,STL-View 輸出非常友好,支持 pvector,plist,plist_member,pmap,pmap_member,pset,pdequeue,pstack,pqueue,ppqueue,pbitset,pstring,pwstring,以 Apache Doris 中使用 pvector 為例,它能夠輸出 vector 中的所有元素,
(gdb) pvector block.data
elem[0]: $5 = {
column = {
<COW<doris::vectorized::IColumn>::intrusive_ptr<doris::vectorized::IColumn const>> = {
t = 0x606000fdc820
}, <No data fields>},
type = {
<std::__shared_ptr<doris::vectorized::IDataType const, (__gnu_cxx::_Lock_policy)2>> = {
<std::__shared_ptr_access<doris::vectorized::IDataType const, (__gnu_cxx::_Lock_policy)2, false, false>> = {<No data fields>},
members of std::__shared_ptr<doris::vectorized::IDataType const, (__gnu_cxx::_Lock_policy)2>:
_M_ptr = 0x6030069e9780,
_M_refcount = {
_M_pi = 0x6030069e9770
}
}, <No data fields>},
name = {
static npos = 18446744073709551615,
_M_dataplus = {
<std::allocator<char>> = {
<__gnu_cxx::new_allocator<char>> = {<No data fields>}, <No data fields>},
members of std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_Alloc_hider:
_M_p = 0x61400006e068 "n_nationkey"
},
_M_string_length = 11,
{
_M_local_buf = "n_nationkey\000\276\276\276\276",
_M_allocated_capacity = 7957695015158701934
}
}
}
elem[1]: $6 = {
column = {
<COW<doris::vectorized::IColumn>::intrusive_ptr<doris::vectorized::IColumn const>> = {
t = 0x6080001ec220
}, <No data fields>},
type = {
...
Pretty-Printer
GCC 7.0 開始支持了 Pretty-Printer 列印 STL 容器,可以將以下代碼放置到~/.gdbinit中使 Pretty-Printer 生效,
注意:/usr/share/gcc/python需要更換為本機對應的地址,
python
import sys
sys.path.insert(0, '/usr/share/gcc/python')
from libstdcxx.v6.printers import register_libstdcxx_printers
register_libstdcxx_printers (None)
end
以 vector 為例, Pretty-Printer 能夠列印出詳細內容,
(gdb) p block.data
$1 = std::vector of length 7, capacity 8 = {{
column = {
<COW<doris::vectorized::IColumn>::intrusive_ptr<doris::vectorized::IColumn const>> = {
t = 0x606000fdc820
}, <No data fields>},
type = std::shared_ptr<const doris::vectorized::IDataType> (use count 1, weak count 0) = {
get() = 0x6030069e9780
},
name = "n_nationkey"
}, {
column = {
<COW<doris::vectorized::IColumn>::intrusive_ptr<doris::vectorized::IColumn const>> = {
t = 0x6080001ec220
}, <No data fields>},
type = std::shared_ptr<const doris::vectorized::IDataType> (use count 1, weak count 0) = {
get() = 0x6030069e9750
},
name = "n_name"
}, {
column = {
<COW<doris::vectorized::IColumn>::intrusive_ptr<doris::vectorized::IColumn const>> = {
t = 0x606000fd52c0
}, <No data fields>},
type = std::shared_ptr<const doris::vectorized::IDataType> (use count 1, weak count 0) = {
get() = 0x6030069e9720
},
name = "n_regionkey"
}, {
column = {
<COW<doris::vectorized::IColumn>::intrusive_ptr<doris::vectorized::IColumn const>> = {
t = 0x6030069e96b0
}, <No data fields>},
type = std::shared_ptr<const doris::vectorized::IDataType> (use count 1, weak count 0) = {
get() = 0x604000a66160
},
name = "n_comment"
Boost Pretty Printer
因為 Apache Doris 使用 Boost 不多,因此不再舉例,
可以參考:https://github.com/ruediger/Boost-Pretty-Printer
總結
有了 Sanitizer 能夠在單測、功能、集成、壓力測驗環境及時發現問題,最重要的是大多數時候都可以給出程式出問題的關聯現場,比如記憶體分配的呼叫堆疊,釋放記憶體的呼叫堆疊,非法訪問記憶體的呼叫堆疊,配合 Core Dump 可以查看現場狀態,解決 C++ 記憶體問題從猜測變成了有證據的現場分析,
作者介紹:楊勇強,SelectDB 聯合創始人兼產品VP,同時也是Apache Doris Committer,曾擔任百度智能云存盤部總架構師,主導構建了云存盤技術產品體系,是Linux內核社區貢獻者,
— End —
相關鏈接:
SelectDB 官方網站:
https://selectdb.com
Apache Doris 官方網站:
http://doris.apache.org
Apache Doris Github:
https://github.com/apache/doris
Apache Doris 開發者郵件組:
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/504209.html
標籤:其他
