再也不依靠巧合編寫 Nginx 配置

原博:https://blog.coordinate35.cn/...html

熱身

首先來看下這幾個小例子:nginx

第一個例子:數組

server {
    listen 80;
    root /var/www/html;
    index index.html;
    
    location /test {
        root /var/www/demo
    }
}

其中,echo指令來源於第三方模塊 echo ,做用是讓 Nginx 在接收到請求的時候將 echo 後面參數做爲HTTP報文體進行返回。數據結構

第二個例子是:框架

location /test {
    set $a 32;
    echo $a;
    set $a 56;
    echo $a;
}

第三個例子是:模塊化

location /test {
    echo hello;
    content_by_lua 'ngx.say("world")';
}

你們能夠想一下,假定全部可能須要的資源都存在,若是 Nginx 收到 /test 的請求,這三種狀況下 Nginx 分別會返回什麼內容。函數

模塊化設計的Nginx

首先咱們們嘗試一下使用官方的代碼構建一次Nginx。從Nginx官網下載最新的穩定版本1.14.0。執行:佈局

./configure

能夠發現,這一操做生成了 Makefile 文件和 objs 目錄,咱們打開生成的其中一個很是關鍵的文件:objs/ngx_modules.c。能夠看到,這個文件定義了兩個數組:post

  1. ngx_modules 數組的成員是 Nginx 全部須要使用的模塊的對象的指針。
  2. ngx_module_names 數組是上一數組成員一一對應的模塊的名字。

從這個文件基本上能夠窺探出,除了少許核心代碼,其他Nginx的代碼是由一個個這樣的模塊構成的。須要特別說明的是,這個數組裏面各個模塊的前後順序特別重要。這個前後順序表明了在Nginx中模塊的優先級,當兩個模塊的功能有重疊的時候,經過在數組裏面的前後順序來決定使用哪一個模塊的邏輯。事實上,Nginx有五大類型的模塊:核心模塊、配置模塊、事件模塊、HTTP模塊、mail模塊。lua

HTTP模塊內與配置相關的關鍵數據結構

因爲HTTP模塊是Nginx中數量最多的模塊,咱們平常寫配置文件是用的命令也大多屬於HTTP模塊,因爲篇幅,咱們就重點關注HTTP類型的模塊。

首先是 ngx_command_t 類型,定義舉例:

static ngx_command_t ngx_http_gzip_filter_commands[] = {
    { ngx_string("gzip"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF
                        |NGX_CONF_FLAG,
      ngx_conf_set_flag_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_gzip_conf_t, enable),
      NULL },
    ...,
    ngx_null_command
};

這是一個數組,存放了這個模塊裏可用的全部指令。對於數組的每個元素,

第一個參數是指令的名稱

第二個參數是有關於這個指令的類型描述:指令是在http塊出現,仍是server塊出現,仍是在location塊出現?這個指令以後跟多少個參數?參數的類型是什麼,數值仍是一個配置塊。

第三個參數是一個函數指針,這個函數用於解析指令後的參數。第四個參數是

第四個參數是指配置項所處內存的相對位置。這個描述會在稍後詳細說明。

第五個參數是配置項在整個存儲配置結構體中的偏移位置。

第六個參數使用較少,不作說明。

而後是 ngx_http_module_t 類型

static ngx_http_module_t  ngx_http_gzip_filter_module_ctx = {
    ngx_http_gzip_add_variables,           /* preconfiguration */
    ngx_http_gzip_filter_init,             /* postconfiguration */

    NULL,                                  /* create_main_conf */
    NULL,                                  /* init_main_conf */

    NULL,                                  /* create_srv_conf */
    NULL,                                  /* merge_srv_conf */

    ngx_http_gzip_create_conf,             /* create_loc_conf */
    ngx_http_gzip_merge_conf               /* merge_srv_conf */
};

這個結構體的做用將在稍後說明。

配置文件解析

首先要對一些名詞進行說明:

  1. 直接在 http{} 下的配置叫 main 配置項
  2. 直接在 server{} 下的配置叫 srv 配置項
  3. 直接在 location{} 下的配置叫 loc 配置項

在Nginx解析配置文件的時候,會調用 ngx_conf_parse 這個函數進行配置文件解析。首先應該清楚地認識到,Nginx 的配置文件實際上就是由指令和指令參數組成的。ngx_conf_parse首先會將配置文件進行詞法分析,將配置文件生成一個指令數組,數組的每個元素也都是一個字符串數組,成員數組的第一個元素是解析出來的指令名字,以後的參數是配置文件裏這個指令的參數列表。而後,ngx_conf_parse 會遍歷這個指令數組,對於每個指令,Nginx會遍歷一次全部的模塊,直到發現第一個,指令出現位置和參數要求都符合要求的模塊(也就是以前提到的ngx_command_t數組元素的第二條配置。這也意味着,若是有兩個模塊都定義了同一個指令的名字,參數和出現的位置都符合要求,Nginx會選擇使用在上面提到的 ngx_module_t* 數組排的靠前的那個模塊,由於先遍歷到)。找到這個模塊的指令後,則會調用這個指令的解析回調函數(即 ngx_command_t 結構體的第三個參數)來進行處理。若是該指令是一個用{}包圍的配置塊,則會遞歸地調用 ngx_conf_parse 來進行配置文件解析。

解析的過程當中,當碰到一個 http 指令的時候(其實一個也只能有一個http指令),該指令的解析回調函數會建立一個叫 ngx_http_conf_ctx_t 的結構體。這個結構體的定義以下:

typedef struct {
    void **main_conf;
    void **srv_conf;
    void **loc_conf;
} ngx_http_conf_ctx_t;

結構體中,兩個星表明這個參數是一個指針數組。而後根據HTTP模塊的數量,創建長度相匹配 main_conf、srv_conf、loc_conf 數組。接着,依次遍歷各個HTTP模塊。調用他們 ngx_http_module_t(上面提到的) 中的 create_main_conf、create_srv_conf、create_loc_conf 回調函數來申請和初始化對應模塊的配置結構體。也就是說main_conf、srv_conf、loc_conf數組中下標爲n的元素,都對應着第 n+1 個HTTP模塊配置結構體。須要注意的是,即時當前是直接在 http 塊(main級別),create_main_conf、create_srv_conf、create_loc_conf 這三個回調函數都會被調用。具體緣由會稍後說明。

作完上述步驟後,Nginx 會遞歸地調用 ngx_conf_parse 來解析 以後 {} 中的配置項,在這個過程當中,每碰到一個 server 指令的以後,這個指令的解析回調函數又會建立一個屬於這個 server 塊的 ngx_http_conf_ctx_t 結構體。惟一不一樣的就是,這個結構體的 main_conf 會指向他的父 http 塊的 main_conf 數組(顯而易見,在srv 級別的配置裏,main級別的配置是不會發生變化的)。在解析 srv 級別的配置中,若是有同一個模塊的同一個指令既出如今了 main 級別的塊下,又出如今了 srv 級別的塊下,應該以哪個爲準呢?這就輪到咱們的merge函數大顯身手,同時這也解釋了爲何無論在什麼級別下,都要爲每一個模塊生成 main_conf、srv_conf、loc_conf。這是由於有些配置項能夠同時出如今 http{} server{} location{} 中。這樣咱們就會把只能在 http{} 出現的指令放在各模塊的 main_conf 結構體裏面,把只能出如今 http{} server{} 的配置項放在 srv_conf 結構體裏面,把在 http{} server{} location{} 都能出現的配置項就放在 loc_conf 結構體裏面。在咱們遍歷到 srv 級別這種狀況,好比 ssl 指令。這時就會調用 ngx_http_ssl_module 模塊的 ngx_http_module_t 結構體(上面有提到) merge_srv_conf 回調函數來進行合併。在 ssl 模塊的 merge_srv_conf 函數中的某一段代碼以下:

if (conf->enable == NGX_CONF_UNSET) {
    if (prev->enable == NGX_CONF_UNSET) {
        conf->enable = 0;

    } else {
        conf->enable = prev->enable;
        conf->file = prev->file;
        conf->line = prev->line;
    }
}

這裏, conf 和 prev 的類型都是ngx_http_ssl_srv_conf_t。當遇到 ssl 指令時,因爲 ssl 指令的值是 on|off, 這個會被對應的將 ngx_http_ssl_srv_conf_t 的結構體中的 enable成員設置成1|0。conf 是當前級別(srv)下的指針,prev 是父級別(main)的指針。這段代碼的意思是,若是當前級別下沒有設置,則使用父級別的配置,若是父級別也沒有配置,則默認關閉。因而可知,並不必定全部指令的內層塊的配置都優先於外層塊的,具體採用哪一個值取決於 merge 函數的編寫。

同理,在解析 srv 級別的配置的時候,每碰到一個 location 塊,這個指令的解析回調函數又會建立一個屬於這個 location 塊的 ngx_http_conf_ctx_t 結構體,他的 main_conf 和 loc_conf 都會指向父級 ngx_http_conf_ctx_t 結構體的 main_conf 和 loc_conf。解析完全部配置項後進行和父級配置的合併。至此,配置的解析完畢,最終會生成一個這樣的內存佈局:

圖片描述

HTTP框架的執行流程

配置文件全部解析完了以後 ,Nginx才正式開始fork出 worker 進程,接收請求的處理。

在 Nginx 中,對 HTTP 請求的處理被劃分紅了11個處理階段:

  1. NGX_HTTP_POST_READ_PHASE
  2. NGX_HTTP_SERVER_REWRITE_PHASE
  3. NGX_HTTP_FIND_CONF_PHASE
  4. NGX_HTTP_REWRITE_PHASE
  5. NGX_HTTP_POST_REWRITE_PHASE
  6. NGX_HTTP_PREACCESS_PHASE
  7. NGX_HTTP_ACCESS_PHASE
  8. NGX_HTTP_POST_ACCESS_PHASE
  9. NGX_HTTP_TRY_FILES_PHASE
  10. NGX_HTTP_CONTENT_PHASE
  11. NGX_HTTP_LOG_PHASE

對於每個請求的處理,都是必須通過這些階段的。在HTTP核心模塊裏,有一個 ngx_http_core_main_conf_t 的結構體,裏面有個成員是:

ngx_http_phase_t phase[NGX_HTTP_LOG_PHASE + 1];

而 ngx_http_phase_t 的定義以下:

typedef struct {
    ngx_array_t handlers;
} ngx_http_phase_t;

也就是說,原則上,每一個階段都有一個本身的 handlers 數組,數組的元素來源於各個模塊將本身的 handler 放到本身感興趣的階段的數組中來介入哥哥執行階段。經過該階段的 handlers 數組中 handler 的依次執行,來達到各個模塊間相互配合的目的。

可是 NGX_HTTP_CONTENT_PHASE 階段,也就是響應內容生成的階段則稍有例外,而這個階段也是大多數模塊介入的階段。要介入這個階段,不只能夠經過往 handlers 數組添加 handler 的方式,還能夠經過設置 ngx_http_core_loc_conf_t 中的 handler 指針來實現。經過這種方式,handlers數組的handler就會所有被屏蔽掉,而只有這個handler生效。顯然,若是有兩個模塊都嘗試去經過這種方式介入 NGX_HTTP_CONTENT_PHASE 階段,必然只有一個能生效。

回看例子

咱們回頭來看看咱們先前的例子,如今有頭緒了嗎?

對於第一個例子,root 的配置在 merge 的過程當中,使用了 loc 級別的配置。不過可能仍是得注意不必定永遠都會這樣。

對於第二個例子,咱們能夠看到 set 指令是在加載配置的過程當中將變量設置好的。在進行 HTTP 請求處理的時候,變量 $a 的值已經被覆蓋過一次了,因此返回的結果是兩個64.這說明配置一般不是按直覺上的從上而下執行的,必定要結合整個 Nginx 的配置加載-請求處理的原理進行考慮。

對於第三個例子,經過閱讀代碼,咱們知道 echo 指令和 content_by_lua 都是經過設置 ngx_http_core_main_conf_t 的 handler 成員來介入 NGX_HTTP_CONTENT_PHASE 階段的,因此只有一個會生效,具體哪一個指令會生效,取決於這兩個指令所在模塊的在 ngx_modules 數組的前後位置。

結論

Nginx 的配置不少時候會和咱們所想的有所出入,同時它又時候也不是那麼直觀明瞭。當踩到坑的時候,必定要多查看文檔,結合 Nginx 的原理進行分析。甚至是去閱讀指令所在模塊的代碼(主要是配置合併函數和模塊介入各個階段的方式),而後去有理有據的書寫配置。拒絕暴力枚舉式編寫配置文件!

相關文章
相關標籤/搜索