4

如何高效解决 C++内存问题,Apache Doris 实践之路|技术解析 - SeleectDB

 2 years ago
source link: https://www.cnblogs.com/SelectDB/p/16659044.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

如何高效解决 C++内存问题,Apache Doris 实践之路|技术解析

导读: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 文件可以分析出何处分配了过大内存。栈举例如下:

06861813865e4e8d805b02a208e1ada8~tplv-k3u1fbpfcp-zoom-1.image

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]


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK