深入理解tengine的backtrace模块

60 篇文章 1 订阅
55 篇文章 0 订阅

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 函数用于将这些回溯信息的符号信息直接写到指定的文件描述符中。

  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农心语

您的鼓励是我写作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值