作为 Linux 平台上优秀的 Web Server,Nginx 也实现了 Linux 服务器的标配管理操作: 重新打开日志文件、终止运行 (优雅地或者强制地) 和 重新读取配置文件并 (优雅地) 重启。 此外,Nginx 还实现了二进制的热切换,有了这个功能,升级 Nginx 时就不需要中断对外 服务了。
这些功能都是通过信号机制 (man 7 signal
) 实现的,本篇对上面提到的功能在 Nginx
是如何实现的进行分析。为简化分析,只对 Linux 平台上的,启用了 master 守护进
程模式下的 Nginx 代码流程。
用法
有两种方式可以向正在运行的 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 实现的在线升级机制稍微复杂一点,需要多个步骤才能完成:
-
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; -
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; -
After some time, old worker processes all quit and only the new worker processes are handling the incoming requests;
-
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; -
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.
-
信号注册
上面提到的信号 (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
结构体变量可知,除了 SIGPIPE
和 SIGSYS
信号被忽略外,其它信号,
SIGQUIT
, SIGTERM
, SIGWINCH
, SINHUP
, SIGUSR1
, SIGUSR2
, SIGALRM
,
SIGINT
, SIGIO
和 SIGCHLD
的信号处理函数均为 ngx_signal_handler
。
其中,SIGINT
和 SIGTERM
作用一样,用于强制 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
,
SIGQUIT
,SIGTERM
,SIGINT
和 SIGHUP
进行响应。同时,上面提到过,worker
进程退出时,操作系统会向其父进程发送 SIGCHLD
信号。下面的分析只针对 master
进程。
在 master 进程的入口函数 ngx_master_process_cycle
中,会调用 sigpromask
函数将上述信号添加到进程的信号阻塞集中,然后在下面的 for
循环中使用
sigsuspend
函数阻塞整个进程直到收到某个信号,这种方式的作用在 man 2
sigsuspend
中有详细的解释:
NOTES Normally,
sigsuspend()
is used in conjunction withsigprocmask(2)
in order to prevent delivery of a signal during the execution of a critical code section. The caller first blocks the signals withsigprocmask(2)
. When the critical code has completed, the caller then waits for the signals by callingsigsupend()
with the signal mask that was returned bysigprocmask(2)
.
接下来对 Nginx 是如何针对各种信号完成本文最开始提到的种种功能进行分析。
重新打开日志文件
Nginx 收到 SIGUSR1
信号后,信号处理函数将 ngx_reopen
全局变量置为 1
,
master 进程在 ngx_master_process_cycle
的 for
循环中检查此变量值并作出相
应处理:
/* 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_reopen
置0
; -
ngx_reopen_files
函数将cycle->open_files
中打开的文件 (全部都是日志文件) 关闭并重新打开; -
将
SIGUSR1
信号转发给所有工作进程;
重新读取配轩文件
Nginx 收到 SIGHUP
信号后,信号处理函数将 ngx_reconfigure
全局变量置为 1
,
master 进程在 ngx_master_process_cycle
的 for
循环中检查此变量值并作出相
应处理:
/* 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 优雅退出。但是如果工作进程因为某些原因未能正常退出时,可 再使用SIGTERM
或SIGINT
信号强制退出; -
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
不要轻轻地离开我,请留下点什么...