nginx 配置解析(11)——merge

工程師WWW發表於2015-06-05

以前我們詳細了分析了 nginx 配置解析的各個過程,尤其是各種 conf 結構的建立、初始化、儲存。那麼現在我們要分析下 merge 的過程,一來它確實是配置解析過程的一個部分,二來我們可以檢驗下我們對各種 conf 結構儲存的位置是否真正的清晰明瞭。

首先我們瞭解下 merge 的背景:

  1. 所謂 merge 操作,就是合併內外層的配置。大體原則是:如果內層沒有配置,那麼以外層為準,如果都沒有配置,那麼就用預設值;
  2. NGX_CORE_MODULE 模組的 ctx(ngx_core_module_t)是沒有 merge 操作的,所以像 http 塊這一層的配置是不需要和上一層去 merge 的,想想也明白為什麼,http 哪來的上一層呢?
  3. NGX_HTTP_MODULE 模組的 ctx(ngx_http_module_t)是有 merge 操作的,但是僅僅有 merge_srv_conf 和 merge_loc_conf,同理對 main 層不需要 merge;
  4. merge 操作發生的時機是在 ngx_http_block函式中(即 http 塊解析函式),在遞迴呼叫 ngx_conf_parse 之後。這是為了讓 http 塊之內所有的指令都解析結束,然後再去做 merge 操作;
  5. 不同層級塊的邏輯關係,基本上都是放在 ngx_http_core_module 這個模組的不同級別的 conf 中,在 merge 中會頻繁用到。

來看看 ngx_http_block 中的一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for (m = 0; ngx_modules[m]; m++) {
    if (ngx_modules[m]->type != NGX_HTTP_MODULE) {
        continue;
    }   
 
    module = ngx_modules[m]->ctx;
    mi = ngx_modules[m]->ctx_index;
 
    /* init http{} main_conf's */
 
    if (module->init_main_conf) {
        rv = module->init_main_conf(cf, ctx->main_conf[mi]);
        if (rv != NGX_CONF_OK) {
            goto failed;
        }   
    }   
 
    rv = ngx_http_merge_servers(cf, cmcf, module, mi);
    if (rv != NGX_CONF_OK) {
        goto failed;
    }   
}

再明顯不過了,init 了 main,merge 了 server。 這裡的處理流程是個 for 迴圈,每次的迭代物件是模組本身,也就是挨個模組的去處理 merge。

在進入 ngx_http_merge_servers 內部分析之前,我們先確認一下這些代入的引數:

  1. cf 是代入的引數,但是我們真正關心的還是 cf->ctx,這個時候它其實就是 http 塊的三元組(代入 ngx_http_block 的時候還不是,但是在函式中賦值了);
  2. cmcf 這個 http 塊的 ngx_http_core_module 的 main_conf 結構;
  3. module 是個迴圈獲取的,代表當前模組;
  4. mi 就是當前模組在 NGX_HTTP_MODULE 模組中的 index;

這裡很奇怪的就是並沒有明確指出 merge loc_conf 的地方,我們推測它是在 ngx_http_merge_servers 中實現的,那麼我們具體的分析下它的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
static char *
ngx_http_merge_servers(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf,
    ngx_http_module_t *module, ngx_uint_t ctx_index)
{
    char                        *rv;
    ngx_uint_t                   s;
    ngx_http_conf_ctx_t         *ctx, saved;
    ngx_http_core_loc_conf_t    *clcf;
    ngx_http_core_srv_conf_t   **cscfp;
 
    cscfp = cmcf->servers.elts;
/* cmcf 裡儲存 server 的陣列,把這個陣列的地址賦值給 cscfp。*/
    ctx = (ngx_http_conf_ctx_t *) cf->ctx;
    saved = *ctx;
/* 這裡做了一個儲存的操作,在下面的程式碼中要改變 ctx 中的值,並且同時使用原始的 ctx。*/
    rv = NGX_CONF_OK;
 
    for (s = 0; s < cmcf->servers.nelts; s++) {
/* 這裡是遍歷所有的 server,挨個處理 merge 它的 srv_conf。*/
        /* merge the server{}s' srv_conf's */
 
        ctx->srv_conf = cscfp[s]->ctx->srv_conf;
/* 改變 cf->ctx 的 srv_conf,換成當前 server 塊對應的 srv_conf。*/
 
        if (module->merge_srv_conf) {
            rv = module->merge_srv_conf(cf, saved.srv_conf[ctx_index],
                                        cscfp[s]->ctx->srv_conf[ctx_index]);
/* 這裡就很明朗了,saved 就是 cf->ctx 的內容,那麼它就是 http 塊的三元組了,這裡代入
 * 的第二個引數也就是 http 塊中三元組所儲存模組對應的 srv_conf。cscfp[s] 代表對應的
 * server,而它的 ctx 也就是在解析那個 server 塊的時候建立的三元組,那麼第三個引數其
 * 實就是那個 server 塊自己所儲存的模組對應 srv_conf 結構。所以這裡,第二個引數是
 * parent,第三個引數是 child。*/
            if (rv != NGX_CONF_OK) {
                goto failed;
            }
        }
 
        if (module->merge_loc_conf) {
 
            /* merge the server{}'s loc_conf */
 
            ctx->loc_conf = cscfp[s]->ctx->loc_conf;
/* 改變 cf->ctx 的 loc_conf,換成當前 server 塊對應的 loc_conf。*/
 
            rv = module->merge_loc_conf(cf, saved.loc_conf[ctx_index],
                                        cscfp[s]->ctx->loc_conf[ctx_index]);
/* 在這裡和我們上一步分析的 merge_srv_conf 如出一轍,只是 srv_conf 換成了 loc_conf,
 * 別忘了在 http 塊和 server 塊也要儲存 loc_conf 的。*/
            if (rv != NGX_CONF_OK) {
                goto failed;
            }
 
            /* merge the locations{}' loc_conf's */
 
            clcf = cscfp[s]->ctx->loc_conf[ngx_http_core_module.ctx_index];
 
            rv = ngx_http_merge_locations(cf, clcf->locations,
                                          cscfp[s]->ctx->loc_conf,
                                          module, ctx_index);
/* 這裡終於要呼叫到下一層了,這個函式看名字就可以知道,是 merge 各個 location 的。*/
            if (rv != NGX_CONF_OK) {
                goto failed;
            }
        }
    }
 
failed:
 
    *ctx = saved;
/* 恢復了 ctx 內容,別忘了這個時候 ctx 表示的是 http 塊建立的三元組。*/
 
    return rv;
}

在這個函式中分別對 srv_conf 和 loc_conf 做了 merge 操作,用 http 塊建立的 srv_conf 和 loc_conf 作為外層,用 server 塊建立的 srv_conf 和 loc_conf 作為內層。但是考慮到這不是終結,還需要繼續深入到 location 中繼續 merge,所以要把 cf->ctx 的內容做修改,不斷的用內層內容覆蓋再繼續去 merge,所以重點關注下 cf->ctx 內容的改變。當然在函式最後結束的時候,還需要恢復 cf->ctx 的值。

那麼下一步我們需要分析一下 ngx_http_merge_locations 函式,這個函式負責各個 location 塊的 merge。同樣的,在分析它之前,先想想上下文環境:

  1. 在 ngx_http_merge_servers 中 cf->ctx 中的 srv_conf 和 loc_conf 都被改成當前 server 對應的 conf 結構了,明顯是為了進一步 merge 做準備;
  2. 從 ngx_http_merge_servers 中進入 ngx_http_merge_locations 的 clcf 就是從 server 塊 ngx_http_core_module 的 loc_conf;
  3. location 是可以巢狀的,那麼巢狀關係的內外層 location 是否也會 merge 呢?

我們來分析 ngx_http_merge_locations 的程式碼吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static char *
ngx_http_merge_locations(ngx_conf_t *cf, ngx_queue_t *locations,
    void **loc_conf, ngx_http_module_t *module, ngx_uint_t ctx_index)
{
    char                       *rv;
    ngx_queue_t                *q;
    ngx_http_conf_ctx_t        *ctx, saved;
    ngx_http_core_loc_conf_t   *clcf;
    ngx_http_location_queue_t  *lq;
 
    if (locations == NULL) {
        return NGX_CONF_OK;
    }
 
    ctx = (ngx_http_conf_ctx_t *) cf->ctx;
/* 這裡程式碼看似和 ngx_http_merge_servers 類似,但是差別在於,這個時候 cf->ctx 內
 * 相對應的 srv_conf 和 loc_conf 內容已經被改變為外層的對應 conf。在
 * ngx_http_merge_servers 中的相關程式碼我們已經分析過了,在這個函式中同樣有類似的
 * 程式碼。*/
    saved = *ctx;
 
    for (q = ngx_queue_head(locations);
         q != ngx_queue_sentinel(locations);
         q = ngx_queue_next(q))
    {
/* 遍歷一層的 location,挨個處理 merge。*/
        lq = (ngx_http_location_queue_t *) q;
 
        clcf = lq->exact ? lq->exact : lq->inclusive;
        ctx->loc_conf = clcf->loc_conf;
/* 改變 cf->ctx 的 loc_conf,換成當前 server 塊對應的 loc_conf。*/
 
        rv = module->merge_loc_conf(cf, loc_conf[ctx_index],
                                    clcf->loc_conf[ctx_index]);
/* 這裡是呼叫各個模組的 merge_loc_conf,在這裡 loc_conf 是上一層代入的,例如在 merge
 * server 的時候,它是對應 server 塊的 loc_conf 陣列。在下面的程式碼中,遞迴呼叫
 * ngx_http_merge_locations,代入的 loc_conf 就是這一層塊的 loc_conf 陣列,所以這裡的
 * loc_conf 其實就代表外層塊的 loc_conf 陣列。而 clcf 是很明顯的,是由 locations 佇列
 * 遍歷產生的,也就是代表當前的 location 塊。所以這裡,第二個引數是 parent,第三個參
 * 數是 child。*/
        if (rv != NGX_CONF_OK) {
            return rv;
        }
 
        rv = ngx_http_merge_locations(cf, clcf->locations, clcf->loc_conf,
                                      module, ctx_index);
/* 這裡遞迴呼叫了 ngx_http_merge_locations,巢狀 location 的 merge 操作也可以成功解決
 * 了,唯一值得注意的就是那些代入的引數,因為進入一層,所以對應的 locations 和
 * loc_conf 也更進了一層。*/
        if (rv != NGX_CONF_OK) {
            return rv;
        }
    }
 
    *ctx = saved;
 
    return NGX_CONF_OK;
}

這裡唯一值得注意的就是那個遞迴呼叫,這裡成功的解決了 location 巢狀的 merge 問題。

總結一下重點吧:

  1. 在 ngx_http_block 中呼叫 ngx_http_merge_servers 去 merge 對應的 srv_conf;
  2. 在 ngx_http_merge_servers 和 ngx_http_merge_locations 中呼叫 ngx_http_merge_locations 去 merge 對應的 loc_conf;
  3. 在記憶體上,注意內外層關係,仔細閱讀程式碼,看看內外級關係的記憶體是如何處理的;
  4. cf->ctx 在一層層的 merge 中也起到了至關重要的作用,到了內層 merge 的時候,它存的總是邏輯上的直接外層三元組。