導(dǎo)讀:Apache Doris 使用 C++ 語(yǔ)言實(shí)現(xiàn)了執(zhí)行引擎,C++ 開(kāi)發(fā)過(guò)程中,影響開(kāi)發(fā)效率的一個(gè)重要因素是指針的使用,包括非法訪問(wèn)、泄露、強(qiáng)制類型轉(zhuǎn)換等。本文將會(huì)通過(guò)對(duì) Sanitizer 和 Core Dump 分析工具的介紹來(lái)為大家分享:如何快速定位 Apache Doris 中的 C++ 問(wèn)題,幫助開(kāi)發(fā)者提升開(kāi)發(fā)效率并掌握更高效的開(kāi)發(fā)技巧。
成都創(chuàng)新互聯(lián)公司堅(jiān)持“要么做到,要么別承諾”的工作理念,服務(wù)領(lǐng)域包括:成都網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣等服務(wù),滿足客戶于互聯(lián)網(wǎng)時(shí)代的文安網(wǎng)站設(shè)計(jì)、移動(dòng)媒體設(shè)計(jì)的需求,幫助企業(yè)找到有效的互聯(lián)網(wǎng)解決方案。努力成為您成熟可靠的網(wǎng)絡(luò)建設(shè)合作伙伴!
?作者|Apache Doris Committer楊勇強(qiáng)
Apache Doris 是一款高性能 MPP 分析型數(shù)據(jù)庫(kù),出于性能的考慮,Apache Doris 使用了 C++ 語(yǔ)言實(shí)現(xiàn)了執(zhí)行引擎。在 C++ 開(kāi)發(fā)過(guò)程中,影響開(kāi)發(fā)效率的一個(gè)重要因素是指針的使用,包括非法訪問(wèn)、泄露、強(qiáng)制類型轉(zhuǎn)換等。Google Sanitizer 是由 Google 設(shè)計(jì)的用于動(dòng)態(tài)代碼分析的工具,在 Apache Doris 開(kāi)發(fā)過(guò)程中遭遇指針使用引起的內(nèi)存問(wèn)題時(shí),正是因?yàn)橛辛?Sanitizer,使得問(wèn)題解決效率可以得到數(shù)量級(jí)的提升。除此以外,當(dāng)出現(xiàn)一些內(nèi)存越界或非法訪問(wèn)的情況導(dǎo)致 BE 進(jìn)程 Crash 時(shí),Core Dump 文件是非常有效的定位和復(fù)現(xiàn)問(wèn)題的途徑,因此一款高效分析 CoreDump 的工具也會(huì)進(jìn)一步幫助更加快捷定位問(wèn)題。
本文將會(huì)通過(guò)對(duì) Sanitizer 和 Core Dump 分析工具的介紹來(lái)為大家分享:如何快速定位 Apache Doris 中的 C++ 問(wèn)題,幫助開(kāi)發(fā)者提升開(kāi)發(fā)效率并掌握更高效的開(kāi)發(fā)技巧。
定位 C++ 程序內(nèi)存問(wèn)題常用的工具有兩個(gè),Valgrind 和 Sanitizer。
二者的對(duì)比可以參考:https://developers.redhat.com/blog/2021/05/05/memory-error-checking-in-c-and-c-comparing-sanitizers-and-valgrind
其中 Valgrind 通過(guò)運(yùn)行時(shí)軟件翻譯二進(jìn)制指令的執(zhí)行獲取相關(guān)的信息,所以 Valgrind 會(huì)非常大幅度的降低程序性能,這就導(dǎo)致在一些大型項(xiàng)目比如 Apache Doris 使用 Valgrind 定位內(nèi)存問(wèn)題效率會(huì)很低。
而 Sanitizer 則是通過(guò)編譯時(shí)插入代碼來(lái)捕獲相關(guān)的信息,性能下降幅度比 Valgrind 小很多,使得能夠在單測(cè)以及其它測(cè)試環(huán)境默認(rèn)使用 Saintizer。
Sanitizer 的算法可以參考:https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm
在 Apache Doris 中,我們通常使用 Sanirizer 來(lái)定位內(nèi)存問(wèn)題。LLVM 以及 GNU C++ 有多個(gè) Sanitizer:
其中 AddressSanitizer, AddressSanitizerLeakSanitizer 以及 UndefinedBehaviorSanitizer 對(duì)于解決指針相關(guān)的問(wèn)題最為有效。
Sanitizer 不但能夠發(fā)現(xiàn)錯(cuò)誤,而且能夠給出錯(cuò)誤源頭以及代碼位置,這就使得問(wèn)題的解決效率很高,通過(guò)一些例子來(lái)說(shuō)明 Sanitizer 的易用程度。
可以參考此處使用 Sanitizer:https://github.com/apache/doris/blob/master/be/CMakeLists.txt
Sanitizer 和 Core Dump 配合定位問(wèn)題非常高效,默認(rèn) Sanitizer 不生成 Core Dump 文件,可以使用如下環(huán)境變量生成 Core Dump文件,建議默認(rèn)打開(kāi)。
可以參考: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
使用如下環(huán)境變量讓 UBSan 生成代碼棧,默認(rèn)不生成。
export UBSAN_OPTIONS=print_stacktrace=1
有時(shí)候需要顯示指定 Symbolizer 二進(jìn)制的位置,這樣 Sanitizer 就能夠直接生成可讀的代碼棧。
export ASAN_SYMBOLIZER_PATH=your path of llvm-symbolizer
User after free 是指訪問(wèn)釋放的內(nèi)存,針對(duì) use after free 錯(cuò)誤,AddressSanitizer 能夠報(bào)出使用釋放地址的代碼棧,地址分配的代碼棧,地址釋放的代碼棧。比如:https://github.com/apache/doris/issues/9525中,使用釋放地址的代碼棧如下:
==ERROR: AddressSanitizer: heap-use-after-free on address 0xc420 at pc 0xf61a4f0 bp 0x7fd89a0 sp 0x7fd8990
READ of size 1 at 0xc420 thread T94 (MemTableFlushTh)
#0 0xf61a4ef in doris::faststring::append(void const*, unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/util/faststring.h:120
// 更詳細(xì)的代碼棧請(qǐng)前往https://github.com/apache/doris/issues/9525查看
此地址初次分配的代碼棧如下:
previously allocated by thread T94 (MemTableFlushTh) here:
#0 0xe9b74b7 in __interceptor_malloc (/mnt/ssd01/tjp/regression_test/be/lib/palo_be+0x536a4b7)
#1 0xee in Allocator::alloc_no_track(unsigned long, unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/vec/common/allocator.h:223
#2 0xee in Allocator::alloc(unsigned long, unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/vec/common/allocator.h:104
地址釋放的代碼棧如下:
0xc420 is located 16 bytes inside of 32-byte region [0xc410,0xc430)
freed by thread T94 (MemTableFlushTh) here:
#0 0xe9b7868 in realloc (/mnt/ssd01/tjp/regression_test/be/lib/palo_be+0x536a868)
#1 0xee8b913 in Allocator::realloc(void*, unsigned long, unsigned long, unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/vec/common/allocator.h:125
#2 0xee814bb in void doris::vectorized::PODArrayBase<1ul, 4096ul, Allocator, 15ul, 16ul>::realloc<>(unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/vec/common/pod_array.h:147
有了詳細(xì)的非法訪問(wèn)地址代碼棧、分配代碼棧、釋放代碼棧,問(wèn)題定位就會(huì)非常容易。
說(shuō)明:限于文章篇幅,示例中的棧展示不全,完整代碼??梢郧巴鶎?duì)應(yīng) Issue 中進(jìn)行查看。
AddressSanitizer 能夠報(bào)出 heap buffer overflow 的代碼棧。
比如https://github.com/apache/doris/issues/5951 里的,結(jié)合運(yùn)行時(shí)生成的 Core Dump 文件就可以快速定位問(wèn)題。
==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
AddressSanitizer 能夠報(bào)出哪里分配的內(nèi)存沒(méi)有被釋放,就可以快速的分析出泄露原因。
====ERROR: LeakSanitizer: detected memory leaks
Direct leak of 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/
https://github.com/apache/doris/pull/3326
分配過(guò)大的內(nèi)存 AddressSanitizer 會(huì)報(bào)出 OOM 錯(cuò)誤,根據(jù)棧以及 Core Dump 文件可以分析出何處分配了過(guò)大內(nèi)存。棧舉例如下:
Fix PR 見(jiàn):https://github.com/apache/doris/pull/
UBSan 能夠高效發(fā)現(xiàn)強(qiáng)制類型轉(zhuǎn)換的錯(cuò)誤,如下方 Issue 鏈接中描述,它能夠精確的描述出強(qiáng)制類型轉(zhuǎn)換帶來(lái)錯(cuò)誤的代碼,如果不能在第一現(xiàn)場(chǎng)發(fā)現(xiàn)這種錯(cuò)誤,后續(xù)因?yàn)橹羔樺e(cuò)誤使用,會(huì)比較難定位。
Issue:https://github.com/apache/doris/issues/9105
UndefinedBehaviorSanitizer 也比 AddressSanitizer 及其它的更容易發(fā)現(xiàn)死鎖。
比如:https://github.com/apache/doris/issues/
AddressSanitizer 是編譯器針對(duì)內(nèi)存分配、釋放、訪問(wèn) 生成額外代碼來(lái)實(shí)現(xiàn)內(nèi)存問(wèn)題分析的,如果程序維護(hù)了自己的內(nèi)存 Pool,AddressSanitizer 就不能發(fā)現(xiàn) Pool 中內(nèi)存非法訪問(wèn)的問(wèn)題。這種情況下需要做一些額外的工作來(lái)使得 AddressSanitizer 盡可能工作,主要是使用 ASAN_POISON_MEMORY_REGION 和 ASAN_UNPOISON_MEMORY_REGION 管理內(nèi)存是否可以訪問(wèn),這種方法使用比較難,因?yàn)?AddressSanitizer 內(nèi)部有地址對(duì)齊等的處理。出于性能以及內(nèi)存釋放等原因,Apache Doris 也維護(hù)了內(nèi)存分配 Pool ,這種方法不能確保 AddressSanitizer 能夠發(fā)現(xiàn)所有問(wèn)題。
可以參考:https://github.com/apache/doris/pull/8148
當(dāng)程序維護(hù)自己的內(nèi)存池時(shí),按照 https://github.com/apache/dorisw/pull/8148 中方法,use after free 錯(cuò)誤會(huì)變成 use after poison。但是 use after poison 不能夠給出地址失效的棧(https://github.com/google/sanitizers/issues/191),從而導(dǎo)致問(wèn)題的定位分析仍然很困難。
因此建議程序維護(hù)的內(nèi)存 Pool 可以通過(guò)選項(xiàng)關(guān)閉,這樣在測(cè)試環(huán)境就可以使用 AddressSanitizer 高效地定位內(nèi)存問(wèn)題。
分析 C++ 程序生成的 Core Dump 文件經(jīng)常遇到的問(wèn)題就是怎么打印出 STL 容器中的值以及 Boost 中容器的值,有如下三個(gè)工具可以高效的查看 STL 和 Boost 中容器的值。
可以將此文件 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 = {
::intrusive_ptr> = {
t = 0xfdc820
}, },
type = {
> = {
> = {},
members of std::__shared_ptr:
_M_ptr = 0xe9780,
_M_refcount = {
_M_pi = 0xe9770
}
}, },
name = {
static npos = ,
_M_dataplus = {
> = {
<__gnu_cxx::new_allocator> = {}, },
members of std::__cxx11::basic_string, std::allocator >::_Alloc_hider:
_M_p = 0xe068 "n_nationkey"
},
_M_string_length = 11,
{
_M_local_buf = "n_nationkey\000\276\276\276\276",
_M_allocated_capacity =
}
}
}
elem[1]: $6 = {
column = {
::intrusive_ptr> = {
t = 0xec220
}, },
type = {
...
GCC 7.0 開(kāi)始支持了 Pretty-Printer 打印 STL 容器,可以將以下代碼放置到~/.gdbinit中使 Pretty-Printer 生效。
注意:/usr/share/gcc/python需要更換為本機(jī)對(duì)應(yīng)的地址。
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 能夠打印出詳細(xì)內(nèi)容。
(gdb) p block.data
$1 = std::vector of length 7, capacity 8 = {{
column = {
::intrusive_ptr> = {
t = 0xfdc820
}, },
type = std::shared_ptr (use count 1, weak count 0) = {
get() = 0xe9780
},
name = "n_nationkey"
}, {
column = {
::intrusive_ptr> = {
t = 0xec220
}, },
type = std::shared_ptr (use count 1, weak count 0) = {
get() = 0xe9750
},
name = "n_name"
}, {
column = {
::intrusive_ptr> = {
t = 0xfd52c0
}, },
type = std::shared_ptr (use count 1, weak count 0) = {
get() = 0xe9720
},
name = "n_regionkey"
}, {
column = {
::intrusive_ptr> = {
t = 0xe96b0
}, },
type = std::shared_ptr (use count 1, weak count 0) = {
get() = 0xa
},
name = "n_comment"
因?yàn)?Apache Doris 使用 Boost 不多,因此不再舉例。
可以參考:https://github.com/ruediger/Boost-Pretty-Printer
有了 Sanitizer 能夠在單測(cè)、功能、集成、壓力測(cè)試環(huán)境及時(shí)發(fā)現(xiàn)問(wèn)題,最重要的是大多數(shù)時(shí)候都可以給出程序出問(wèn)題的關(guān)聯(lián)現(xiàn)場(chǎng),比如內(nèi)存分配的調(diào)用棧,釋放內(nèi)存的調(diào)用棧,非法訪問(wèn)內(nèi)存的調(diào)用棧,配合 Core Dump 可以查看現(xiàn)場(chǎng)狀態(tài),解決 C++ 內(nèi)存問(wèn)題從猜測(cè)變成了有證據(jù)的現(xiàn)場(chǎng)分析。
作者介紹:楊勇強(qiáng),SelectDB 聯(lián)合創(chuàng)始人兼產(chǎn)品VP,同時(shí)也是Apache Doris Committer。曾擔(dān)任百度智能云存儲(chǔ)部總架構(gòu)師,主導(dǎo)構(gòu)建了云存儲(chǔ)技術(shù)產(chǎn)品體系,是Linux內(nèi)核社區(qū)貢獻(xiàn)者。
— End —
相關(guān)鏈接:
SelectDB 官方網(wǎng)站:
https://selectdb.com
Apache Doris 官方網(wǎng)站:
http://doris.apache.org
Apache Doris Github:
https://github.com/apache/doris
Apache Doris 開(kāi)發(fā)者郵件組:
dev@doris.apache.org