關(guān)于C++內(nèi)存問(wèn)題排查攻略
作者 | johncchen
C++因其高性能仍然是許多關(guān)鍵應(yīng)用的首選語(yǔ)言,但其復(fù)雜的內(nèi)存管理也帶來(lái)了諸多挑戰(zhàn)。雖然使用現(xiàn)代C++能夠有效解決大部分問(wèn)題,但掌握常用的內(nèi)存問(wèn)題排查方法仍然十分必要,特別是在維護(hù)一些歷史系統(tǒng)時(shí)。本文分為上下兩篇:上篇(15)按照問(wèn)題分類(lèi)介紹和比較常用工具,下篇(67)通過(guò)兩個(gè)具體案例展示這些工具的組合使用,希望能為讀者帶來(lái)有益的啟發(fā)。筆者個(gè)人水平有限,文中難免存在疏漏之處,歡迎大家批評(píng)指正。

一、棧溢出(stack-overflow):查看coredump文件為主,動(dòng)態(tài)檢測(cè)為輔
棧溢出的定位方法主要有靜態(tài)分析、動(dòng)態(tài)檢測(cè)、查看coredump文件三種。
1. 靜態(tài)分析
(1) 原理
GCC提供了-fstack-usage選項(xiàng),能輸出每個(gè)函數(shù)棧的最大使用量。開(kāi)啟后,為每個(gè)編譯目標(biāo)創(chuàng)建.su文件,每行包括函數(shù)名、字節(jié)數(shù)、修飾符(static/dynamic/bounded)中的一個(gè)或多個(gè)。修飾符的含義如下:
- static: 堆棧使用量在編譯時(shí)是已知的,不依賴(lài)于任何運(yùn)行時(shí)條件。
- dynamic: 堆棧使用量依賴(lài)于運(yùn)行時(shí)條件,例如遞歸調(diào)用或基于輸入數(shù)據(jù)的條件分支。
- bounded: 堆棧使用量雖然依賴(lài)于運(yùn)行時(shí)條件,但有一個(gè)可預(yù)知的上限。
(2) 舉個(gè)栗子
void static_stack_usage() { int static_array[5]; }
void dynamic_stack_usage(int n) { int val[n]; }
int main() {
static_stack_usage();
int n = 10;
dynamic_stack_usage(n);
return 0;
}
g++ ./stack_test.cc -o stack_test -fstack-usage
./stack_test.cc:2:6:void static_stack_usage() 16 static
./stack_test.cc:4:6:void dynamic_stack_usage(int) 48 dynamic
./stack_test.cc:6:5:int main() 32 static疑問(wèn):看到這里,估計(jì)有小伙伴會(huì)問(wèn)了:既然dynamic是不確定的,靜態(tài)分析還有意義嗎?其實(shí),實(shí)際代碼的.su一般是下面這種,dynamic和bounded組合在一起,雖然動(dòng)態(tài)但有上限,因此可以計(jì)算出“最大”的棧用量。
xxbuild.cpp:277:5:int XXBuild::BuildPage() 528 dynamic,bounded每個(gè)函數(shù)的棧使用量有了,如果知道函數(shù)的調(diào)用鏈就可以得出棧的最大使用量了。調(diào)用鏈可以從二進(jìn)制文件中反匯編得到。
(3) 工具
靜態(tài)分析常用于資源有限的嵌入式系統(tǒng),常常集成在它們的開(kāi)發(fā)工具中。但非嵌入式系統(tǒng)的這類(lèi)工具比較少。開(kāi)源的有 checkStackUsage等,收費(fèi)的有stackanalyzer等。
注意事項(xiàng):
若使用bazel編譯,默認(rèn)的沙箱模式會(huì)刪除.su文件,因此編譯時(shí)需要增加--spawn_strategy=standalone選項(xiàng)(非沙箱模式)
2. 動(dòng)態(tài)檢測(cè)
(1) 通過(guò)proc文件系統(tǒng)
pmap或查看/proc/pid/maps中的stack,缺點(diǎn)是進(jìn)程退出后就看不到了。
(2) 捕捉操作系統(tǒng)信號(hào)
原理:
- 在 Unix-like 系統(tǒng)中,當(dāng)程序執(zhí)行非法內(nèi)存訪問(wèn)時(shí),操作系統(tǒng)會(huì)向該程序發(fā)送
SIGSEGV信號(hào)(段錯(cuò)誤)。默認(rèn)情況下,接收到此信號(hào)的程序會(huì)終止。 - 如果通過(guò)注冊(cè)一個(gè)自定義的信號(hào)處理函數(shù)來(lái)攔截
SIGSEGV信號(hào),處理函數(shù)會(huì)收到一個(gè)siginfo_t結(jié)構(gòu)體,其中包含錯(cuò)誤的地址和寄存器狀態(tài)等上下文信息,可以判斷是否發(fā)生了棧溢出。
工具:
libsigsegv-devel,可以定義自己的處理函數(shù)來(lái)響應(yīng)內(nèi)存訪問(wèn)錯(cuò)誤,例如嘗試恢復(fù)、記錄錯(cuò)誤信息或者優(yōu)雅地關(guān)閉程序。
注意事項(xiàng):
libsigsegv是GPL協(xié)議
3. 查看coredump文件
重點(diǎn)關(guān)注:
- 層級(jí)是否過(guò)多,是否遞歸調(diào)用
- 棧變量是否過(guò)大
修改棧(以及線(xiàn)程堆棧、協(xié)程堆棧)大小后測(cè)試。
二、棧緩沖區(qū)溢出(stack-buffer-overflow):GCC -fstack-protector/C11 Annex K/AddressSanitizer
棧緩沖區(qū)溢出原因中很大一部分是數(shù)組索引/指針越界。在我看來(lái),在項(xiàng)目中停止使用C風(fēng)格的指針、使用STL容器能解決大部分問(wèn)題。當(dāng)然,一些項(xiàng)目處于維護(hù)狀態(tài),大規(guī)模改造未必合算,可以考慮使用以下工具。
1. GCC -fstack-protector
-fstack-protector的原理:
- 函數(shù)調(diào)用時(shí),編譯器在棧上分配一個(gè)隨機(jī)生成的 canary 值(guard值),通常被放置在局部變量和控制數(shù)據(jù)(如返回地址)之間。
- 函數(shù)執(zhí)行過(guò)程中,所有的局部變量操作都應(yīng)當(dāng)保持 canary 值不變。如果有緩沖區(qū)溢出,超出局部變量的數(shù)據(jù)可能會(huì)覆蓋到 canary 值。
- 如果 canary 值被修改,程序會(huì)認(rèn)為發(fā)生了棧溢出攻擊,通常會(huì)立即終止,例如通過(guò)調(diào)用 __stack_chk_fail() 函數(shù)。
有不同的保護(hù)強(qiáng)度-fstack-protector/-fstack-protector-all/-fstack-protector-strong/-fstack-protector-explicit,一般-fstack-protector-strong即可。
2. C11 Annex K (Bounds-checking interfaces)
使用 C11 標(biāo)準(zhǔn)中引入的strncpy_s()等函數(shù),比 strcpy()/strncpy() 等函數(shù)更安全。它要求指定源和目標(biāo)的大小,并在復(fù)制過(guò)程中檢查這些大小,以防止溢出。如果發(fā)生錯(cuò)誤(如無(wú)效參數(shù)或目標(biāo)太小),strncpy_s() 將設(shè)置 errno 并可以選擇使程序失敗。
較低版本的gcc不支持c11, 可以使用一些第三方實(shí)現(xiàn),比如的openharmony的third_party_bounds_checking_function
3. AddressSanitizer
詳見(jiàn)4.1
4. Valgrind memcheck
詳見(jiàn)4.2
三、內(nèi)存泄漏:eBPF+火焰圖,高效直觀
1.Valgrind memcheck/AddressSanitizer/eBPF bcc-tools memleak比較

eBPF的最大的優(yōu)點(diǎn)是“非侵入”,不需要重新編譯或重啟業(yè)務(wù)進(jìn)程,對(duì)運(yùn)行速度和內(nèi)存用量的影響極小,可以忽略不計(jì),可以線(xiàn)上使用。
2. eBPF bcc-tools memleak檢測(cè)原理
eBPF程序是事件驅(qū)動(dòng)的,在內(nèi)核或應(yīng)用經(jīng)過(guò)特定鉤子點(diǎn)(hook point)時(shí)運(yùn)行。在memleak的源碼中可以看到注冊(cè)到了以下鉤子點(diǎn)
attach_probes("malloc")
attach_probes("calloc")
attach_probes("realloc")
attach_probes("mmap", can_fail=True) # failed on jemalloc
attach_probes("posix_memalign")
attach_probes("valloc", can_fail=True) # failed on Android, is deprecated in libc.so from bionic directory
attach_probes("memalign")
attach_probes("pvalloc", can_fail=True) # failed on Android, is deprecated in libc.so from bionic directory
attach_probes("aligned_alloc", can_fail=True) # added in C11
attach_probes("free", need_uretprobe=False)
attach_probes("munmap", can_fail=True, need_uretprobe=False) # failed on jemalloc3. 舉個(gè)栗子
先寫(xiě)一段內(nèi)存泄漏(不斷增長(zhǎng))的測(cè)試代碼
#include <iostream>
#include <chrono>
#include <thread>
#include <vector>
#include <string>
void LeakOnce(std::vector<std::string>& strs) {
// Generate a random string
std::string str;
const std::string characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
for (int i = 0; i < 10; i++) {
char randomChar = characters[rand() % characters.length()];
str += randomChar;
}
strs.emplace_back(std::move(str));
}
void CallLeak(){
std::vector<std::string> strs;
while(true){
LeakOnce(strs);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main() {
CallLeak();
return 0;
}
g++ ./leak_test.cc -o leak_test --std=c++11 -g檢測(cè)結(jié)果如圖,符合預(yù)期~

memleak具體選項(xiàng)詳見(jiàn)-h,也可以參考官方例子。需要注意的是-O選項(xiàng), attach to allocator functions in the specified object. 如果沒(méi)有使用glibc而是使用jemlloc或tcmalloc,需要使用-O指定二進(jìn)制文件(靜態(tài)鏈接)或動(dòng)態(tài)庫(kù)(動(dòng)態(tài)鏈接)。
4. 改進(jìn)memleak,支持火焰圖
實(shí)際的內(nèi)存泄漏經(jīng)常是小規(guī)模、長(zhǎng)時(shí)間的,會(huì)混雜在大量正常的內(nèi)存申請(qǐng)和釋放動(dòng)作中,這時(shí)候memleak文本形式的輸出就不夠直觀了。想到cpu性能調(diào)優(yōu)經(jīng)常用到的火焰圖,如果memleak能生成直觀的火焰圖就好了。
火焰圖的格式并不復(fù)雜,格式如下:
[堆棧] [采樣值]
main;foo;bar 76PR4766有一個(gè)繪制火焰圖的簡(jiǎn)單實(shí)現(xiàn),沒(méi)有合入主干很可惜。可以參考它,來(lái)修改已安裝的bcc/tools/memleak。修改后執(zhí)行:
/usr/share/bcc/tools/memleak2.py -p $(pgrep leak_test) --report --report-file leak_test.stacks
flamegraph.pl --color=mem --countname="bytes"< leak_test.stacks > leak_test.svg
在中大型項(xiàng)目中,火焰圖能夠很好地區(qū)分框架與業(yè)務(wù)模塊的內(nèi)存操作,便于逐級(jí)排查,非常清晰。
四、其他內(nèi)存問(wèn)題:AddressSanitizer為主,Valgrind memcheck為輔
1. AddressSanitizer
編譯和鏈接時(shí)加上-fsanitize=address,完整選項(xiàng)見(jiàn)AddressSanitizerFlags,一些常用選項(xiàng)如下:
export :ASAN_OPTIONS="log_path=/my_path/asan:abort_on_error=1:disable_coredump=0:unmap_shadow_on_exit=1:debug=true:check_initialization_order=true:print_stats=true:strict_string_checks=true:dump_instruction_bytes=true"
AddressSanitizer會(huì)使程序運(yùn)行慢約2倍,比Valgrind memcheck好太多,可以考慮使用線(xiàn)上節(jié)點(diǎn)排查問(wèn)題。
2. Valgrind memcheck
運(yùn)行速度慢10~50倍,消耗大量?jī)?nèi)存,可以通過(guò)關(guān)閉檢查項(xiàng)目來(lái)提高速度、減少內(nèi)存使用。
五、多線(xiàn)程/協(xié)程的數(shù)據(jù)競(jìng)爭(zhēng)(data race):ThreadSanitizer/Valgrind的helgrind和drd基本不可用,AddressSanitizer仍然可用
1. ThreadSanitizer
編譯和鏈接增加-fsanitize=thread,編譯通常遇到std::atomic_thread_fence報(bào)錯(cuò),官方解釋如下,好吧,std::atomic_thread_fence很常見(jiàn),ThreadSanitizer基本不可用了。
-Wno-tsan Disable warnings about unsupported features in ThreadSanitizer. ThreadSanitizer does not support std::atomic_thread_fence and can report false positives.
除此之外,開(kāi)啟ThreadSanitizer對(duì)運(yùn)行速度和內(nèi)存消耗也有較大影響:
The cost of race detection varies by program, but for a typical program, memory usage may increase by 5-10x and execution time by 2-20x.
2. Valgrind helgrind/drd
比起ThreadSanitizer,需要消耗更多內(nèi)存。我做了個(gè)測(cè)試,一個(gè)使用內(nèi)存2.5G的服務(wù),使用Valgrind helgrind或drd啟動(dòng),32G內(nèi)存都不夠、直接OOM,因此在規(guī)模大些的項(xiàng)目中基本不可用。
3. AddressSanitizer仍然可用
AddressSanitizer不針對(duì)data race,但能檢測(cè)內(nèi)存異常。
下篇以排查某A服務(wù)內(nèi)存問(wèn)題的過(guò)程為例,演示上篇中工具的使用。其實(shí),上篇的工具是下篇踩坑、填坑的經(jīng)驗(yàn)總結(jié)。
六、低成本解決歷史代碼崩潰問(wèn)題
A 服務(wù)中有一大塊老舊的業(yè)務(wù)邏輯,稱(chēng)之為模塊 B,其特點(diǎn)如下:
- 代碼行數(shù)多, 2w+
- 大量 C 風(fēng)格字符串操作(如 strcpy 等),存在越界風(fēng)險(xiǎn)
- 依賴(lài)大量老舊版本的第三方庫(kù)
- 需求很少,處于維護(hù)狀態(tài)
問(wèn)題出現(xiàn):服務(wù)以前運(yùn)行平穩(wěn),但從某天開(kāi)始,線(xiàn)上節(jié)點(diǎn)隔三差五就會(huì)出現(xiàn)崩潰。查看 coredump 文件,發(fā)現(xiàn)崩潰在模塊B的代碼中, frame 0 中某些局部變量損壞。然而,重放崩潰前后一段時(shí)間內(nèi)的請(qǐng)求并不能復(fù)現(xiàn)崩潰,應(yīng)該是其他請(qǐng)求的棧緩沖區(qū)溢出,破壞了這條請(qǐng)求的棧。此類(lèi)問(wèn)題很難直接根據(jù) coredump 文件定位。
排查過(guò)程:如 2.1 中所述,使用 -fstack-protector-strong 重新編譯并上線(xiàn),結(jié)果斷斷續(xù)續(xù)地因?yàn)?nbsp;__stack_chk_fail 出現(xiàn)崩潰,這就好辦了。按圖索驥,發(fā)現(xiàn)是某些請(qǐng)求觸發(fā)了歷史 bug,導(dǎo)致一些局部變量指針越界,針對(duì)性地添加邊界判斷就修復(fù)了,從而以較小的代價(jià)解決了復(fù)雜歷史代碼的崩潰問(wèn)題。
后續(xù)措施:考慮到模塊 B 可能還有其他坑,一旦出現(xiàn)問(wèn)題將導(dǎo)致 A 服務(wù)的節(jié)點(diǎn)崩潰,影響整體 SLA。因此將模塊 B 拆分成獨(dú)立的微服務(wù) C。如果服務(wù) A 調(diào)用服務(wù) C 失敗,可以走降級(jí)鏈路,從而提高業(yè)務(wù)整體的可用性。
七、解決偶發(fā)崩潰問(wèn)題
問(wèn)題出現(xiàn):A 服務(wù)頻繁上線(xiàn),經(jīng)常在一周內(nèi)發(fā)布三四個(gè)版本。某段時(shí)間內(nèi),崩潰的概率顯著增加。查看 coredump 文件,發(fā)現(xiàn)經(jīng)常崩潰在 STL 容器(如 std::map、std::unordered_map、std::vector 等)中 std::allocator 的析構(gòu)相關(guān)函數(shù),但backstrace不確定,有時(shí)在這個(gè)模塊中有時(shí)在那個(gè)模塊中。重放崩潰前后一段時(shí)間內(nèi)的請(qǐng)求無(wú)法復(fù)現(xiàn)崩潰,推測(cè)又是內(nèi)存踩踏問(wèn)題。
第一次嘗試:逐一使用2.1 ~2.3的 GCC -fstack-protector /C11 Annex K/AddressSanitizer ,回放線(xiàn)上請(qǐng)求,結(jié)果都正常,這就尷尬了……
鑒于一時(shí)難以解決問(wèn)題,首先采取措施確保線(xiàn)上穩(wěn)定:
將容器的健康檢查方式從 TCP 改為 HTTP,這樣在 core dump 開(kāi)始而不是結(jié)束后就能檢測(cè)出節(jié)點(diǎn)異常(core 文件約 20G,core dump 過(guò)程持續(xù)幾分鐘),盡早從北極星(服務(wù)注冊(cè)與發(fā)現(xiàn)平臺(tái))上摘除,減少對(duì)線(xiàn)上的影響。這樣線(xiàn)上可以繼續(xù)開(kāi)啟coredump,方便排查問(wèn)題。
第二次嘗試:
通過(guò)監(jiān)控逐漸發(fā)現(xiàn)一些規(guī)律:崩潰集中在進(jìn)程啟動(dòng)階段,日常運(yùn)行時(shí)很少。因此懷疑與進(jìn)程啟動(dòng)時(shí)的狀態(tài)或特定請(qǐng)求有關(guān)。
下一步是復(fù)現(xiàn)問(wèn)題。在崩潰概率最高的地域,新建一個(gè)旁路 workload(兩個(gè)節(jié)點(diǎn)),將北極星權(quán)重調(diào)為其他節(jié)點(diǎn)的 1/N,使用 API 定期重啟旁路 workload 的 pod。經(jīng)過(guò)幾天,問(wèn)題復(fù)現(xiàn)了!
backstrace與之前類(lèi)似,找不出線(xiàn)索。那就上工具吧,能在線(xiàn)上使用的檢測(cè)工具也就只有 AddressSanitizer了,編譯一版部署到旁路 workload,繼續(xù)定期重啟,等待結(jié)果……
果然,斷斷續(xù)續(xù)出現(xiàn)了一些崩潰,但查看 coredump 文件的backstrace仍難以找到有效線(xiàn)索。有時(shí)崩潰在插件中,有時(shí)在 encode 過(guò)程中。咨詢(xún)相關(guān)插件的同學(xué),他們也感到很奇怪,沒(méi)有思路。直到,直到,下面這個(gè)錯(cuò)誤出現(xiàn):
==181==ERROR: AddressSanitizer: attempting double-free on 0x61b000258480 in thread T14 (FiberWorker_02):
#0 0x7f3a1f52a878 in operator delete(void*, unsigned long) ../../../../libsanitizer/asan/asan_new_delete.cpp:164
#1 0x13d4f0c in std::__new_allocator<char>::deallocate(char*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/new_allocator.h:158
#2 0x13d4f0c in std::allocator<char>::deallocate(char*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/allocator.h:200
#3 0x13d4f0c in std::allocator_traits<std::allocator<char> >::deallocate(std::allocator<char>&, char*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/alloc_traits.h:496
#4 0x13d4f0c in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_destroy(unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:300
#5 0x13d4f0c in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_dispose() /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:294
#6 0x13d4f0c in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_mutate(unsigned long, unsigned long, char const*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.tcc:338
#7 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_append(char const*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.tcc:420
#8 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::append(char const*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:1430
#9 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::append(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:1396
#10 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator+=(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:1338
#11 0x1b91ac5 in construct_xx_query(thread_data*) xx/yy/zz/aa_util.cc:66
···
0x61b000258480 is located 0 bytes inside of 1539-byte region [0x61b000258480,0x61b000258a83)
freed by thread T13 (FiberWorker_01) here:
#0 0x7f3a1f52a878 in operator delete(void*, unsigned long) ../../../../libsanitizer/asan/asan_new_delete.cpp:164
#1 0x13d4f0c in std::__new_allocator<char>::deallocate(char*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/new_allocator.h:158
#2 0x13d4f0c in std::allocator<char>::deallocate(char*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/allocator.h:200
#3 0x13d4f0c in std::allocator_traits<std::allocator<char> >::deallocate(std::allocator<char>&, char*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/alloc_traits.h:496
#4 0x13d4f0c in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_destroy(unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:300
#5 0x13d4f0c in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_dispose() /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:294
#6 0x13d4f0c in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_mutate(unsigned long, unsigned long, char const*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.tcc:338
#7 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_append(char const*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.tcc:420
#8 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::append(char const*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:1430
#9 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::append(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:1396
#10 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator+=(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:1338
#11 0x1b91ac5 in construct_xx_query(thread_data*) xx/yy/zz/aa_util.cc:66
···
construct_xx_query(thread_data*) xx/yy/zz/aa_util.cc:66的代碼是
thread_data->string_bb += judge_cc()查看代碼上下文,終于找到了原因!在某類(lèi)請(qǐng)求中使用協(xié)程并發(fā)調(diào)用后端服務(wù),而 thread_data->string_bb(std::string 類(lèi)型)變量是唯一的,多個(gè)協(xié)程同時(shí)修改 thread_data->string_bb,導(dǎo)致 double-free!由于同時(shí)寫(xiě)入是小概率事件,所以崩潰是偶發(fā)的。原來(lái)是 data race 問(wèn)題……
再查看提交歷史,發(fā)現(xiàn)多協(xié)程并發(fā)調(diào)用是在某個(gè)版本上線(xiàn)的,當(dāng)時(shí)一切正常;上百個(gè)版本之后,調(diào)用流程中增加了這行問(wèn)題代碼。冗長(zhǎng)膨脹的流程函數(shù)中新增一行代碼很難引起注意,多人開(kāi)發(fā)非常容易踩坑。
徹底解決問(wèn)題需要從設(shè)計(jì)入手:重構(gòu)流程,遵循單一職責(zé),將修改集中到一處,便于檢查;傳參變成只讀引用,消除 data race。
測(cè)試通過(guò),上線(xiàn),不再崩潰!
總結(jié)
大部分問(wèn)題,尤其是難以排查的問(wèn)題,應(yīng)該在設(shè)計(jì)階段就被解決掉,越往后代價(jià)越大。正所謂“善戰(zhàn)者無(wú)赫赫之功”。


























