Nginx运行时二进制更新代码分析

来源:互联网 发布:final 定义数组 编辑:程序博客网 时间:2024/06/13 14:32

1.本文内容

要实现运行时二进制更新,既要保证服务器使用更新后的二进制文件处理新接受的任务,又要保证服务器更新前正在处理的任务都能正确处理完再结束,Nginx将其称为从容地退出(graceful exit)。本文主要内容就是通过分析Nginx代码,了解Nginx如何实现这两个功能。具体包括:1Nginx运行时二进制文件更新命令。2)启动新进程。3)从容关闭老进程。阅读本文之前要对Nginx多进程模型,以及事件模型有一定了解,望读者自行查阅资料。

 

2. Nginx二进制文件更新命令

       Nginx使用Unix信号机制来触发这一功能,更新命令为:

kill -USR2 `cat /usr/local/nginx/nginx.pid`

kill -QUIT `cat /usr/local/nginx/nginx.pid.oldbin`

       Nginx源码目录下,文件ngx_config.h中有以下代码:

#define NGX_CHANGEBIN_SIGNAL     USR2

#define NGX_SHUTDOWN_SIGNAL      QUIT

因为文件/usr/local/nginx/nginx.pid中保存的是当前Nginx进程pid,所以,第一条命令旨在告诉Nginx二进制文件改变了,Nginx会将/usr/local/nginx/nginx.pid重命名为/usr/local/nginx/nginx.pid.oldbin。所以,第二条命令旨在通知Nginx老进程从容地退出。下面我们就来看这两条命令的背后到底发生了什么。

 

3. 启动新进程

了解Nginx如何启动新进程的关键在于查看Nginx处理信号USR2的代码。该部分代码在文件ngx_process.c的函数ngx_signal_hander()中,代码片段如下所示。

    switch (ngx_process) {

    case
 NGX_PROCESS_MASTER:
    case
 NGX_PROCESS_SINGLE:
        switch (
signo) {

        case
 ngx_signal_value(NGX_SHUTDOWN_SIGNAL):
            ngx_quit = 1;
            action = ", shutting down";
            break;

        case
 ngx_signal_value(NGX_TERMINATE_SIGNAL):
        case
 SIGINT:
            ngx_terminate = 1;
            action = ", exiting";
            break;

        case
 ngx_signal_value(NGX_NOACCEPT_SIGNAL):
            if (
ngx_daemonized) {
                ngx_noaccept = 1;
                action = ", stop accepting connections";
            }
            break;

        case
 ngx_signal_value(NGX_RECONFIGURE_SIGNAL):
            ngx_reconfigure = 1;
            action = ", reconfiguring";
            break;

        case
 ngx_signal_value(NGX_REOPEN_SIGNAL):
            ngx_reopen = 1;
            action = ", reopening logs";
            break;


        case ngx_signal_value(NGX_CHANGEBIN_SIGNAL):

/* getppid() > 1 || ngx_new_binary > 0表明目前正在进行二进制更新处理的阶段,所以直接忽视该信号*/
            if (getppid() > 1 || ngx_new_binary > 0) {
                action = ", ignoring";
                ignore = 1;
                break;
            }


            ngx_change_binary = 1;
            action = ", changing binary";
            break;


        case SIGALRM:
            ngx_sigalrm = 1;
            break;

        case
 SIGIO:
            ngx_sigio = 1;
            break;

        case
 SIGCHLD:
            ngx_reap = 1;
            break;
        }

        break;

    case
 NGX_PROCESS_WORKER:
    case
 NGX_PROCESS_HELPER:
        switch (
signo) {

        case
 ngx_signal_value(NGX_NOACCEPT_SIGNAL):
            if (!
ngx_daemonized) {
                break;
            }

            ngx_debug_quit = 1;
        case
 ngx_signal_value(NGX_SHUTDOWN_SIGNAL):
            ngx_quit = 1;
            action = ", shutting down";
            break;

        case
 ngx_signal_value(NGX_TERMINATE_SIGNAL):
        case
 SIGINT:
            ngx_terminate = 1;
            action = ", exiting";
            break;

        case
 ngx_signal_value(NGX_REOPEN_SIGNAL):
            ngx_reopen = 1;
            action = ", reopening logs";
            break;

        case
 ngx_signal_value(NGX_RECONFIGURE_SIGNAL):
        case ngx_signal_value(NGX_CHANGEBIN_SIGNAL):
        
case SIGIO:
            action = ", ignoring";
            break;
        }


        break;
    }


       由以上红色部分代码可以看出,当进程为master类型时,将标志位ngx_binary_change设置为1。当进程为worker类型时,忽略该信号。接下来,我们看看Nginx如何处理标志位ngx_binary_change

       在文件ngx_process_cycle.c的函数ngx_master_process_cycle中有如下代码片段:

if (ngx_change_binary) {
      ngx_change_binary = 0;
      ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "changing binary");
      ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv);
}

由以上代码可以看出,在master进程的事件处理函数中,如果标志ngx_change_binary被设置为1,则调用函数ngx_exec_new_binary执行新的二进制文件。函数ngx_exec_new_binary在文件nginx.c,该函数的执行流程如下:

1)            构造启动新进程需要的环境变量。该环境变量的内容包括当前进程的环境变量以及当前进程的所有侦听socket描述字。

2)            给当前进程的pid文件添上后缀名oldbin

3)            fork一个子进程并触发系统调用execve,该系统调用使用更新后的二进制文件路径作为参数。

这样看来,由于Nginx只有一个可执行程序,当该可执行程序更新时,就创建一个子进程来执行新的二进制文件。同时要注意的是老进程通过环境变量将所有的侦听描述字传递给新进程,又因为文件描述字可以跨execve系统调用保留(关于跨exec函数保留文件描述字,可参考《Unix系统环境高级编程》第二版8.10节:exec函数集),所以在新进程中这些侦听描述字可以接受新的连接请求。新进程获取环境变量中的描述字的实现在nginx.c的函数ngx_add_inherited_sockets中。读者有兴趣的话可自行查看。

 

4. 从容关闭老进程

了解Nginx如何从容关闭老进程的关键在于查看Nginx处理信号QUIT的代码。该部分代码在文件ngx_process.c的函数ngx_signal_hander()中,代码如下所示。

switch (ngx_process) {

    case
 NGX_PROCESS_MASTER:
    case
 NGX_PROCESS_SINGLE:
        switch (
signo) {

        case ngx_signal_value(NGX_SHUTDOWN_SIGNAL):
            ngx_quit = 1;
            action = ", shutting down";
            break;


        case ngx_signal_value(NGX_TERMINATE_SIGNAL):
        case
 SIGINT:
            ngx_terminate = 1;
            action = ", exiting";
            break;

        case
 ngx_signal_value(NGX_NOACCEPT_SIGNAL):
            if (
ngx_daemonized) {
                ngx_noaccept = 1;
                action = ", stop accepting connections";
            }
            break;

        case
 ngx_signal_value(NGX_RECONFIGURE_SIGNAL):
            ngx_reconfigure = 1;
            action = ", reconfiguring";
            break;

        case
 ngx_signal_value(NGX_REOPEN_SIGNAL):
            ngx_reopen = 1;
            action = ", reopening logs";
            break;


        case ngx_signal_value(NGX_CHANGEBIN_SIGNAL):

/* getppid() > 1 || ngx_new_binary > 0表明目前正在进行二进制更新处理的阶段,所以直接忽视该信号*/
            if (getppid() > 1 || ngx_new_binary > 0) {
                action = ", ignoring";
                ignore = 1;
                break;
            }


            ngx_change_binary = 1;
            action = ", changing binary";
            break;

        case
 SIGALRM:
            ngx_sigalrm = 1;
            break;

        case
 SIGIO:
            ngx_sigio = 1;
            break;

        case
 SIGCHLD:
            ngx_reap = 1;
            break;
        }

        break;

    case
 NGX_PROCESS_WORKER:
    case
 NGX_PROCESS_HELPER:
        switch (
signo) {

        case
 ngx_signal_value(NGX_NOACCEPT_SIGNAL):
            if (!
ngx_daemonized) {
                break;
            }

            ngx_debug_quit = 1;
        case ngx_signal_value(NGX_SHUTDOWN_SIGNAL):
            ngx_quit = 1;
            action = ", shutting down";
            break;


        case ngx_signal_value(NGX_TERMINATE_SIGNAL):
        case
 SIGINT:
            ngx_terminate = 1;
            action = ", exiting";
            break;

        case
 ngx_signal_value(NGX_REOPEN_SIGNAL):
            ngx_reopen = 1;
            action = ", reopening logs";
            break;

        case
 ngx_signal_value(NGX_RECONFIGURE_SIGNAL):
        case
 ngx_signal_value(NGX_CHANGEBIN_SIGNAL):
        case
 SIGIO:
            action = ", ignoring";
            break;
        }

        break;
    }

    由以上红色部分代码可以看出,当进程为master类型时,将标志位ngx_quit设置为1。当进程为worker类型时,也将标志位ngx_quit设置为1。不过,QUIT信号的发送者不同,master进程收到的的QUIT信号是由Nginx管理员调用kill命令发送的,而worker进程收到的QUIT信号是由master进程发送的(读者将会在下文看到)。而且,masterworker进程对这个标志的处理是不同的。下面分别来看masterworker进程如何处理ngx_quit标志位。

 

4.1 从容退出master进程

       文件ngx_process_cycle.c中的函数ngx_master_process_cyclemaster进程的事件处理循环。这个函数对ngx_quit标志的处理如下:

 

       /*live0时表示所有子进程已经退出,为1时表示还有子进程存在*/

        if (!live && (ngx_terminate || ngx_quit)) {
            ngx_master_process_exit(cycle);
        }

        if (
ngx_quit) {
            ngx_signal_worker_processes(cycle,
                               ngx_signal_value(NGX_SHUTDOWN_SIGNAL));

            ls = cycle->listening.elts;
            for (
= 0; n < cycle->listening.nelts; n++) {
                if (
ngx_close_socket(ls[n].fd) == -1) {
                    ngx_log_error(NGX_LOG_EMERG, cycle->log,                                                   ngx_socket_errno,
                                  ngx_close_socket_n " %V failed",
                                  &
ls[n].addr_text);
                }
            }

            cycle->listening.nelts = 0;

            continue;
        }

       从以上代码片段可以看出,master对该标志位的处理有两个条件判断语句,第一个是当master进程的所有子进程退出,且ngx_quit标志值为1时,调用函数ngx_master_process_exit。该函数的执行流程为:

1)删除该进程对应的pid文件。

2)执行所有ngx_moduleexit_master回调函数。

3)关闭master进程所有侦听socket

4)析构内存池。

5)调用exit(0)退出进程。

第二个条件判断语句功能是:当ngx_quit标志值为1时,master进程调用函数ngx_signal_worker_processes通知所有worker进程退出。该函数的实现代码在文件ngx_process_cycle.c中。主要功能是给所有worker进程发送QUIT信号。有兴趣的读者可以查看源码。然后关闭master所有侦听socket

按时间顺序来说,第二个条件判断会先执行,当master进程的所有子进程结束后,第一个条件判断会执行,退出master进程,完成从容地退出。这样看来,只要所有子进程退出,master进程就退出了。接下来看看Nginxworker进程如何从容地退出。

 

4.2 从容退出worker进程

       文件ngx_process_cycle.c中的函数ngx_worker_process_cycleworker进程的事件处理循环。这个函数对ngx_quit标志的处理如下:

        if (ngx_quit) {
            ngx_quit = 0;
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
                          "gracefully shutting down");
            ngx_setproctitle("worker process is shutting down");

            if (!
ngx_exiting) {
                ngx_close_listening_sockets(cycle);
                ngx_exiting = 1;
            }
        }

       有以上代码可以看出,如果ngx_quit的值为1worker进程关闭所有侦听socket,并将ngx_exiting设置为1ngx_exiting标志位的处理也在函数ngx_worker_process_cycle中,代码如下所示:

 

if (ngx_exiting) {

    c = cycle->connections;

    for (= 0; i < cycle->connection_n; i++) {

    /* THREAD: lock */

if (c[i].fd != -&& c[i].idle) {
            c[i].close = 1;
            c[i].read->handler(c[i].read);
        }
    }

    if (
ngx_event_timer_rbtree.root==ngx_event_timer_rbtree.sentinel)
    {

         ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");

         ngx_worker_process_exit(cycle);
    }

}

       从以上代码可以看出,当ngx_exiting值为1时,首先关闭所有空闲连接。然后判断如果没有定时器存在时,调用函数ngx_worker_process_exit退出worker进程。没有定时器存在意味着worker已经处理完了所有事件,可以退出了。函数ngx_worker_process_exit在文件ngx_process_cycle.c中,其处理流程为:

1)调用所有ngx_moduleexit_process回调函数。

2)析构内存池。

3)调用exit(0)退出worker进程。

这样看来,当worker处理完所有事件之后就直接退出进程了。

原创粉丝点击