從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!
 
 

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