1. 引言
在生产环境中,有时候我们希望在nginx worker进程crash的时候知道程序是挂在哪段代码,以便进行排障。我们可以用linux操作系统提供的core dump功能,将当时进程的完整镜像都保存下面便于后面用gdb挂载后进行调试。但是core dump功能因为会保存进程的完整内存镜像,所以导致dump下来的文件非常大,经常会把服务器硬盘撑爆。因此,tengine也提供了一个叫做# ngx_http_backtrace_module的模块,它能够在程序crash的时候,自动保存当前进程的调用栈的快照,以便后面进行故障分析,通过这个模块输出的调用栈相对core dump的内容会轻量化很多,这应该说是本模块的优势吧。
本文从源码层面对ngx_http_backtrace_module模块的实现进行深入分析,以便大家了解程序如何来实现调用栈的快照输出功能。
2. 如何配置
ngx_http_backtrace_module模块默认是不开启的,为了开启这个模块,首先需要在编译的时候将ngx_http_backtrace_module模块添加进去。如下:
./configure --add-module=modules/ngx_backtrace_module
make
编译完成后,需要在nginx.conf配置文件中开启本模块功能,如下:
backtrace_log logs/backtrace.log;
backtrace_max_stack_size 30;
http {
...
}
其中backtrace_log配置指令用于指定调用堆栈snapshot的输出文件路径;而backtrace_max_stack_size用来指定支持dump的堆栈的最大深度,一般设置深度为30足够了。
3. 模拟验证
经过以上配置后,先启动nginx。
用ps -ef|grep nginx找到一个nginx的worker进程。
向该worker进程发送SIGSEGV信号,指令如下:
kill -SIGSEGV {nginx worker进程id}
查看logs/backtrace.log文件,如下:
2024/05/21 13:08:46 [error] 6021#0: nginx coredump by signal 11 (SIGSEGV)
./objs/nginx(+0xdf413)[0x56245f928413]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f7c29e2d520]
/lib/x86_64-linux-gnu/libc.so.6(epoll_wait+0x1a)[0x7f7c29f10dea]
./objs/nginx(+0x5821b)[0x56245f8a121b]
./objs/nginx(ngx_process_events_and_timers+0x7a)[0x56245f89527c]
./objs/nginx(ngx_single_process_cycle+0xf6)[0x56245f8a05c3]
./objs/nginx(main+0xb3a)[0x56245f873931]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f7c29e14d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f7c29e14e40]
./objs/nginx(_start+0x25)[0x56245f871f65]
对照gdb打印的堆栈,如下:
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff78fadea in epoll_wait (epfd=3, events=0x5555556d7600, maxevents=512, timeout=timeout@entry=1500) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30 ../sysdeps/unix/sysv/linux/epoll_wait.c: No such file or directory.
(gdb) bt
#0 0x00007ffff78fadea in epoll_wait (epfd=3, events=0x5555556d7600, maxevents=512, timeout=timeout@entry=1500)
at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
#1 0x00005555555ac21b in ngx_epoll_process_events (cycle=0x5555556ca610, timer=1500, flags=1)
at src/event/modules/ngx_epoll_module.c:860
#2 0x00005555555a027c in ngx_process_events_and_timers (cycle=cycle@entry=0x5555556ca610) at src/event/ngx_event.c:284
#3 0x00005555555ab5c3 in ngx_single_process_cycle (cycle=cycle@entry=0x5555556ca610)
at src/os/unix/ngx_process_cycle.c:336
#4 0x000055555557e931 in main (argc=<optimized out>, argv=<optimized out>) at src/core/nginx.c:416
可以看到,前者和后者是一致的(虽然gdb的输出会更加直观),程序就是在调用epoll_wait的系统函数的时候收到了SIGSEGV信号,从而导致程序退出的。本模块输出的堆栈信息,对于一个有经验的c工程师来说,可能已经足够他快速定位代码问题并进行bug修复了。
4. 源码分析
4.1 模块初始化
本模块在模块初始化的时候调用下面函数,如下:
static ngx_int_t
ngx_backtrace_init_worker(ngx_cycle_t *cycle)
{
if (ngx_init_error_signals(cycle->log) == NGX_ERROR) {
return NGX_ERROR;
}
return NGX_OK;
}
而ngx_init_error_signals则是注册了一系列signal信号,并统一由ngx_error_signal_handler函数处理。源码如下:
static ngx_int_t
ngx_init_error_signals(ngx_log_t *log)
{
ngx_signal_t *sig;
struct sigaction sa;
for (sig = ngx_backtrace_signals; sig->signo != 0; sig++) {
ngx_memzero(&sa, sizeof(struct sigaction));
sa.sa_handler = sig->handler;
sigemptyset(&sa.sa_mask);
if (sigaction(sig->signo, &sa, NULL) == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
"sigaction(%s) failed", sig->signame);
return NGX_ERROR;
}
}
return NGX_OK;
}
其中ngx_backtrace_signals的定义如下:
static ngx_signal_t ngx_backtrace_signals[] = {
{ SIGABRT, "SIGABRT", "", ngx_error_signal_handler },
#ifdef SIGBUS
{ SIGBUS, "SIGBUS", "", ngx_error_signal_handler },
#endif
{ SIGFPE, "SIGFPE", "", ngx_error_signal_handler },
{ SIGILL, "SIGILL", "", ngx_error_signal_handler },
{ SIGIOT, "SIGIOT", "", ngx_error_signal_handler },
{ SIGSEGV, "SIGSEGV", "", ngx_error_signal_handler },
{ 0, NULL, "", NULL }
};
所以对于前面我们模拟的SIGSEGV信号,一旦nginx worker进程收到该信号,会由ngx_error_signal_handler进行处理。
4.2 信号处理
ngx_error_signal_handler函数的实现源码如下:
static void
ngx_error_signal_handler(int signo)
{
void *buffer;
size_t size;
ngx_log_t *log;
ngx_signal_t *sig;
struct sigaction sa;
ngx_backtrace_conf_t *bcf;
/* 检查当前的signo是否本模块关心的信号 */
for (sig = ngx_backtrace_signals; sig->signo != 0; sig++) {
if (sig->signo == signo) {
break;
}
}
/* 获取本模块的配置 */
bcf = (ngx_backtrace_conf_t *) ngx_get_conf(ngx_cycle->conf_ctx,
ngx_backtrace_module);
/* 如果本模块特别配置了目标日志文件则用本模块的配置,否则用nginx默认的error log日志 */
log = bcf->log ? bcf->log : ngx_cycle->log;
ngx_log_error(NGX_LOG_ERR, log, 0,
"nginx coredump by signal %d (%s)", signo, sig->signame);
/* 将当前的信号处理函数设置为系统默认,即本模块不再捕获该信号 */
ngx_memzero(&sa, sizeof(struct sigaction));
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
if (sigaction(signo, &sa, NULL) == -1) {
ngx_log_error(NGX_LOG_ERR, log, ngx_errno,
"sigaction(%s) failed", sig->signame);
}
/* 如果没有设置最大堆栈层数,则默认为NGX_BACKTRACE_DEFAULT_STACK_MAX_SIZE */
if (bcf->max_stack_size == NGX_CONF_UNSET) {
bcf->max_stack_size = NGX_BACKTRACE_DEFAULT_STACK_MAX_SIZE;
}
/* 分配用于保存堆栈快照的缓冲区 */
buffer = ngx_pcalloc(ngx_cycle->pool, sizeof(void *) * bcf->max_stack_size);
if (buffer == NULL) {
goto invalid;
}
/* 获取当前进程的堆栈信息 */
size = backtrace(buffer, bcf->max_stack_size);
/* 将堆栈信息输出到目标文件中 */
backtrace_symbols_fd(buffer, size, log->file->fd);
invalid:
/* 因为本模块捕获了当前的信号,这里需要重新模拟发送该信号,
由系统默认的处理函数对该进行进行处理,一般默认处理会是退出当前进程 */
kill(ngx_getpid(), signo);
}
以上代码里面详细地进行了注释,我们可以看到,处理过程中需要将信号的处理函数设置回系统默认的信号处理函数,待本模块完成所有处理后,再重新触发该信号,让系统处理函数能够有机会重新获得处理当前信号的权利。
在这里,我们需要学习一下backtrace和backtrace_symbols_fd两个函数,这也是生成调用堆栈信息的核心函数。
在使用这两个函数之前,首先需要包含execinfo.h头文件,如下:
#include <execinfo.h>
下面是这两个函数的函数原型:
int backtrace(void **buffer, int size);
void backtrace_symbols_fd(void *const *buffer, int size, int fd);
其中backtrace函数的形参说明如下:
void **buffer: 指向一个指针数组,用于存储调用栈中每一帧的返回地址。
int size: 数组 buffer 的大小,即最多能储存多少个返回地址。
其中backtrace_symbols_fd函数的形参说明如下:
void *const *buffer: 指向调用栈中每一帧的返回地址的数组(通常由 backtrace 函数获取)。
int size: 数组 buffer 中的地址数量。
int fd: 文件描述符,指定输出符号信息的目标文件。
backtrace 函数用于获取调用栈的回溯信息,并存储在一个指针数组中,该数组需要调用者提前分配好。
backtrace_symbols_fd 函数用于将这些回溯信息的符号信息直接写到指定的文件描述符中。