有一天我们公司将 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 两次。
回應 (Leave a comment)