如何高效解決 C++記憶體問題,Apache Doris 實踐之路|技術解析

来源:https://www.cnblogs.com/SelectDB/archive/2022/09/05/16659044.html
-Advertisement-
Play Games

摘要:經常有朋友問,學 Python 面向對象時,翻閱別人代碼,會發現一個 super() 函數,那這個函數的作用到底是什麼? 本文分享自華為雲社區《Python 中的 super 函數怎麼學,怎麼解?》,作者:夢想橡皮擦。 實戰場景 經常有朋友問,學 Python 面向對象時,翻閱別人代碼,會發現 ...


導讀: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 開發者郵件組:

[email protected]


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 發現問題 前幾天在看別人的項目的時候,發現一個問題,簡單復現一下這個問題 // 註意這是一個Integer對象的數組哦 Integer[] arr = new Integer[]{9999,88,77}; List<Integer> list = Arrays.asList(arr); // 執行以 ...
  • 前置知識 什麼是進程,什麼又是線程?咱不是講系統,簡單說下,知道個大概就好了。 進程:一個可執行文件執行的過程。 線程:操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務 什 ...
  • Spring(三)——AOP 概念 什麼是AOP (1)面向切麵編程(方面),利用 AOP 可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。 (2)通俗描述:不通過修改源代碼方式,在主幹功能裡面添加新功能 AOP底層原理 JDK動態 ...
  • 線程基礎03 6.用戶線程和守護線程 用戶線程:也叫工作線程,當線程的任務執行完或者通知方法結束。平時用到的普通線程均是用戶線程,當在Java程式中創建一個線程,它就被稱為用戶線程 守護線程(Daemon):一般是為工作線程服務的,當所有的用戶線程結束,守護線程自動結束 常見的守護線程:垃圾回收機制 ...
  • 前文再續,上一回我們完成了用戶管理模塊的CURD(增刪改查)功能,功能層面,無甚大觀,但有一個結構性的缺陷顯而易見,那就是項目結構過度耦合,項目的耦合性(Coupling),也叫耦合度,進而言之,模塊之間的關係,是對項目結構中各模塊間相互聯繫緊密程度的一種量化。耦合的強弱取決於模塊間調用的複雜性、調 ...
  • 非同步編程在 Rust 中的地位非常高,很多 crate 尤其是多IO操作的都使用了 async/await. 首先弄清楚非同步編程的幾個基本概念: Future Future 代表一個可在未來某個時候獲取返回值的 task,為了獲取這個 task 的執行狀況,Future 提供了一個函數用於判斷該 t ...
  • 引入依賴 <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>2.5.5</version> </dependency> 基礎創建方式 Cac ...
  • 序言 每逢佳節倍思親,想買個東西給家裡,結果發現手速不夠,網速不夠快,沒有時間下單等等各種原因導致最後想買的東西售罄了… 甚至跟你一起搶購的可能是腳本,太真實了! 今天就給大家分享一個python版搶購月餅的腳本,我們要用魔法打敗魔法!話不多說,直接開搞! 準備工作 今天要用的是一個測試工具的庫:S ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...