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)