如何高效解决 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。
其中 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)
异常分配
分配过大的内存 AddressSanitizer 会报出 OOM 错误,根据栈以及 Core Dump 文件可以分析出何处分配了过大内存。栈举例如下:
UBSan 能够高效发现强制类型转换的错误,如下方 Issue 链接中描述,它能够精确的描述出强制类型转换带来错误的代码,如果不能在第一现场发现这种错误,后续因为指针错误使用,会比较难定位。
UndefinedBehaviorSanitizer 也比 AddressSanitizer 及其它的更容易发现死锁。
程序维护内存 Pool 时 AddressSanitizer 的使用
AddressSanitizer 是编译器针对内存分配、释放、访问 生成额外代码来实现内存问题分析的,如果程序维护了自己的内存 Pool,AddressSanitizer 就不能发现 Pool 中内存非法访问的问题。这种情况下需要做一些额外的工作来使得 AddressSanitizer 尽可能工作,主要是使用 ASAN_POISON_MEMORY_REGION 和 ASAN_UNPOISON_MEMORY_REGION 管理内存是否可以访问,这种方法使用比较难,因为 AddressSanitizer 内部有地址对齐等的处理。出于性能以及内存释放等原因,Apache Doris 也维护了内存分配 Pool ,这种方法不能确保 AddressSanitizer 能够发现所有问题。
当程序维护自己的内存池时,按照 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 不多,因此不再举例。
总结
有了 Sanitizer 能够在单测、功能、集成、压力测试环境及时发现问题,最重要的是大多数时候都可以给出程序出问题的关联现场,比如内存分配的调用栈,释放内存的调用栈,非法访问内存的调用栈,配合 Core Dump 可以查看现场状态,解决 C++ 内存问题从猜测变成了有证据的现场分析。
作者介绍:杨勇强,SelectDB 联合创始人兼产品VP,同时也是Apache Doris Committer。曾担任百度智能云存储部总架构师,主导构建了云存储技术产品体系,是Linux内核社区贡献者。
— End —
相关链接:
SelectDB 官方网站:
Apache Doris 官方网站:
Apache Doris Github:
https://github.com/apache/doris
Apache Doris 开发者邮件组:
如何高效解决 C++内存问题,Apache Doris 实践之路|技术解析的更多相关文章
- [Apache Doris] Apache Doris 元数据设计及DDL操作源码阅读
元数据设计 如上图,Doris 的元数据主要存储4类数据: 用户数据信息.包括数据库.表的 Schema.分片信息等. 各类作业信息.如导入作业,Clone 作业.SchemaChange 作业等. ...
- 基于Ansible实现Apache Doris快速部署运维指南
Doris Ansible 使用指南 Apache Doris 介绍 Apache Doris是一个现代化的MPP分析型数据库产品.仅需亚秒级响应时间即可获得查询结果,有效地支持实时数据分析.Apac ...
- Apache Doris 单节点(可多节点)Docker集群制作教程
集群制作Author:苏奕嘉脚本研发Author:种益调研测试Author:杨春东 前言 Apache Doris是当下非常火热和流行的MPP架构OLAP数据库,很多同学想自学/测试Doris的使用和 ...
- [转]怎样解决Myeclipse内存溢出?
在用myeclipes10 开发 遇到了 内存溢出问题,百度了很久,这篇比较完善. 总结起来三个方面去检查 1)myeclipes的配置:myeclipes 10 的安装路径下 的myeclipse. ...
- 解决Windows内存问题的两个小工具RamMap和VMMap(这个更牛更好)
来源:http://www.cr173.com/html/13006_1.html .net程序内存监测分配工具(CLR Profiler for .NET Framework 4)官方安装版 类型: ...
- 解决Windows内存问题的两个小工具RamMap和VMMap
解决Windows内存问题需要对操作系统的深入理解,同时对于如何运用Windows调试器或性能监控器要有工作认知.如果你正试着得到细节,诸如内核堆栈大小或硬盘内存消耗,你会需要调试器命令和内核数据架构 ...
- 解决JVM内存溢出问题
今天遇到了一个问题,当我在增加配置文件(*.xml)内容的时候,重新启动tomcat6时,控制台报错:java.lang.StackOverflowError: 即,栈溢出错误. 内存溢出,即程序运行 ...
- 使用Analyze 和Instruments-Leaks分析解决iOS内存泄露
版权声明:本文为博主原创文章,未经博主允许不得转载. 使用Analyze和Instruments-Leaks分析解决iOS内存泄露 实验的开发环境:Xcode 7 一.使用Product-An ...
- AutoDispose代替RxLifecycle优雅的解决RxJava内存泄漏问题
使用过Rxjava的小伙伴都知道,在使用RxJava时如果处理不当,很可能会产生内存泄漏的问题. 我们使用rxjava最大的原因是响应式编程使我们的异步操作代码变得很优雅,在Android中,也使线程 ...
随机推荐
- JS 会有变量提升和函数提升
JavaScript变量函数声明提升(Hoisting)是在 Javascript 中执行上下文工作方式的一种认识(也可以说是一种预编译),从字面意义上看,"变量提升"意味着变量和 ...
- Spring Boot:整合knife4j
前言 这玩意就swagger的升级版,但是用起来比swagger舒服些,界面也看着好看. knife4j 是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger- ...
- .NET ORM框架HiSql实战-第二章-使用Hisql实现菜单管理(增删改查)
一.引言 上一篇.NET ORM框架HiSql实战-第一章-集成HiSql 已经完成了Hisql的引入,本节就把 项目中的菜单管理改成hisql的方式实现. 菜单管理界面如图: 二.修改增删改查相关代 ...
- 记一次 .NET 某工控数据采集平台 线程数 爆高分析
一:背景 1. 讲故事 前几天有位朋友在 B站 加到我,说他的程序出现了 线程数 爆高的问题,让我帮忙看一下怎么回事,截图如下: 说来也奇怪,这些天碰到了好几起关于线程数无缘无故的爆高,不过那几个问题 ...
- Unsupervised Person Re-identification by Soft Multilabel Learning
简介: 这是一篇19年CVPR的跨域无监督Re-ID论文,在Market1501和DukeMTMC-reID上分别达到了67.7%和67.1%的rank-1精度,算是一篇将准确度刷得比较高的论文了,在 ...
- 练习-用if语句替换三元运算符和选择结构-标准的switch语句
if语句和三元运算符的互换 在某些简单的应用中,if语句是可以和三元运算符互换使用的 public static void main(String[] args) { int a = 10; int ...
- 万字干货|Java基础面试题(2022版)
目录 概念常识 Java 语言有哪些特点? JVM.JRE和JDK的关系 什么是字节码? 为什么说 Java 语言是"编译与解释并存"? Oracle JDK 和OpenJDK的区 ...
- Visual Studio 生产环境配置方案:SlowCheetah
原文 SlowCheetah 能满足你不同编译模式产生不同 app.config 配置的需求,已被微软纳入麾下,支持XML,JSON格式. 下面我们用XML的格式来试试如何使用SlowCheetah, ...
- 【破解】设置 Codesys for Raspberry 每118分钟自动重启Runtime
Codesys for Raspberry 无授权时,试用2小时后会自动退出,重启Runtime后就又恢复2小时试用时长. 官网授权购买地址: [单核] https://store.codesys.c ...
- 水电表/压力表/传感器/流量计/行车记录仪/分贝仪等 超低功耗LCD段码液晶驱动IC-VKL076(VKL系列)SSOP28 19*4COM,工作电流约7.5微安
产品品牌:永嘉微电/VINKA 产品型号:VKL076 封装形式:SSOP28 产品年份:新年份 概述: VKL076 SSOP28是一个点阵式存储映射的LCD驱动器,可支持最大76点(19SEGx4 ...