作为 Linux 平台上优秀的 Web Server,Nginx 也实现了 Linux 服务器的标配管理操作: 重新打开日志文件、终止运行 (优雅地或者强制地) 和 重新读取配置文件并 (优雅地) 重启。 此外,Nginx 还实现了二进制的热切换,有了这个功能,升级 Nginx 时就不需要中断对外 服务了。

这些功能都是通过信号机制 (man 7 signal) 实现的,本篇对上面提到的功能在 Nginx 是如何实现的进行分析。为简化分析,只对 Linux 平台上的,启用了 master 守护进 程模式下的 Nginx 代码流程。

用法

NginxCommandLine

有两种方式可以向正在运行的 Nginx 进程的发送信号以完全管理操作:使用 Nginx 进程 的 -s 选项 或者 直接使用系统命令 kill 发送信号给 master 进程。使用 -s 选项时,nginx 会自动查找运行中的 master 进程 ID (master 进程负责接收并处理 信号,同时根据不同的信号,对所有工作进程完成不同的管理操作):

/path/to/nginx -s reload
kill -HUP $(cat /path/to/pidfile)

-s 选项支持的参数有:stop, quit, reopen, reload,在 Linux 平台上这些 参数和 Nginx 接受的信息对应关系如下:

signal              function                    -s argument
------------------------------------------------------------
TERM, INT       Quick shutdown                      stop
QUIT            Graceful shutdown                   quit
HUP             Configuration reload                reload
USR1            Reopen the log files                reopen
USR2            Upgrade Executable on the fly
WINCH           Gracefully shutdown the worker
                processes
KILL            Halts a stubborn process

Nginx 实现的在线升级机制稍微复杂一点,需要多个步骤才能完成:

  1. First, replace old binary with a new one, then send USR2 signal to the master process. It renames its .pid file to .oldbin, then executes a new binary, which in turn starts a new master process and new worker processes;

  2. At this point, two instances of nginx are running, handling the incoming requests together. To phase the old instance out, you have to send WINCH signal to the old master process, and its worker processes will start to gracefully shutdown;

  3. After some time, old worker processes all quit and only the new worker processes are handling the incoming requests;

  4. If an update is successful and you want to keep the new instance, send QUIT signal to the old master process to leave only new server running;

  5. At this point you can still revert to the old instance because it hasn't closed its listen socket yet, by following these steps:

    • Send HUP signal to the old master process - it will start the worker processes without reloading a configuration file;

    • Send QUIT signal to the new master process to gracefully shutdown its worker processes;

    • Send TERM signal to the new master process to force it quit;

    • If for some reason new worker processes do not quit, send KILL signal to them.

信号注册

More on Linux signals

上面提到的信号 (KILL 信号除外,Linux 不允许进程对 KILL 信号的行为进行修改) 在 Nginx 代码中被重新命名,对应关系如下:

#define NGX_SHUTDOWN_SIGNAL     QUIT
#define NGX_TERMINATE_SIGNAL    TERM
#define NGX_NOACCEPT_SIGNAL     WINCH
#define NGX_RECONFIGURE_SIGNAL  HUP
#define NGX_REOPEN_SIGNAL       USR1
#define NGX_CHANGEBIN_SIGNAL    USR2

在 Nginx 的 main 函数读取并分析完配置文件和其它初始化工作完成后,在启动工作进 程前,调用 ngx_init_signals 函数注册信号及其处理函数:

int main()
{
    ...
    ngx_debug_init();
    ...
    ngx_sterror_init();
    ...
    ngx_time_init();
    ...
    ngx_regex_init();
    ...
    ngx_ssl_init();
    ...
    ngx_process_options();
    ...
    ngx_init_cycle();
    ...
    ngx_init_signals();
    ...
    ngx_create_pidfile();
    ...
    if (ngx_process == NGX_PROCESS_SINGLE) {
        ngx_single_process_cycle(cycle);

    } else {
        ngx_master_process_cycle(cycle);
    }

    return 0;
}

ngx_init_signals 函数调用 sigaction 对每一个 Nginx 关心的信号和其处理函数告 知操作系统。Nginx 关心的信号定义在 signals (os/unit/ngx_process.c) 中:

ngx_signal_t signals[] = {
    { ngx_signal_value(NGX_RECONFIGURE_SIGNAL),
      "SIG" ngx_value(NGX_RECONFIGURE_SIGNAL),
      "reload",
      ngx_signal_handler },

    { ngx_signal_value(NGX_REOPEN_SIGNAL),
      "SIG" ngx_value(NGX_REOPEN_SIGNAL),
      "reopen",
      ngx_signal_handler },
    ...
}

signals 结构体变量可知,除了 SIGPIPESIGSYS 信号被忽略外,其它信号, SIGQUIT, SIGTERM, SIGWINCH, SINHUP, SIGUSR1, SIGUSR2, SIGALRM, SIGINT, SIGIOSIGCHLD 的信号处理函数均为 ngx_signal_handler

其中,SIGINTSIGTERM 作用一样,用于强制 Nginx 进程退出;master 进程接 到强制退出信号时,会向所有工作进程发送强制退出信号,如果工作进程未能及时退出, master 使用计时器重复发送强制信号,计时器触发时会发送 SIGALRM 信号;SIGIO 信号被 Nginx 显式忽略;SIGCHLD 信号告诉 master 进程有工作进程退出,需 要完成资源回收或者重启工作进程的工作。

ngx_signal_handler 函数的代码比较简单,它根据收到的信号设置不同的开关变量:

SIGQUIT             ngx_quit = 1
SIGTERM, SIGINT     ngx_terminate = 1
SIGWINCH            ngx_noaccept = 1
SIGHUP              ngx_reconfigure = 1
SIGUSR1             ngx_reopen = 1
SIGUSR2             ngx_change_binary = 1
SIGALARM            ngx_sigalrm = 1
SIGIO               ngx_sigio = 1
SIGCHLD             ngx_reap = 1

master 进程和工作进程都会收到上述注册的信号 (worker 进程是 master 进程的子进程,它会继承父进程的信号设置),但是工作进程只对 SIGWINCH, SIGQUITSIGTERMSIGINTSIGHUP 进行响应。同时,上面提到过,worker 进程退出时,操作系统会向其父进程发送 SIGCHLD 信号。下面的分析只针对 master 进程。

master 进程的入口函数 ngx_master_process_cycle 中,会调用 sigpromask 函数将上述信号添加到进程的信号阻塞集中,然后在下面的 for 循环中使用 sigsuspend 函数阻塞整个进程直到收到某个信号,这种方式的作用在 man 2 sigsuspend 中有详细的解释:

NOTES Normally, sigsuspend() is used in conjunction with sigprocmask(2) in order to prevent delivery of a signal during the execution of a critical code section. The caller first blocks the signals with sigprocmask(2). When the critical code has completed, the caller then waits for the signals by calling sigsupend() with the signal mask that was returned by sigprocmask(2).

接下来对 Nginx 是如何针对各种信号完成本文最开始提到的种种功能进行分析。

重新打开日志文件

Nginx 收到 SIGUSR1 信号后,信号处理函数将 ngx_reopen 全局变量置为 1master 进程在 ngx_master_process_cyclefor 循环中检查此变量值并作出相 应处理:

/* ngx_master_process_cycle */
for ( ;; ) {
    ...
    if (ngx_reopen) {
        ngx_reopen = 0;
        ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");
        ngx_reopen_files(cycle, ccf->user);
        ngx_signal_worker_processes(cycle,
                                    ngx_signal_value(NGX_REOPEN_SIGNAL));
    }
    ...
}

对上述代码的补充说明:

  • Nginx 运行过程中可以多次接收 SIGUSR1 信号,每次执行完 SIGUSR1 信号对应的逻辑 后,将 ngx_reopen0

  • ngx_reopen_files 函数将 cycle->open_files 中打开的文件 (全部都是日志文件) 关闭并重新打开;

  • SIGUSR1 信号转发给所有工作进程;

重新读取配轩文件

Nginx 收到 SIGHUP 信号后,信号处理函数将 ngx_reconfigure 全局变量置为 1master 进程在 ngx_master_process_cyclefor 循环中检查此变量值并作出相 应处理:

/* ngx_master_process_cycle */
for ( ;; ) {
    ...
    if (ngx_reconfigure) {
        ngx_reconfigure = 0;
        ...
        cycle = ngx_init_cycle(cycle);
        ...
        ngx_cycle = cycle;
        ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,
                                               ngx_core_module);
        ngx_start_worker_processes(cycle, ccf->worker_processes,
                                   NGX_PROCESS_JUST_RESPAWN);
        ngx_start_cache_manager_processes(cycle, 1);

        /* allow new processes to start */
        ngx_msleep(100);

        live = 1;
        ngx_signal_worker_processes(cycle,
                                    ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
    }
    ...
}

对上述代码的补充说明:

  • 配置文件更新时,需要重新读取配置并初始化 (共享内存、日志文件等),然后使用新的 配置 (cycle) 启动新的工作进程 (它们就拥有了新的 cycle 拷贝);

  • 随后通过发送 NGX_SHUTDOWN_SIGNAL 信号,让老工作进程优雅退出;

  • 使用 NGX_PROCESS_JUST_RESPAWN 标识新进程,这样 ngx_signal_worker_processes 函数就不会将 NGX_SHUTDOWN_SIGNAL 发送给它们;

  • 为了避免因为新进程还未完全启动完成,老的工作进程已经退出,造成这期间无 worker 进程可用,所以,执行 ngx_msleep(100) 以便尽量确保新进程可以启动完成;

退出进程

Nginx 进程退出分为两种:

  • master 进程接到 SIGQUIT 信号时,将此信号转发给工作进程。工作进程随后关闭 监听端口以便不再接收新的连接请求,并闭空闲连接,等待活跃连接全部正常结速后,调 用 ngx_worker_process_exit 退出。而 master 进程在所有工作进程都退出后,调用 ngx_master_process_exit 函数退出;

  • master 进程接收到 SIGTERM 或者 SIGINT 信号时,将信号转发给工作进程。工 作进程直接调用 ngx_worker_process_exit 函数退出。master 进程在所有工作进程 都退出后,调用 ngx_master_process_exit 函数退出。另外,如果工作进程未能正常退 出,master 进程会等待 1 秒后,发送 SIGKILL 信号强制终止工作进程。

实现上述逻辑的代码如下:

/* ngx_master_process_cycle */
for ( ;; ) {
    if (delay) {
        if (ngx_sigalrm) {
            sigio = 0;
            delay *= 2;
            ngx_sigalrm = 0;
        }

        ...
        setitimer(ITIMER_REAL, &itv, NULL);
        ...
    }
    ...
    if (ngx_reap) {
        ngx_reap = 0;
        ...
    }

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

    if (ngx_terminate) {
        if (delay == 0) {
            delay = 50;
        }

        if (sigio) {
            sigio--;
            continue;
        }

        sigio = ccf->worker_processes + 2 /* cache processes */;

        if (delay > 1000) {
            ngx_signal_worker_processes(cycle, SIGKILL);
        } else {
            ngx_signal_worker_processes(cycle,
                                    ngx_signal_value(NGX_TERMINATE_SIGNAL));
        }

        continue;
    }

    if (ngx_quit) {
        ngx_signal_worker_processes(cycle,
                                    ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
        ls = cycle->listening.elts;

        for (n = 0; n < cycle->listening.nelts; n++) {
            ngx_close_socket(ls[n].fd);
            ...
        }

        cycle->listening.nelts = 0;

        continue;
    }
    ...
}

对上述代码的补充说明:

  • master 进程接收到 SIGQUIT, SIGTERM 或者 SIGINT 信号后,不再处理除了 SIGCHLD 外的其它信号 (注意 continue 语句和 ngx_terminate, ngx_quit 变量 的值不会再被复位为 0);

  • SIGQUIT 告知 Nginx 优雅退出。但是如果工作进程因为某些原因未能正常退出时,可 再使用 SIGTERMSIGINT 信号强制退出;

  • SIGTERM 或者 SIGINT 作为 Nginx 必须有效的退出手段,它会设置等待时间 (delay):超时前,使用 SIGTERM 尝试使工作进程退出;超时后,使用 SIGKILL 暴 力终止异常工作进程。超时通过定时器发送 SIGALRM 信号实现。

  • 在非优雅退出操作设置超时后,由于 sigio 变量的控制,不会重复发送 NGX_SHUTDOWN_SIGNAL 给工作进程。但是目前代码貌似存在问题:第一次设置定时器后, 如果收到非 SIGALRM 外的其它信号,会重复设置定时器 (delay !=0 && ngx_sigalarm == 0 时),造成 delay 增加太快? 修改为下面的代码 或许对定时器的控制更精确一点:

    timer = 0;
    
    for ( ;; ) {
        if (delay) {
            if (ngx_sigalrm) {
                ...
                timer = 0;
            }
    
            if (!timer) {
                timer = 1;
                ...
                setitimer(...);
            }
        }
    
        ...
    }
    

工作进程监控

master 进程除了上述的功能外,还要对工作进程进行监控。如果工作进程因为某些原因 (模块 BUG、被人为意外中止等) 退出的话,master 需要再次创建新的工作进程。

工作进程退出时,操作系统会向其父进程,即 master 进程发送 SIGCHLD 信号,这个 状态由 ngx_reap 变量标识:

/* ngx_master_process_cycle */
for ( ;; ) {
    ...
    if (ngx_reap) {
        ngx_reap = 0;

        live = ngx_reap_children(cycle);
    }
    ...
}

/* ngx_reap_children */
for (i = 0; i < ngx_last_process; i++) {
    ...
    if (ngx_processes[i].exited) {

        if (!ngx_processes[i].detached) {
            ...
            for (n = 0; n < ngx_last_process; n++) {
                if (ngx_processes[n].exited
                    || ngx_processes[n].pid == -1
                    || ngx_processes[n].channel[0] == -1)
                {
                    continue;
                }

                ngx_write_channel(ngx_processes[n].channel[0],
                                  &ch, sizeof(ngx_channel_t), cycle->log);
            }
        }

        if (ngx_process[i].respawn
            && !ngx_processes[i].exiting
            && !ngx_terminate
            && !ngx_quit)
        {
            ngx_spawn_process(cycle, ngx_processes[i].proc,
                              ngx_processes[i].data, ngx_processes[i].name, i);
            ...
            ngx_pass_open_channel(cycle, &ch);

            live = 1;

            continue;
        }
        ...
    }
    ...
}

对上述代码的补充说明:

  • 进程是否退出在 ngx_signal_handler 函数中使用 waitpid 调用确定,并使用 exited 字段标识;

  • 使用 ngx_write_channel 函数将退出进程的信息通知其它正常工作进程,以便它们各 自更新工作进程 channel 信息;

  • 如果退出进程被设置为需要自动重启 (respawn),就调用 ngx_spawn_process 函数 使用它的原始信息启动该进程;

  • 随后调用 ngx_pass_open_channel 函数将进程启动的信息通知其它正常工作进程;

热切换

在热切换场景下, Nginx 使用新的二进制文件启动新的 master 进程;新 master 进 程会继承老 master 进程的文件句柄,共享内存等信息,这些继承数据在新 master 初始化过程中会被整理。

下面将按照第一节介绍的热切换过程,按步骤分析代码的执行流程。

第一步:老 master 进程接到 SIGUSR2 信号后,在其 for 循环中启动新的 master 进程。这一过程完成后,新老 master 进程同时运行,接收和处理请求:

/* ngx_master_process_cycle */
for ( ;; ) {
    ...
    if (ngx_change_binary) {
        ngx_change_binary = 0;
        ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv);
    }
    ...
}

对上述代码的补充说明:

  • 新的 master 进程依然是老 master 进程的子进程,但是老 master 进程不会再 像和它创建的工作进程那样和其维护管理通信 (NGX_PROCESS_DETACHED);

ngx_new_binary 存储新 master* 进程的进程 ID;

第二步:发送 SIGWINCH 给老 master 进程,使其停止接收新连接,并让其工作进程 优雅退出:

/* ngx_master_process_cycle */
for ( ;; ) {
    ...
    if (ngx_noaccept) {
        ngx_noaccept = 0;
        ngx_noaccepting = 1;
        ngx_signal_worker_processes(cycle,
                                    ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
    }
    ...
}

第三步:通过 SIGQUIT 信号,停止老 master 进程。

在上述步骤进程过程中:

  • 如果新 master 进程启动后又因为异常退出时,老 master 进程在 ngx_reap_children 函数中会重新启动老工作进程,以便整个 Nginx 能够继续工作:

    /* ngx_reap_children */
    if (ngx_processes[i].exited) {
        ...
        if (ngx_processes[i].pid == ngx_new_binary) {
            ...
            ngx_rename_file((char *) ccf->oldpid.data, (char *) ccf->pid.data);
    
            ngx_new_binary = 0;
            if (ngx_noaccepting) {
                ngx_restart = 1;
                ngx_noaccepting = 0;
            }
            ...
        }
        ...
    }
    
    /* ngx_master_process_cycle */
    if (ngx_restart) {
        ngx_restart = 0;
        ngx_start_worker_processes(cycle, ccf->worker_processes,
                                   NGX_PROCESS_RESPAWN);
        ngx_start_cache_manager_processes(cycle, 0);
        live = 1;
    }
    
  • 如果新的 master 进程正常工作,但是你依然想让其退出,依然使用老 master 进程 的话,需要使用下面的步骤:

    • 发送 SIGHUP 信号给老 master 进程,启动工作进程 (此时不会重读配置文件):

      if (ngx_reconfigure) {
          ngx_reconfigure = 0;
      
          if (ngx_new_binary) {
              ngx_start_worker_processes(cycle, ccf->worker_processes,
                                         NGX_PROCESS_RESPAWN);
              ngx_start_cache_manager_processes(cycle, 0);
              ngx_noaccepting = 0;
      
              continue;
          }
          ...
      }
      
    • 发送 SIGQUIT 信号给新 master 进程,让其正常退出。

Comments

不要轻轻地离开我,请留下点什么...

comments powered by Disqus

Published

Category

Nginx

Tags

Contact