Nginx 配置指令的執行順序(四)
ngx_lua 模塊提供了配置指令 access_by_lua,用于在 access
請求處理階段插入用戶 Lua 代碼。這條指令運行于 access
階段的末尾,因此總是在 allow 和 deny 這樣的指令之后運行,雖然它們同屬 access
階段。一般我們通過 access_by_lua 在 ngx_access 這樣的模塊檢查過客戶端 IP 地址之后,再通過 Lua 代碼執行一系列更為復雜的請求驗證操作,比如實時查詢數據庫或者其他后端服務,以驗證當前用戶的身份或權限。
我們來看一個簡單的例子,利用 access_by_lua 來實現 ngx_access 模塊的 IP 地址過濾功能:
location /hello {
access_by_lua '
if ngx.var.remote_addr == "127.0.0.1" then
return
end
ngx.exit(403)
';
echo "hello world";
}
這里在 Lua 代碼中通過引用 Nginx 標準的內建變量 $remote_addr 來獲取字符串形式的客戶端 IP 地址,然后用 Lua 的 if
語句判斷是否為本機地址,即是否等于 127.0.0.1
. 如果是本機地址,則直接利用 Lua 的return
語句返回,讓 Nginx 繼續執行后續的請求處理階段(包括 echo 指令所處的 content
階段);而如果不是本機地址,則通過 ngx_lua 模塊提供的 Lua 函數 ngx.exit 中斷當前的整個請求處理流程,直接返回 403
錯誤頁給客戶端。
這個例子在功能上完全等價于先前在 (三) 中介紹過的那個使用 ngx_access 模塊的例子:
location /hello {
allow 127.0.0.1;
deny all;
echo "hello world";
}
雖然這兩個例子在功能上完全相同,但在性能上還是有區別的,畢竟 ngx_access 是用純 C 實現的專門化的 Nginx 模塊。
下面我們不妨來實際測量一下這兩個例子的性能差別。因為我們使用 Nginx 就是為了追求性能,而量化的性能比較,在工程上具有很大的現實意義,所以我們順便介紹一下重要的測量技術。由于無論是 ngx_access 還是 ngx_lua 在進行 IP 地址驗證方面的性能都非常之高,所以為了減少測量誤差,我們希望能對 access
階段的用時進行直接測量。為了做到這一點,傳統的做法一般會涉及到修改 Nginx 源碼,自己插入專門的計時代碼和統計輸出代碼,抑或是重新編譯 Nginx 以啟用像 GNU gprof
這樣專門的性能監測工具。
幸運的是,在新一點的 Solaris, Mac OS X, 以及 FreeBSD 等系統上存在一個叫做 dtrace
的工具,可以對任意的用戶程序進行微觀性能分析(以及行為分析),而無須對用戶程序的源碼進行修改或者對用戶程序進行重新編譯。因為 Mac OS X 10.5 以后就自帶了 dtrace
,所以為方便起見,下面在我的 MacBook Air 筆記本上演示一下這里的測量過程。
首先,在 Mac OS X 系統中打開一個命令行終端,在某一個文件目錄下面創建一個名為 nginx-access-time.d
的文件,并編輯內容如下:
#!/usr/bin/env dtrace -s
pid$1::ngx_http_handler:entry
{
elapsed = 0;
}
pid$1::ngx_http_core_access_phase:entry
{
begin = timestamp;
}
pid$1::ngx_http_core_access_phase:return
/begin > 0/
{
elapsed += timestamp - begin;
begin = 0;
}
pid$1::ngx_http_finalize_request:return
/elapsed > 0/
{
@elapsed = avg(elapsed);
elapsed = 0;
}
保存好此文件后,再賦予它可執行權限:
$ chmod +x ./nginx-access-time.d
這個 .d
文件中的代碼是用 dtrace
工具自己提供的 D
語言來編寫的(注意,這里的 D
語言并不同于 Walter Bright 作為另一種“更好的 C++”而設計的 D
語言)。由于本系列教程并不打算介紹如何編寫 dtrace
的 D
腳本,同時理解這個腳本需要不少有關 Nginx 內部源碼實現的細節,所以這里我們不展開介紹。大家只需要知道這個腳本的功能是:統計指定的 Nginx worker 進程在處理每個請求時,平均花費在 access
階段上的時間。
現在來演示一下這個 D
腳本的運行方法。這個腳本接受一個命令行參數用于指定監視的 Nginx worker 進程的進程號(pid)。由于 Nginx 支持多 worker 進程,所以我們測試時發起的 HTTP 請求可能由其中任意一個 worker 進程服務。為了確保所有測試請求都為固定的 worker 進程處理,不妨在 nginx.conf
配置文件中指定只啟用一個 worker 進程:
worker_processes 1;
重啟 Nginx 服務器之后,可以利用 ps
命令得到當前 worker 進程的進程號:
$ ps ax|grep nginx|grep worker|grep -v grep
在我機器上的一次典型輸出是
10975 ?? S 0:34.28 nginx: worker process
其中第一列的數值便是我的 nginx worker 進程的進程號,10975
。如果你得到的輸出不止一行,則通常意味著你的系統中同時運行著多個 Nginx 服務器實例,或者當前 Nginx 實例啟用了多個 worker 進程。
接下來使用剛剛得到的 worker 進程號以及 root 身份來運行 nginx-access-time.d
腳本:
$ sudo ./nginx-access-time.d 10975
如果一切正常,則會看到這樣一行輸出:
dtrace: script './nginx-access-time.d' matched 4 probes
這行輸出是說,我們的 D
腳本已成功向目標進程動態植入了 4 個 dtrace
“探針”(probe)。緊接著這個腳本就掛起了,表明 dtrace
工具正在對進程 10975
進行持續監視。
然后我們再打開一個新終端,在那里使用 curl
這樣的工具多次請求我們正在監視的接口
$ curl 'http://localhost:8080/hello'
hello world
$ curl 'http://localhost:8080/hello'
hello world
最后我們回到原先那個一直在運行 D
腳本的終端,按下 Ctrl-C
組合鍵中止 dtrace
的運行。而該腳本在退出時會向終端打印出最終統計結果。例如我的終端此時是這個樣子的:
$ sudo ./nginx-access-time.d 10975
dtrace: script './nginx-access-time.d' matched 4 probes
^C
19219
最后一行輸出 19219
便是那幾次 curl
請求在 access
階段的平均用時(以納秒,即 10 的負 9 次方秒為單位)。
通過上面介紹的步驟,可以通過 nginx-access-time.d
腳本分別統計出各種不同的 Nginx 配置下 access
階段的平均用時。針對我們感興趣的三種情況可以進行三組平行試驗,即使用 ngx_access 過濾 IP 地址的情況,使用 access_by_lua 過濾 IP 地址的情況,以及不在 access
階段使用任何配置指令的情況。最后一種情況屬于“空白對照組”,用于校正測試過程中因 dtrace
探針等其他因素而引入的“系統誤差”。另外,為了最小化各種不可控的“隨機誤差”,可以用 ab
這樣的批量測試工具來取代 curl
發起連續十萬次以上的請求,例如
$ ab -k -c1 -n100000 'http://127.0.0.1:8080/hello'
這樣我們的 D
腳本統計出來的平均值將更加接近“真實值”。
在我的蘋果系統上,一次典型的測試結果如下:
ngx_access 組 18146
access_by_lua 組 35011
空白對照組 15887
把前兩組的結果分別減去“空白對照組”的結果可以得到
ngx_access 組 2259
access_by_lua 組 19124
可以看到,ngx_access 組比 access_by_lua 組快了大約一個數量級,這正是我們所預期的。不過其絕對時間差是極小的,對于我的 Intel Core2Duo 1.86 GHz
的 CPU 而言,也只有區區十幾微秒,或者說是在十萬分之一秒的量級。
當然,上面使用 access_by_lua 的例子還可以通過換用 $binary_remote_addr 內建變量進行優化,因為$binary_remote_addr 讀出的是二進制形式的 IP 地址,而 $remote_addr 則返回更長一些的字符串形式的地址。更短的地址意味著用 Lua 進行字符串比較時通常可以更快。
值得注意的是,如果按 (一) 中介紹的方法為 Nginx 開啟了“調試日志”的話,上面統計出來的時間會顯著增加,因為“調試日志”自身的開銷是很大的。
RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成