天天看點

深入剖析nginx平滑重新開機

本文适合對nginx實作原理比較感興趣的同學閱讀,需要具備一定的服務端程式設計知識。

一、背景

在伺服器開發過程中,難免需要重新開機服務加載新的代碼或配置,如果能夠保證server重新開機的過程中服務不間斷,那重新開機對于業務的影響可以降為0。最近調研了一下nginx平滑重新開機,覺得很有意思,記錄下來供有興趣的同學查閱。

二、重新開機流程

重新開機意味着新舊接替,在交接任務的過程中勢必會存在新舊server并存的情形,是以,重新開機的流程大緻為:

  1. 啟動新的server
  2. 新舊server并存,兩者共同處理請求,提供服務
  3. 舊的server處理完所有的請求之後優雅退出

這裡,最主要的問題在于如何保證新舊server可以并存,如果重新開機前後的server端口一緻,如何保證兩者可以監聽同一端口。

三、nginx實作

1.為了驗證nginx平滑重新開機,筆者首先嘗試nginx啟動的情形下再次開啟一個新的server執行個體,結果如圖:

深入剖析nginx平滑重新開機

很明顯,重新開啟server執行個體是行不通的,原因在于新舊server使用了同一個端口80,在未開始socket reuseport選項複用端口時,bind系統調用會出錯。nginx預設bind重試5次,失敗後直接退出。而nginx需要監聽IPV4位址0.0.0.0和IPV6位址[::],故圖中列印出10條emerg日志。

2.接下來就開始嘗試平滑重新開機指令了,一共兩條指令:

kill -USR2 `cat /var/run/nginx.pid`
kill -QUIT `cat /var/run/nginx.pid.oldbin`
           
  • 第一條指令是發送信号USR2給舊的master程序,程序的pid存放在/var/run/nginx.pid檔案中,其中nginx.pid檔案路徑由nginx.conf配置。
  • 第二條指令是發送信号QUIT給舊的master程序,程序的pid存放在/var/run/nginx.pid.oldbin檔案中,随後舊的master程序退出。

那麼問題來了,為什麼舊的master程序的pid存在于兩個pid檔案之中?事實上,在發送信号USR2給舊的master程序之後,舊的master程序将pid重命名,原先的nginx.pid檔案rename成nginx.pid.oldbin。這樣新的master進行就可以使用nginx.pid這個檔案名了。

先執行第一條指令,結果如圖: 

深入剖析nginx平滑重新開機

不錯,新舊master和worker程序并存了。 再來第二條指令,結果如圖: 

深入剖析nginx平滑重新開機

如你所見,舊的master程序8527和其worker程序全部退出,隻剩下新的master程序12740。

不由得産生困惑,為什麼手動開啟一個新的執行個體行不通,使用信号重新開機就可以達到。先看下nginx log檔案: 

深入剖析nginx平滑重新開機

除了之前的錯誤日志,還多了一條notice,意思就是繼承了sockets,fd值為6,7。 随着日志翻看nginx源碼,定位到nginx.c/ngx_exec_new_binary函數之中,

ngx_pid_t
ngx_exec_new_binary(ngx_cycle_t *cycle, char *const *argv)
{
    ...

    ctx.path = argv[0];
    ctx.name = "new binary process";
    ctx.argv = argv;

    n = 2;
    env = ngx_set_environment(cycle, &n);
...
    var = ngx_alloc(sizeof(NGINX_VAR)
                    + cycle->listening.nelts * (NGX_INT32_LEN + 1) + 2,
                    cycle->log);
...

    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;
...
    env[n] = NULL;

...
    ctx.envp = (char *const *) env;

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

    if (ngx_rename_file(ccf->pid.data, ccf->oldpid.data) == NGX_FILE_ERROR) {
       ...
        return NGX_INVALID_PID;
    }

    pid = ngx_execute(cycle, &ctx);

    if (pid == NGX_INVALID_PID) {
        if (ngx_rename_file(ccf->oldpid.data, ccf->pid.data)
            == NGX_FILE_ERROR)
        {
            ...
        }
    }

...
    return pid;
}
           

函數的流程為

  • 将舊的master程序監聽的所有fd,拷貝至新master程序的env環境變量NGINX_VAR。
  • rename重命名pid檔案
  • ngx_execute函數fork子程序,execve執行指令行啟動新的server。

在server啟動流程之中,涉及到環境變量NGINX_VAR的解析,ngx_connection.c/ngx_add_inherited_sockets具體代碼為:

static ngx_int_t
ngx_add_inherited_sockets(ngx_cycle_t *cycle)
{
...
    inherited = (u_char *) getenv(NGINX_VAR);
    if (inherited == NULL) {
        return NGX_OK;
    }
    if (ngx_array_init(&cycle->listening, cycle->pool, 10,
                       sizeof(ngx_listening_t))
        != NGX_OK)
    {
        return NGX_ERROR;
    }

    for (p = inherited, v = p; *p; p++) {
        if (*p == ':' || *p == ';') {
            s = ngx_atoi(v, p - v);
            ...
            v = p + 1;

            ls = ngx_array_push(&cycle->listening);
            if (ls == NULL) {
                return NGX_ERROR;
            }

            ngx_memzero(ls, sizeof(ngx_listening_t));

            ls->fd = (ngx_socket_t) s;
        }
    }
    ...
    ngx_inherited = 1;

    return ngx_set_inherited_sockets(cycle);
}
           

函數流程為:

  • 解析環境變量NGINX_VAR的值,擷取fd存入數組
  • fd對應的socket設為ngx_inherited,儲存這些socket的資訊。

也就是說,新的server壓根就沒重新bind端口listen,這些fd狀态和值都是新的master程序fork時帶過來的,新的master程序監聽處理繼承來的檔案描述符即可,這裡比較關鍵的一點在于listen socket檔案描述符通過ENV傳遞。

想要擷取最新技術文章?歡迎訂閱微信公衆号----軟體程式設計之路

深入剖析nginx平滑重新開機

繼續閱讀