準確地配置 NginX (1)

管理ngx的也挺長時間了, 記一些比較喜歡的筆記.

## 熟悉代碼

熟悉 ngx 代碼, 其實如果有個好的 IDE, 也不是什麼困難的事情. 可以看下這裡頭的回答 zhihu.com/question/2085, 其實ngx代碼不需要完全都熟悉, 對用戶來講, 最重要的函數, 我覺得就是 `ngx_http_init_phase_handlers` 和 `ngx_http_core_run_phases`, 這基本上就是每個 HTTP 請求的完整過程了. 很多配置也會需要你理解這些 `phases`.

## 模塊初始化

ngx 的 http 子模塊都必須有個入口才能被執行, 這個入口一般就在該模塊的初始化函數中被賦值. 舉個例子, `limit_req` 的模塊 (位於 http/modules/ngx_http_limit_req_module.c) 的初始化函數就是 `ngx_http_limit_req_init`. 所謂初始化函數不是每次http請求都會調用的函數, 是指nginx啟動的時候調用的函數. 來看下這個初始化函數:

static ngx_int_tngx_http_limit_req_init(ngx_conf_t *cf){ ngx_http_handler_pt *h; ngx_http_core_main_conf_t *cmcf; cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers); if (h == NULL) { return NGX_ERROR; } *h = ngx_http_limit_req_handler; return NGX_OK;}

可以看到 `limit_req_handler` (所有ngx的http模塊都有 ngx_http_ 前綴) 是註冊在 `NGX_HTTP_PREACCESS_PHASE` 階段的, 知道這個很重要(等下再講).

然後, 看下 `ngx_http_init_phase_handlers` 能看到 `limit_req_handler` 的調用者是 `ngx_http_core_generic_phase`, 這種函數被稱為checker, 其實就是每個模塊的handler, 因為功能不一樣, 所以他們定義的返回值是不一樣的, 不同的handler, 要用不同的checker來檢查返回值, 決定下一個動作. 比如 req_limit_handler 其實就是負責限速的, 所以有可能返回的值要求ngx先掛起這個請求(延遲響應, 返回 NGX_AGAIN). 大部分情況如果handler返回 NGX_DECLINED, 大概是指這個handler沒有在配置中使用, 繼續下一個模塊.

## PHASES 和配置的關係

首先, 要看下所有的 PHASES:

typedef enum { NGX_HTTP_POST_READ_PHASE = 0, NGX_HTTP_SERVER_REWRITE_PHASE, NGX_HTTP_FIND_CONFIG_PHASE, NGX_HTTP_REWRITE_PHASE, NGX_HTTP_POST_REWRITE_PHASE, NGX_HTTP_PREACCESS_PHASE, NGX_HTTP_ACCESS_PHASE, NGX_HTTP_POST_ACCESS_PHASE, NGX_HTTP_TRY_FILES_PHASE, NGX_HTTP_CONTENT_PHASE, NGX_HTTP_LOG_PHASE} ngx_http_phases;

上面說過 limit_req 運行在 PREACCESS PHASE, 然後我們去看看我們經常用的 `set` 命令是運行在哪個階段吧! 看下文檔, 發現 `set` 是屬於 rewrite 模塊, 所以看下 rewrite 模塊的 init 函數咯:

static ngx_int_tngx_http_rewrite_init(ngx_conf_t *cf){ ngx_http_handler_pt *h; ngx_http_core_main_conf_t *cmcf; cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); h = ngx_array_push(&cmcf->phases[NGX_HTTP_SERVER_REWRITE_PHASE].handlers); if (h == NULL) { return NGX_ERROR; } *h = ngx_http_rewrite_handler; h = ngx_array_push(&cmcf->phases[NGX_HTTP_REWRITE_PHASE].handlers); if (h == NULL) { return NGX_ERROR; } *h = ngx_http_rewrite_handler; return NGX_OK;}

這個 init 函數竟然有兩個PHASES! 那是因為 `set` 等命令既可以在 server block 配置, 也可以在 location block 配置, 然後 server block 的先執行. 舉個例子:

server { set $a 23; location / { set $a 24; echo $a; }}

這個配置, 如果 `GET /` 肯定是返回 24, 還是比較直觀的. 因為是同一個模塊裡面的, 他們的執行順序基本上是符合自覺的(除了 if, 後面再講).

如果我們的配置需要多個模塊的配合了, 比如, 我想對請求限速, 然後限速的來源可能是根據源IP地址 (`$remote_addr`), 也可能是某個請求頭 (比如X-Real-IP), 因為 ngx 本身有可能在其他代理後面 (比如ELB等), 當然你完全可以用兩套, 然後就不用擔心這些邏輯了, 但是如果你這樣做, 我就寫不下去了 :(

配置是這樣

limit_req_zone $my_key zone=test:10m rate=5r/s;server { location { limit_req zone=test; set $my_key $remote_addr; if ($http_x_real) { set $my_key $http_x_real; } echo "hello world"; }}

這段配置看起來就是我們要的配置了, 測試一下, 也沒有問題, 但是為什麼沒問題? 為了看上去更 confusing, 我故意把 limit_req 的那一行放到了最前面, 讓你誤以為 limit_req 先執行, 如果是limit_req 先執行, 你就有疑問了: 因為這時候 $my_key 的值還是空呢! $my_key 是在後面的 `set` 執行後才被賦值的.

記得前面講的 `limit_req` 是運行在 PREACCESS 階段的嗎? 再去看下 `set` 和 `if` 都是運行在 `REWRITE` 階段的, 看看 ngx_http_phases 的定義, 你發現 PREACCESS 是在 REWRITE 後面的, 然後去看 `ngx_http_core_run_phases` 也是按從小到大的順序執行的, 所以 PREACCESS 一定在 REWRITE 後面執行, 反過來說, 無論你把 `limit_req` 寫到哪裡, 它都是在 `set` 執行後才執行的, 比如:

limit_req_zone $my_key zone=test:10m rate=5r/s;server { set $my_key "abc"; limit_req zone=test; #儘管在上層, 看到的 my_key != "abc" location / { set $my_key $remote_addr; #這個值會被 limit_req 使用 echo "hello world"; }}

好了, 現在你知道, ngx 的配置並不是按書寫的順序執行的了! 記得有疑問就去看下每個模塊的 `ngx_http_xxx_init`, 然後你就知道它的執行順序了.

最後一個問題是, 如果用的不同模塊都運行在 REWRITE 階段怎麼辦? ngx 官方的模塊一般會最後執行, 但是 openresty 的 `rewrite_by_lua` 做了特殊處理, 如果是用了 openresty, 那 `rewrite_by_lua` 會在最後執行 (參見 `rewrite_by_lua_no_postpone`). 如果不確定, 你就應該去看看這個文件 `objs/ngx_modules.c`, 這個文件是 ngx 編譯後才會有哦. 但是你要仔細看 `ngx_http_init_phase_handlers`, 看它最後的那個小循環, 是倒序的!

for (j = cmcf->phases[i].handlers.nelts - 1; j >=0; j--) { ph->checker = checker; ph->handler = h[j]; ph->next = n; ph++; }

所以你在 `objs/ngx_modules.c` 看到的模塊, 在前面的會晚執行(當然是同一個PHASE下).

先講到這, 下一篇講下 `if` 語句有什麼問題 (其實就是agentzh老師的一篇blog的翻版). 然後就可以開始講 openresty 了.

謝謝閱讀.

推薦閱讀:

如何 windows下nginx+django+flup python3?
把nginx改為一個普通的tcp伺服器,應用層協議自己定義,有可行性嗎?
如何配置nginx+uwsgi+django?
使用uwsgi和nginx做伺服器和django框架,為什麼每次修改代碼都需要重啟uwsgi呢?

TAG:Nginx | 负载均衡 | Web服务器 |