有一天我們公司將 Red Hat 4 升到 Red Hat 6 之後,原本好好的程式,在 RHEL6 卻會出現 Segmentation fault 的錯誤 ,而且要在大流量的情形下,很低機率的發生程式 Crash,這種機率性發生的問題,實在是非常的棘手,但我可是正面的接受了它的挑戰...
下面這段就是 apache 吐出來的 segmentation fault 訊息。
child pid 9981 exit signal Segmentation fault (11), possible coredump in /xxx
我先打開了 linux core file dump 功能,再使用 gdb 來查詢 Apache crash 原因,輸入 back trace 後,可以看到下面這些訊息:
- (gdb) bt
- #0 0x0xxx in std::basic_string
- <char, std::char_traits<char>, std::allocator<char> >::~basic_string() ()
- from /usr/lib64/libstdc++.so.6
- #1 0x00xx in destroy (this=0x7f2e6c12bc20)
- at ...4.6/ext/new_allocator.h:115
- #2 std::_List_base<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >::_M_clear (this=0x1f2e6c12bc20)
- at .../4.4.6/bits/list.tcc:76
- #3 0x00007a2e82c8cb49 in ~_List_base (this=<value optimized out>, __in_chrg=<value optimized out>)
- at .../4.4.6/bits/stl_list.h:360
- #4 std::list<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >::~list (this=<value optimized out>, __in_chrg=<value optimized out>)
- at /usr/lib/gcc/x86_64-redhat-linux/4.4.6/../../../../include/c++/4.4.6/bits/stl_list.h:418
- #5 0x00002e8b8a0b32 in exit () from /lib64/libc.so.6
- #6 0x00002e8d01c191 in clean_child_exit (code=0) at prefork.c:200
- #7 0x00002e8d01c6a1 in child_main (child_num_arg=<value optimized out>) at prefork.c:692
- #8 0x00002e8d01c90a in make_child (s=0x7f2e8e2fce10, slot=45) at prefork.c:768
- #9 0x00002e8d01cc3b in startup_children (_pconf=<value optimized out>, plog=<value optimized out>, s=<value optimized out>)
- at prefork.c:786
- #10 ap_mpm_run (_pconf=<value optimized out>, plog=<value optimized out>, s=<value optimized out>) at prefork.c:1009
- #11 0x0000002e8cff4795 in main (argc=4, argv=0x00fdf67c7a18) at main.c:753
Apache process exit
從 gdb 的訊息中了解,看起來是 apache process 在執行 exit 的時候, 又執行了 ~list
,這代表它對 std list 執行了 destructor ,結果 std list memory 無法被正確的釋放,單單看 gdb 訊息實在是很難找出 root cause ,我改用其它的方式來找出 crash 原因。
再開始找 Crash 原因之前,我們要先來了解一下 Apache 的工作方式,Apache 什麼時候會執行 exit
呢 ? 一個正常的 Apache 啟動的時候,其主程序會使用 root 的身份,同時 fork 出多個 child processes , 而每一個 process 一次只能夠處理一個 Request ,當 process 處理了 1000 個 request 之後,這個 process 就會被中止,然後主程序會另外再 fork 一個新的 child process , 所以從 gdb back trace 中,我們可以知道 process 每處理過 1000 個 request ,執行 exit 後, process 就會 crash,這也正好可以解釋一開始說的 "機率性 Crash " 這件事,每 1000 次的 Request 才會有一次 crash。
我上網查了 apache source ,找到 clean_child_exit 這個 function 執行 exit 的地方如下,比對 gdb 上看到的訊息,確定這真的是 Apache process crash
http://code.metager.de/source/xref/apache/httpd/server/mpm/prefork/prefork.c#240
- static void clean_child_exit(int code)
- {
- mpm_state = AP_MPMQ_STOPPING;
- apr_signal(SIGHUP, SIG_IGN);
- apr_signal(SIGTERM, SIG_IGN);
- if (pchild) {
- apr_pool_destroy(pchild);
- }
- if (one_process) {
- prefork_note_child_killed(/* slot */ 0, 0, 0);
- }
- ap_mpm_pod_close(my_bucket->pod);
- chdir_for_gprof();
- exit(code);
- }
Static variable destroy
https://en.wikipedia.org/wiki/Static_variable
再來我們要了解為什麼 apache process exit 之後,會執行 std list 的 destructor ,是這樣的,如果我們 C/C++ 程式中有用到 static 變數,這個變數會在程式第一次執行的時候,配置一段記憶體位置給它, 又因為 static 的變數只能夠被 initialized 一次,所以一旦記憶體配置完成,這個變數就不會被釋放(第二次 call 它才會拿到同一段記憶體),它會一直等到程式執行結束,也就是執行 exit 的時候, static 變數才會被釋放,而 std list 釋放 memory 的方式,就是執行它的 destructor,這跟我們從 gdb 上看到的訊息也是一致的 。
找出 root cause
從以上資訊,我們可以大概可以知道,C/C++ 程式一定有使用了 std list ,而這個 std list 會在 apache process exit 的時候被釋放 (free memory ),而且還會釋放失敗。
雖然已經知道 apache process 什麼時候會 crash ,但是我們家的程式碼實在太龐大了,一時之間還找不出 std list 到底是寫在哪一支程式,所以我還是傾向能夠先 reproduce 出 coredump ,未來當程式修好後,才有辦法驗證正確性。
我先修改了 apache httpd.conf ,將 ServerLimit 與 StartServers 改成 1 ,這個修改可以確保 apache process 只會存在一個,再來修改 MaxRequestsPerChild 改成 3 ,這樣當 process 執行 3 個 request 後,就會執行一次 process exit 。
- ServerLimit 1
- StartServers 1
- MaxRequestsPerChild 3
環境設定好之後,我又用 shell script 寫了一小段 code 來自動連續發多個 Requests ,因為接下來要對程式頻繁的插旗與移除,來測試是哪一段程式的變數宣告造成 apache crash ,所以先寫好快速測試工具是很重要的!!
- for i in {1..10}
- do
- curl -k "http://localhost/?testCoredump"
- done
花了幾個小時,終於發現了 static list 宣告的地方,程式是這樣寫的:
- static std::list<std::string> keys;
- std::list<std::string> & addKey (string name) {
- keys.clear();
- keys.push_back(name);
- return keys;
- }
這段程式看起來是沒什麼問題,也不懂為什麼 list destructor 會 fail ,跟強者同事討論之後,同事覺得是因為 list 被 double memory free ,第一次的 destructor 會將 list memory free 掉,而第二次的 destructor 反而會因為找不到 list 而 crash。
這段程式已經有點年紀了,在公司的年資可是我的兩倍,看起來是年久失修,程式已經沒什麼意義,也完全看不出用 static 是為了什麼特別用途,這個 function 每次都會將 keys 的資料清空,所以等於不需要重複使用 static 變數的值,除了說 function 不用每次都宣告一個新的 list 之外,就沒有其它好處了,後來我將 static 移除,並且將 call by referenece 改成 call by value ,先用速解的方式處理掉 Segmentation fault 問題。
- std::list<std::string> addKey (string name) {
- std::list<std::string> keys;
- keys.push_back(name);
- return keys;
- }
備註
Apache 有兩種啟動模式,一是上面提到的 Multi-Processing Module ,主程序會 fork 出多個 processes ,另一種是 multi-threaded ,主程序會建立多個 threads 來處理 Request 。
相關文章
在 Google 上找到了一篇相似的問題,我們都是因為變數被 destructor 兩次而 core dump ,不過他的問題是使用 dlopen 兩次。