leevis.com
leevis.com copied to clipboard
nginx 动态升级
概述
ngx提供动态升级,在不阶段提供服务的情况下更新可执行文件。 在Linux不支持reuseport的时期,老的nginx进程绑定了端口,新的进程再绑定监听会失败。 如果把老的进程停止,再启动新的进程。会有一段时间端口是不通的。 虽然Linux新的内核支持了reuseport,在开启了以后。老的进程不停止,新的进程也可以绑定监听。这样虽然可以解决端口不通的问题,老的进程如果先关闭监听,等所有请求结束再推出,基本可以避免上述问题了。 但是,nginx为了跨平台,自己实现了动态升级。
来看下nginx动态升级的流程:
- 把./sbin/nginx 改名。再把新的nginx可执行文件放到./sbin/ 目录下。
- 向nginx的master进程发送SIGUSR2信号。
- 向旧的nginx的master进程发送SIGWINCH信号。
- 此时应检查新的进程是否可以正确的处理请求。如果可以则执行6步。如果不可以则执行第5步。
- 向旧的master进程发送SIGHUP唤醒老的worker进程起来处理请求。
- 向旧的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进程在处理请求。动态升级成功。