高负载多线程C++网络应用偶发SEGV崩溃排查方向咨询
高负载多线程C++网络服务随机崩溃排查方向
故障基础信息
- 业务场景:高负载C++网络应用,采用多线程架构,共启动20个工作线程,单线程每秒新建200条网络连接
- 故障现象:程序运行过程中间歇性崩溃,UndefinedBehaviorSanitizer(UBSan)捕获段错误,错误日志如下:
UndefinedBehaviorSanitizer:DEADLYSIGNAL ==3518897=-ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x7f4a980000b0 (pc 0x7f4a980000b0 bp 0x7f4acbffeb40 sp 0x7f4acbffeaf8 T3521225) ==3518897==The signal is caused by a READ memory access. ==3518897==Hint: PC is at a non-executable region. Maybe a wild jump? #0 0x7f4a980000b0 (<unknown module>) UndefinedBehaviorSanitizer can not provide additional info. SUMMARY: UndefinedBehaviorSanitizer: SEGV (<unknown module>) ==3518897==ABORTING
- 故障特征:崩溃点随机,同等故障概率下,可能出现在业务代码任意位置,也可能出现在依赖的libevent库
malloc调用处,初步怀疑malloc存在线程安全问题。
可行排查路径
注意:不要先入为主判定是malloc线程安全问题,优先排查上层内存破坏类错误
glibc自带的ptmalloc、libevent默认调用的系统malloc均是经过工业界几十年验证的线程安全实现。随机在malloc调用点崩溃的场景中,90%以上的根因是上层代码的内存破坏:malloc用于管理内存块的元数据被越界写、野指针写操作篡改后,后续任意线程调用malloc/free遍历内存块链表时就会触发崩溃,崩溃点完全不固定,和当前观察到的特征完全吻合。
- 第一优先级:换用全量内存检测工具压测复现
单独使用UBSan无法检测内存越界、释放后使用(UAF)这类高频内存错误,编译时增加编译选项-fsanitize=address,undefined -fno-omit-frame-pointer,保留二进制符号表不strip,用AddressSanitizer(ASAN)+UBSan的组合跑同等负载压测。ASAN会在所有内存操作前后插桩校验,90%以上的内存踩坏问题会在第一次非法写操作发生时就打出精准的调用栈,直接定位根因,不会等到内存彻底损坏、程序崩溃时才输出无参考价值的错误栈。
ASAN运行时会带来2~3倍的性能损耗,如果压测环境资源不足,可以等比例降低线程数、新建连接速率,只要保持高频率的内存分配/释放、跨线程对象传递逻辑,一样可以复现问题。 - 第二优先级:针对性核查高频出错代码逻辑
重点排查和网络连接、内存操作强相关的逻辑:- 所有涉及网络包拷贝的
memcpy/memmove/字符串拷贝操作,核查长度参数计算逻辑,尤其是socket读包后的拆包逻辑,未校验网络包携带的长度字段就直接作为拷贝长度,是高负载网络服务最常见的堆越界写诱因 - 跨线程传递的连接上下文、事件回调对象的生命周期:核查是否存在对象已经被一个线程释放,其他线程/事件回调仍持有裸指针进行写操作的情况;libevent是事件驱动模型,多线程场景下如果没有用引用计数兜底回调对象的生命周期,非常容易出现UAF问题,写坏相邻内存块的malloc元数据
- 自定义内存池、对象池逻辑:核查内存块边界判断、多线程并发取/还对象的锁逻辑,排查是否存在块大小计算错误、无锁并发导致的内存块重复分配问题
- 所有涉及网络包拷贝的
- 第三优先级:核查malloc链路兼容性
如果前两步未定位到问题,再排查malloc相关的配置和依赖:- 检查进程实际链接的内存分配器实现:确认是否通过
LD_PRELOAD挂载了自定义的内存统计、泄漏检测动态库,这类非官方库如果实现存在线程安全问题,高负载下很容易触发崩溃;确认是否误链接了非线程安全的第三方malloc实现 - 检查编译链接一致性:确认编译时引用的libevent头文件版本和实际链接的libevent动态库版本完全匹配,版本不一致会导致结构体大小计算偏差,引发内存写越界
- 可以临时替换为tcmalloc/jemalloc跑压测做对比:如果替换分配器后崩溃概率变化甚至消失,不要直接判定原malloc有问题——不同分配器的内存块布局、元数据存储位置存在差异,内存踩坏的触发时机和表现会不同,本质还是上层存在未被发现的内存破坏问题
- 检查进程实际链接的内存分配器实现:确认是否通过
- 第四优先级:coredump事后分析
压测环境提前打开coredump生成:执行ulimit -c unlimited配置coredump输出到固定目录,崩溃后用gdb加载带符号表的二进制和coredump文件分析。从错误日志看,崩溃时的PC指针落在0x7f4a开头的堆地址区间,属于典型的函数指针被写坏后跳转到非法堆地址的场景,可以顺着栈回溯查找被覆盖的函数指针所属的对象,定位被踩内存块的归属。
内容的提问来源于stack exchange,提问作者Евгений Коржиков
相关产品推荐
相关产品推荐

