leevis.com icon indicating copy to clipboard operation
leevis.com copied to clipboard

nginx 动态升级

Open vislee opened this issue 7 years ago • 0 comments

概述

ngx提供动态升级,在不阶段提供服务的情况下更新可执行文件。 在Linux不支持reuseport的时期,老的nginx进程绑定了端口,新的进程再绑定监听会失败。 如果把老的进程停止,再启动新的进程。会有一段时间端口是不通的。 虽然Linux新的内核支持了reuseport,在开启了以后。老的进程不停止,新的进程也可以绑定监听。这样虽然可以解决端口不通的问题,老的进程如果先关闭监听,等所有请求结束再推出,基本可以避免上述问题了。 但是,nginx为了跨平台,自己实现了动态升级。

来看下nginx动态升级的流程:

  1. 把./sbin/nginx 改名。再把新的nginx可执行文件放到./sbin/ 目录下。
  2. 向nginx的master进程发送SIGUSR2信号。
  3. 向旧的nginx的master进程发送SIGWINCH信号。
  4. 此时应检查新的进程是否可以正确的处理请求。如果可以则执行6步。如果不可以则执行第5步。
  5. 向旧的master进程发送SIGHUP唤醒老的worker进程起来处理请求。
  6. 向旧的nginx的master进程发送SIGQUIT信号。

源码分析

动态升级是通过向进程发送信号驱动的,如果不熟悉ngx的信号处理,先看上一篇。

处理SIGUSR2信号

  • 向master进程发送SIGUSR2信号前,ngx的master进程阻塞在ngx_master_process_cycle函数的sigsuspend(&set)调用上,接收到信号时会被信号唤醒执行信号处理函数ngx_signal_handler,把ngx_change_binary改为1,action改为 ", changing binary"。
        case ngx_signal_value(NGX_CHANGEBIN_SIGNAL):
            if (getppid() > 1 || ngx_new_binary > 0) {

                /*
                 * Ignore the signal in the new binary if its parent is
                 * not the init process, i.e. the old binary's process
                 * is still running.  Or ignore the signal in the old binary's
                 * process if the new binary's process is already running.
                 */

                action = ", ignoring";
                ignore = 1;
                break;
            }

            ngx_change_binary = 1;
            action = ", changing binary";
            break;
  • 信号处理函数执行完毕后,master进程从ngx_master_process_cycle函数的sigsuspend(&set)调用上接着开始向下执行,ngx_change_binary变量被信号处理函数改为1,所以ngx_exec_new_binary函数被调用执行。
        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);
        }
  • 在ngx_exec_new_binary函数中,根据启动时命令行参数和配置文件环境变量,调用ngx_execute函数启动一个分离进程去执行新的可以执行文件。分离进程(对应分离线程理解)就是不需要父进程管理和收尸。
typedef struct {
    char         *path;
    char         *name;
    char *const  *argv;
    char *const  *envp;
} ngx_exec_ctx_t;

ngx_pid_t
ngx_exec_new_binary(ngx_cycle_t *cycle, char *const *argv)
{
    char             **env, *var;
    u_char            *p;
    ngx_uint_t         i, n;
    ngx_pid_t          pid;
    ngx_exec_ctx_t     ctx;
    ngx_core_conf_t   *ccf;
    ngx_listening_t   *ls;

    ngx_memzero(&ctx, sizeof(ngx_exec_ctx_t));

    ctx.path = argv[0];
    ctx.name = "new binary process";
    ctx.argv = argv;

    n = 2;
    // 环境变量,默认会清空从父进程继承的环境变量
    // 通过env这个指令,保留从父进程继承的系统环境变量,修改环境变量的值,新加环境变量。
    env = ngx_set_environment(cycle, &n);
    if (env == NULL) {
        return NGX_INVALID_PID;
    }

    var = ngx_alloc(sizeof(NGINX_VAR)
                    + cycle->listening.nelts * (NGX_INT32_LEN + 1) + 2,
                    cycle->log);
    if (var == NULL) {
        ngx_free(env);
        return NGX_INVALID_PID;
    }

    p = ngx_cpymem(var, NGINX_VAR "=", sizeof(NGINX_VAR));

    ls = cycle->listening.elts;
    for (i = 0; i < cycle->listening.nelts; i++) {
        p = ngx_sprintf(p, "%ud;", ls[i].fd);
    }

    *p = '\0';

    env[n++] = var;

#if (NGX_SETPROCTITLE_USES_ENV)

    /* allocate the spare 300 bytes for the new binary process title */

    env[n++] = "SPARE=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
               "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
               "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
               "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
               "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

#endif

    env[n] = NULL;

#if (NGX_DEBUG)
    {
    char  **e;
    for (e = env; *e; e++) {
        ngx_log_debug1(NGX_LOG_DEBUG_CORE, cycle->log, 0, "env: %s", *e);
    }
    }
#endif

    ctx.envp = (char *const *) env;

    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);

    // 修改nginx进程pid文件名
    if (ngx_rename_file(ccf->pid.data, ccf->oldpid.data) == NGX_FILE_ERROR) {
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      ngx_rename_file_n " %s to %s failed "
                      "before executing new binary process \"%s\"",
                      ccf->pid.data, ccf->oldpid.data, argv[0]);

        ngx_free(env);
        ngx_free(var);

        return NGX_INVALID_PID;
    }

    // fork一个子进程,调用execve执行可执行文件。
    pid = ngx_execute(cycle, &ctx);

    if (pid == NGX_INVALID_PID) {
        if (ngx_rename_file(ccf->oldpid.data, ccf->pid.data)
            == NGX_FILE_ERROR)
        {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          ngx_rename_file_n " %s back to %s failed after "
                          "an attempt to execute new binary process \"%s\"",
                          ccf->oldpid.data, ccf->pid.data, argv[0]);
        }
    }

    ngx_free(env);
    ngx_free(var);

    return pid;
}

ngx_pid_t
ngx_execute(ngx_cycle_t *cycle, ngx_exec_ctx_t *ctx)
{
    return ngx_spawn_process(cycle, ngx_execute_proc, ctx, ctx->name,
                             NGX_PROCESS_DETACHED);
}


static void
ngx_execute_proc(ngx_cycle_t *cycle, void *data)
{
    ngx_exec_ctx_t  *ctx = data;

    if (execve(ctx->path, ctx->argv, ctx->envp) == -1) {
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "execve() failed while executing %s \"%s\"",
                      ctx->name, ctx->path);
    }

    exit(1);
}

  • 新的nginx从main函数开始执行,调用ngx_add_inherited_sockets函数处理父进程继承的监听文件描述符。添加到cycle->listening数组中。在ngx_add_inherited_sockets函数中还会调用ngx_set_inherited_sockets从文件描述符获取socket的一些信息,例如绑定监听的地址和端口等。

  • 新的nginx接着会继续执行,调用ngx_init_cycle函数解析配置文件。调用fork后,子进程又调用了execve后,子进程的代码段数据段堆栈全部被新的程序覆盖,继承的打开的文件描述符因设置了FD_CLOEXEC也在调用execve后关闭,仅剩下监听的描述符被复制到子进程中,因内存段被覆盖新的文件描述符也丢了,而ngx是通过环境变量传递过来的。在该函数中,把继承的监听描述符,不用的错误的都需要调用close关闭(关闭也只是关闭了一个引用)。

  • 配置文件解析完毕后,打开文件,打开监听。调用ngx_master_process_cycle函数fork工作进程。

至此,新老进程同时处理请求。

处理SIGWINCH信号

  • 老的master进程还是阻塞在ngx_master_process_cycle函数的sigsuspend(&set);调用上,等待信号。接收到WINCH信号后,调用该信号的处理函数,如下。如果是master进程且是后台进程则把ngx_noaccept设置为1.
    switch (ngx_process) {

    case NGX_PROCESS_MASTER:
    case NGX_PROCESS_SINGLE:
        switch (signo) {

        ......

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

......
  • 老的master进程从sigsuspend函数接着继续执行。ngx_noaccept为真,调用 ngx_signal_worker_processes(cycle, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));; 向所有未分离子进程发送NGX_SHUTDOWN_SIGNAL信号。新的master进程也是老的master进程的子进程,但是是分离进程(分离进程是ngx自身的设计,不是linux和glib层面的)。
static void
ngx_signal_worker_processes(ngx_cycle_t *cycle, int signo)
{
    ngx_int_t      i;
    ngx_err_t      err;
    ngx_channel_t  ch;

    ngx_memzero(&ch, sizeof(ngx_channel_t));

#if (NGX_BROKEN_SCM_RIGHTS)

    ch.command = 0;

#else

    switch (signo) {

    case ngx_signal_value(NGX_SHUTDOWN_SIGNAL):
        ch.command = NGX_CMD_QUIT;
        break;

    case ngx_signal_value(NGX_TERMINATE_SIGNAL):
        ch.command = NGX_CMD_TERMINATE;
        break;

    case ngx_signal_value(NGX_REOPEN_SIGNAL):
        ch.command = NGX_CMD_REOPEN;
        break;

    default:
        ch.command = 0;
    }

#endif

    ch.fd = -1;


    for (i = 0; i < ngx_last_process; i++) {

        ngx_log_debug7(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "child: %i %P e:%d t:%d d:%d r:%d j:%d",
                       i,
                       ngx_processes[i].pid,
                       ngx_processes[i].exiting,
                       ngx_processes[i].exited,
                       ngx_processes[i].detached,
                       ngx_processes[i].respawn,
                       ngx_processes[i].just_spawn);

        // detached 分离进程,回顾SIGUSR2信号的处理流程。
        if (ngx_processes[i].detached || ngx_processes[i].pid == -1) {
            continue;
        }

        if (ngx_processes[i].just_spawn) {
            ngx_processes[i].just_spawn = 0;
            continue;
        }

        if (ngx_processes[i].exiting
            && signo == ngx_signal_value(NGX_SHUTDOWN_SIGNAL))
        {
            continue;
        }

        // 先通过管道发送命令
        if (ch.command) {
            if (ngx_write_channel(ngx_processes[i].channel[0],
                                  &ch, sizeof(ngx_channel_t), cycle->log)
                == NGX_OK)
            {
                if (signo != ngx_signal_value(NGX_REOPEN_SIGNAL)) {
                    ngx_processes[i].exiting = 1;
                }

                continue;
            }
        }

        ngx_log_debug2(NGX_LOG_DEBUG_CORE, cycle->log, 0,
                       "kill (%P, %d)", ngx_processes[i].pid, signo);

        // 管道发送指令失败,发送信号。
        if (kill(ngx_processes[i].pid, signo) == -1) {
            err = ngx_errno;
            ngx_log_error(NGX_LOG_ALERT, cycle->log, err,
                          "kill(%P, %d) failed", ngx_processes[i].pid, signo);

            if (err == NGX_ESRCH) {
                ngx_processes[i].exited = 1;
                ngx_processes[i].exiting = 0;
                ngx_reap = 1;
            }

            continue;
        }

        if (signo != ngx_signal_value(NGX_REOPEN_SIGNAL)) {
            ngx_processes[i].exiting = 1;
        }
    }
}
  • 老的worker进程在启动时调用ngx_worker_process_init函数,该函数调用ngx_add_channel_event添加了管道可读回调ngx_channel_handler。在收到master进程发送的NGX_CMD_QUIT指令后,worker进程的ngx_quit置为1。

  • 老的worker进程在循环里,根据ngx_quit变量为真,调用了关闭监听函数。并最终调用ngx_worker_process_exit函数退出。

    for ( ;; ) {
        if (ngx_exiting) {
        // worker进程退出中
            if (ngx_event_no_timers_left() == NGX_OK) {
                ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
                ngx_worker_process_exit(cycle);
            }
        }

        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "worker cycle");

        ngx_process_events_and_timers(cycle);

        if (ngx_terminate) {
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
            ngx_worker_process_exit(cycle);
        }

        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_exiting = 1;
                // 有长链接的worker进程关不掉,如果设置了shutdown_timeout。
                // 会注册一个定时器,超时强制关闭。
                ngx_set_shutdown_timer(cycle);
                // 关闭监听,不再接收新的请求。
                ngx_close_listening_sockets(cycle);
                // 关闭空闲的链接
                ngx_close_idle_connections(cycle);
            }
        }

        if (ngx_reopen) {
            ngx_reopen = 0;
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");
            ngx_reopen_files(cycle, -1);
        }
    }

自此,只有新的进程在启动接收链接处理请求。老的进程只剩下master进程,worker进程已经全部退出。 如果新的进程测试正常,则可以让老的master进程也退出了。

处理SIGQUIT信号

  • 在向master发送该信号前,老的worker进程退出会向其父进程发送了SIGCHLD信号。老的master进程受到该信号,把ngx_reap置为1,并调用了ngx_process_get_status函数获取子进程退出状态,并最终销毁子进程。 父进程(即:老的master)会收调用ngx_reap_children函数检测子进程已经全部退出后,置live为0。

  • 老的master接收到SIGQUIT信号后,ngx_quit被置为1。并最终调用ngx_master_process_exit函数退出。

        if (ngx_reap) {
            ngx_reap = 0;
            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "reap children");

            live = ngx_reap_children(cycle);
        }

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

自此,老的master和worker进程都退出。只剩下新的master和worker进程在处理请求。动态升级成功。

vislee avatar Sep 18 '17 08:09 vislee