从nginx角度看服务器多进程模型设计(二)

在ngx_master_process_cycle中要处理众多的全局变量,正是通过一些信号处理函数设置这些变量,才会后面检测到一些事件的发生。我们来看看都有哪些预定义的事件,以及他们是如何被处理的。

多数的事件来自于nginx的用户,他们可能终止nginx,重启,重读配置等等,这些操作则主要依赖于信号,在nginx官网,给出了比较全面的控制命令介绍,用户可以通过他们来控制nginx的行为:
感兴趣的朋友可以自己去查看一下,这里我们只讨论三个主要的功能实现,包括重新加载配置,滚动日志,在线升级程序。

许多常用的操作,nginx通过一些定义的命令(“-s”后跟命令)来给用户使用,避免了用户去记住那些烦人的信号值。如
重新加载配置,“-s reload”,即向master进程发送-HUP信号
日志滚动, “-s reopen”,即向master进程发送-USR1信号
快速停止nginx, “-s stop”,即向master进程发送-TERM或者-INT信号
从容关闭nginx,“-s quit”,即向master进程发送-QUIT信号

先看重新加载配置:
操作起来很简单,两种方式,要么你针对master的进程id,发送SIGHUP信号,或者在终端使用./nginx -s reload。
实际上使用reload命令,也是向master进程发送SIGHUP信号,只不过是nginx帮我们去发这个信号罢了。机理呢也很简单,就是新起一个nginx,找到master进程的pid,然后“kill -HUP pid”。执行这个操作的nginx是一种特殊类型的进程,类型为NGX_PROCESS_SIGNALLER。

master进程收到SIGHUP信号时,master进程中的全局变量ngx_reconfigure会置1:
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;
            }

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

            // 解析初始化失败,后续的处理将继续使用旧的
            cycle = ngx_init_cycle(cycle);
            if (cycle == NULL) {
                cycle = (ngx_cycle_t *) ngx_cycle;
                continue;
            }

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

            // 以NGX_PROCESS_JUST_RESPAWN标记启动,这个类型的作用是什么呢?
            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;

            // 向其他worker进程发送QUIT信号,让他们“和谐”退出。关于这个函数的具体内容,很直白,大家可以自己看。
            ngx_signal_worker_processes(cycle,
                                        ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
        }

        /* 
         * master做的工作就是这些,我们看看worker进程内部都发生了什么变化,以及如何“和谐”退出。
         * 当worker进程收到master发来的信息,例如让自己“和谐”退出,信息中的command是NGX_CMD_QUIT,
         * 那么worker进程中的变量ngx_quit会置1。
         */
       if (ngx_quit) {
            ngx_quit = 0;
            
            // 改进程显示标题
            ngx_setproctitle("worker process is shutting down");

            if (!ngx_exiting) {
                // 关闭该进程里的listen fd,标记exiting,表示当前进程正在退出过程中。。
                ngx_close_listening_sockets(cycle);
                ngx_exiting = 1;
            }
        }

        /* 
         * 关闭listen fd的作用在最开始官方给出的解释已经很明确了,就是让要旧的进程不再接收新连接,
           * 等到旧连接都处理完之后,就可以退出了,也就是所谓“和谐”退出的含义了。在进入退出过程之后的后续处理,
           * 还有一些细节,如ngx_exiting。*/
        if (ngx_exiting) {
            
            c = cycle->connections;
            
            /*
             * 关闭所有的keepalive连接,keepalive有个idle标记,close被置1会,
                * 会在接下来的handler(即ngx_http_keepalive_handler)中检查到,从而将该连接关闭掉。
                */
            for (i = 0; i < cycle->connection_n; i++) {

                /* THREAD: lock */

                if (c[i].fd != -1 && c[i].idle) {
                    c[i].close = 1;
                    c[i].read->handler(c[i].read);
                }
            }
            
            /*
             * 最近的一次事件处理没有再新加timer或者原有的timer都已过期处理掉了,这样也就意味真该进程把它该干的都干完了,
             * 那现在就可以退出了。
             */
            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的最后,有一些ngx_exit_log相关的处理,关于这些,代码里给出了详细的注释。
                 * 一旦当前的pool被ngx_destroy_pool函数全部释放了,如果此时有个信号过来,在信号的处理函数ngx_signal_handler中,
                 * 还会有有一些记log的动作,那么此时如果还用原来的ngx_cycle->pool, 必然会出问题,
                 * 这里处理的意图也就为了解决这个问题。
                 */
                ngx_worker_process_exit(cycle);
            }
        }
好了,关于reload功能就先分析到这里,下面看在线升级程序的功能。
通常的操作是,将原来的nginx执行文件,备份一下,如"mv nginx nginx.bak", 之后将新编译的文件放到原sbin目录下。向master进程发送USR2信号,即可以通过命令kill -USR2 `cat /usr/local/nginx/logs/nginx.pid`,经过这个处理之后,新旧程序会并存,并且都可以对外服务。如果此时需要让旧程序退出,那么可以向旧master进程发送QUIT信号,即使用kill -QUIT `cat /usr/local/nginx/logs/nginx.pid.oldbin`,其中nginx.pid.oldbin是在处理USR2信号时生成的,其中含有旧master进程的pid。功能描述就是这样,下面看看程序实现。
        /*
         * 在信号处理函数,ngx_signal_handler中检测到USR2信号时,会将ngx_change_binary变量置1,
           * 然后会在master cycle中实际去处理。
           */
        if (ngx_change_binary) {
            ngx_change_binary = 0;
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "changing binary");
            // 函数ngx_exec_new_binary是核心
            ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv);
        }
关于ngx_exec_new_binary,其实说来也简单,它最终就是通过fork+execve的经典处理来实现的,不过在函数的开始部分有一些设置环境变量的处理,它有什么作用呢?设想一下,如果新的二进制文件在启动时必然要涉及bind端口的动作,而此时旧进程已经做了绑定,我们知道多个进程是不能同时绑定
同一个地址和端口的,所以新的进程要避免这种情况发生。nginx的做法是,将原来的绑定得到的listen fd保存在环境变量中,这样在新进程初始化的过程中,通过函数ngx_add_inherited_sockets中就可以获取listen fd来使用了,不必再次绑定。关于listen fd如何在环境变量中设置和获取,大家可以参考相关代码。

关于通过QUIT信号让旧程序退出的处理就不讨论了,在分析reload“和谐”退出的时候已经讲过。这里有个细节要说一下:
新程序的执行,是通过旧程序通过ngx_spawn_process函数来处理的,这时的启动类型是NGX_PROCESS_DETACHED,意思是这个进程跟原来的旧进程的worker是不同的,分离的。旧进程中很多处理都需要跳过这种类型或者做专门的处理,因为新的进程也会在旧进程的ngx_processes中占用一个位置。


接下来看日志滚动。那什么是日志滚动?日志滚动又叫日志切割,它的作用是为了避免生成超大日志文件,这类文件在读取分析时会非常慢,而且会有占满磁盘的可能,如果将文件每次以较小的尺寸产生,那么在读取和定期删除时会方便很多。nginx的日志滚动机制虽然不强,但是有值得借鉴的地方,我们来看:
首先要将有日志文件移走(或者重命名),然后向master进程发送USR1或者使用“-s reopen”命令。
跟其他的处理类似,master进程中有个名叫ngx_reopen的变量会在此时被置1,然后看处理:
if (ngx_reopen) {
            ngx_reopen = 0;
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");
            // master进程会刷空记录log的buffer,然后重新打开文件
            ngx_reopen_files(cycle, ccf->user);
            // 通知worker进程reopen日志文件,而worker进程也是调用ngx_reopen_files函数来处理的。
            ngx_signal_worker_processes(cycle,
                                        ngx_signal_value(NGX_REOPEN_SIGNAL));
        }
 
现在nginx的日志滚动已经被很多人改过,例如通过cronolog,syslog-ng等,不仅实现了更好的切割,还能做到实时上传到日志服务器上做处理。
好了,这里再扯点细节,在我们上面的分析中,特别是在upgrade和reopen操作时,我们分别做了mv可执行文件和日志文件的处理,可是这时候这两者都是“正在使用”的,会不会影响服务或者引起一些异常呢?如程序正在使用,移动失败或者写日志阻塞或者丢失。有这类疑问的同学,可以考虑下或者google下相关解释,这里就不说了。

最后呢,跟大家探讨下关于worker进程宕掉重启的机制。
大家多数应该知道,在linux中,通过fork产生的子进程在coredump时,会向父进程发送SIGCHLD信号,正是这种机制的存在,才使得重启子进程的工作变的容易。看nginx的如何做的吧。
在信号处理函数ngx_signal_handler中,会通过ngx_process_get_status,将master中ngx_processes里面将该宕掉进程置位exited。另外master进程的ngx_reap会被置1,然后在master cycle的处理中:
if (ngx_reap) {
            ngx_reap = 0;
            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "reap children");

            live = ngx_reap_children(cycle);
        }

关于ngx_reap_children细节,我们这里总体上说一下,留给大家自己去研究就可以了。
在这个函数会找到那个宕掉的进程(即exited == 1的),然后判断这个进程是原来的普通woker进程还是用来升级的新程序,两者的处理时不同的。
1. 普通worker进程
对于普通worker进程(即detached == 0),需要通告各个worker进程关于这个进程的信息,让他们保持同步,具体来说,就是通过NGX_CMD_CLOSE_CHANNEL来控制其他进程,让他们修改各自ngx_processer中的相应条目。通告完这些信息之后,master就需要重启那个宕掉的worker了,在ngx_processes中的位置跟宕掉之前是一样的,在创建完成之后,同样需要将信息广播,以同步各个worker。
2. 升级时的进程
首先pid文件会被还原回来(前面讲过的.oldbin文件,还记得吗?),然后修改一下信息,例如该进程位于ngx_processes的最后一个,那么last_process减1,意味着该进程不会被重启,应该这个进程你可以认为是升级后的程序运行时的master进程,它自己的worker由它负责重启,要是自己本身出了问题,那问题就大了,根本即没有重启的必要,去check代码吧,呵呵。

还有一些小地方没分析道,大家可以自己去看,相信有了上面这些做铺垫,剩下的就不难了。Good luck!
 
 

 
 
 
 
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章