问题:
我在nginx中fork出一个进程来专门做DNS解析的工作,在本地开发环境中一切正常,而在线上服务器环境中这个进程就不定期的死掉重启,而且还不太容易复现。
排查:
直接在线上服务器开启coredump,让linux帮忙把进程挂掉时的core文件保存下来,再用gdb分析,后来如愿得到core文件了,用gdb调试,从调用堆栈来看,是死在glibc的_int_malloc函数中,再细看是一个指针值等于0x4,而对这个指针进行解引用将引起段错误,从而进程挂掉重启。这种问题肯定是glibc的内存管理器被整乱了,导致这个问题也只有两个原因:一是内存越界写入,二是重复释放内存。这个问题是与glibc有关,所以第二种可能比较大。关于内存问题有挺多调试工具的,比如valgrind、mudflap、efence,可惜我的这个问题用这几个工具都没找出来,后来只能用SystemTap来调试了,调试脚本就是上一篇SystemTap使用技巧【四】讲的技巧,如下:
probe begin {
printf("=============begin============\n")
}
//记录内存分配和释放的计数关联数组
global g_mem_ref_tbl
//记录内存分配和释放的调用堆栈关联数组
global g_mem_bt_tbl
probe process("/lib/x86_64-linux-gnu/libc.so.6").function("__libc_malloc").return, process("/lib/x86_64-linux-gnu/libc.so.6").function("__libc_calloc").return {
if (target() == pid()) {
if (g_mem_ref_tbl[$return] == 0) {
g_mem_ref_tbl[$return]++
g_mem_bt_tbl[$return] = sprint_ubacktrace()
}
}
}
probe process("/lib/x86_64-linux-gnu/libc.so.6").function("__libc_free").call {
if (target() == pid()) {
g_mem_ref_tbl[$mem]--
if (g_mem_ref_tbl[$mem] == 0) {
if ($mem != 0) {
//记录上次释放的调用堆栈
g_mem_bt_tbl[$mem] = sprint_ubacktrace()
}
} else if (g_mem_ref_tbl[$mem] < 0 && $mem != 0) {
//如果调用free已经失衡,那就出现了重复释放内存的问题,这里输出当前调用堆栈,以及这个地址上次释放的调用堆栈
printf("MMMMMMMMMMMMMMMMMMMMMMMMMMMM\n")
printf("g_mem_ref_tbl[%p]: %d\n", $mem, g_mem_ref_tbl[$mem])
print_ubacktrace()
printf("last free backtrace:\n%s\n", g_mem_bt_tbl[$mem])
printf("WWWWWWWWWWWWWWWWWWWWWWWWWWWW\n")
}
}
}
probe end {
//最后输出产生泄漏的内存是在哪里分配的
printf("=============end============\n")
foreach(mem in g_mem_ref_tbl) {
if (g_mem_ref_tbl[mem] > 0) {
printf("%s\n", g_mem_bt_tbl[mem])
}
}
}
调试得到的结果类似下图(这个图是自己写一个简单的ldns例子的调试结果):
从上面的截图中可以看出,在libldns.so.1.6.11库里面有两处重复释放了内存,还从上图标出的第一次释放内存的堆栈中看出调用路径是ldns_resolver_pop_nameserver->__libc_realloc->free,而第二次调用路径是ldns_resolver_deep_free->free,下面先看看ldns_resolver_pop_nameserver和ldns_resolver_deep_free这两个函数的实现代码:
ldns_resolver_pop_nameserver函数中第256、257行都用到了LDNS_XREALLOC,这是一个宏,就是realloc,在上面SystemTap结果的调用路径中两处从ldns_resolver_pop_nameserver调用__libc_realloc再到free的地方应该就是这两代码了,那为什么会调用到free呢,realloc代码暂且不看了,先看看这两行代码的第三个参数都是(ns_count - 1),而ns_count就是之前ldns_resolver_push_nameserver进去的nameserver个数,如果为1,那ns_count-1就为0,相当于调用realloc调整一个内存大小为0,realloc将直接把这块内存给释放掉,然后返回NULL,这些从realloc的实现代码可以得到证实。所以ldns_resolver_pop_nameserver函数的第260、262行将不会得到执行,从而保存在r中的nameservers和rtt这两个指针就变为野指针,然后在ldns_resolver_deep_free函数的第916、936行又把这两个野指针再释放一次,所以glibc的内存管理器链表就乱了,下次再申请内存时就很容易出段错误了。
问题的根本原因已经找到了,那解决办法就好办了->
修复:
因为ldns这个库是用apt-get安装的,版本为1.6.11,比较老,官网的版本已经为1.6.17了,下载最新代码对比一下,这个bug的确已经被官网修复了:
再多个版本之间diff几次发现,在1.6.14这个版本就已经修复了这个问题。所以解决办法就是更新ldns库到最新版本就可以了,如果更新不了ldns库的话,就不要调用ldns_resolver_pop_nameserver函数了,反正在ldns_resolver_deep_free函数里也会再次把内存释放掉,只是后面这种方法不能在一个ldns_resolver句柄中切换DNS nameserver了,建议还是更新ldns到最新版吧,老版本的一些其他bug可能在新版本中也解决了呢。