2016
Apr
25

有一天我們公司將 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
  1. (gdb) bt
  2. #0 0x0xxx in std::basic_string
  3. <char, std::char_traits<char>, std::allocator<char> >::~basic_string() ()
  4. from /usr/lib64/libstdc++.so.6
  5. #1 0x00xx in destroy (this=0x7f2e6c12bc20)
  6. at ...4.6/ext/new_allocator.h:115
  7. #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)
  8. at .../4.4.6/bits/list.tcc:76
  9. #3 0x00007a2e82c8cb49 in ~_List_base (this=<value optimized out>, __in_chrg=<value optimized out>)
  10. at .../4.4.6/bits/stl_list.h:360
  11. #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>)
  12. at /usr/lib/gcc/x86_64-redhat-linux/4.4.6/../../../../include/c++/4.4.6/bits/stl_list.h:418
  13. #5 0x00002e8b8a0b32 in exit () from /lib64/libc.so.6
  14. #6 0x00002e8d01c191 in clean_child_exit (code=0) at prefork.c:200
  15. #7 0x00002e8d01c6a1 in child_main (child_num_arg=<value optimized out>) at prefork.c:692
  16. #8 0x00002e8d01c90a in make_child (s=0x7f2e8e2fce10, slot=45) at prefork.c:768
  17. #9 0x00002e8d01cc3b in startup_children (_pconf=<value optimized out>, plog=<value optimized out>, s=<value optimized out>)
  18. at prefork.c:786
  19. #10 ap_mpm_run (_pconf=<value optimized out>, plog=<value optimized out>, s=<value optimized out>) at prefork.c:1009
  20. #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。

1000 這個數字是可以透過 httpd.conf 修改 MaxRequestsPerChild 的值

我上網查了 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
prefork.c
  1. static void clean_child_exit(int code)
  2. {
  3. mpm_state = AP_MPMQ_STOPPING;
  4.  
  5. apr_signal(SIGHUP, SIG_IGN);
  6. apr_signal(SIGTERM, SIG_IGN);
  7.  
  8. if (pchild) {
  9. apr_pool_destroy(pchild);
  10. }
  11.  
  12. if (one_process) {
  13. prefork_note_child_killed(/* slot */ 0, 0, 0);
  14. }
  15.  
  16. ap_mpm_pod_close(my_bucket->pod);
  17. chdir_for_gprof();
  18. exit(code);
  19. }

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 。

httpd.conf
  1. ServerLimit 1
  2. StartServers 1
  3. MaxRequestsPerChild 3

環境設定好之後,我又用 shell script 寫了一小段 code 來自動連續發多個 Requests ,因為接下來要對程式頻繁的插旗與移除,來測試是哪一段程式的變數宣告造成 apache crash ,所以先寫好快速測試工具是很重要的!!

Example
  1. for i in {1..10}
  2. do
  3. curl -k "http://localhost/?testCoredump"
  4. done

花了幾個小時,終於發現了 static list 宣告的地方,程式是這樣寫的:

keys.cc
  1. static std::list<std::string> keys;
  2.  
  3. std::list<std::string> & addKey (string name) {
  4. keys.clear();
  5. keys.push_back(name);
  6. return keys;
  7. }

這段程式看起來是沒什麼問題,也不懂為什麼 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 問題。

keys.cc
  1. std::list<std::string> addKey (string name) {
  2. std::list<std::string> keys;
  3. keys.push_back(name);
  4. return keys;
  5. }
後續: 想知道 double memory free 的原因嗎? 請期待我的下一篇文章 !

備註

Apache 有兩種啟動模式,一是上面提到的 Multi-Processing Module ,主程序會 fork 出多個 processes ,另一種是 multi-threaded ,主程序會建立多個 threads 來處理 Request 。

相關文章

在 Google 上找到了一篇相似的問題,我們都是因為變數被 destructor 兩次而 core dump ,不過他的問題是使用 dlopen 兩次。


回應 (Leave a comment)