Nginx if 指令工作原理
Nginx 的if
指令被認為是“邪惡”的,就和 C 語言的goto
一樣。甚至官方有一篇
ofollow,noindex" target="_blank">If is Evial
來警告你不要使用if
。但有時候 if 還是非常有用的,如果掌握了它的原理,在合適的地方正確使用 if,會讓事情更簡單一些。當然前提是你真正知道自己在做什麼,就和 goto 一樣。 ——這是我的觀點。
首先每個接觸 Nginx 的人應該意識到的事情是,Nginx 是分 phase(階段) 的,並不像 C 這種程式語言一樣順序執行。指令執行的順序和書寫的順序沒有太大關係(跟具體模組的實現有關),一個 phase 執行完了就會執行到下一個階段。
If 是屬於 rewrite 模組的,所以對於 if 來講,會和其他的 rewrite 模組執行全部執行完之後再進行下一階段。如果 if 指令的結果是 match 的,那麼 if 會建立一個內嵌的 location 塊,只有這裡面的 content 處理指令(NGX_HTTP_CONTENT_PHASE 階段)會執行。
下面是 agentzh 的四個例子,我這裡稍加自己的解釋。
location /proxy { set $a 32; if ($a = 32) { set $a 56; } set $a 76; proxy_pass http://127.0.0.1:$server_port/$a; } location ~ /(\d+) { echo $1; }
實驗的機器 IP 是 172.28.128.4 ,結果如下:
➜ nginx git:(master) ✗ curl 172.28.128.4/proxy 76
首先,對於一個請求 Nginx 會執行 rewrite 階段,即如下程式碼。rewrite 階段的執行順序和指令的順序是一樣的,這個 rewrite 模組的實現有關。
set $a 32; if ($a = 32) { set $a 56; } set $a 76;
$a
被設為 32,然後進入 if block,$a
在這裡被設為 56,最後$a
被設為 76. 中間 if block 生效。但是 if block 中沒有任何 content 階段的指令,所以會繼承 outter block,即ngx_proxy
模組的proxy_pass
設定。這裡要注意的是請求在 if block 內完成,if 命中之後就進入了 if block 來處理下一階段,而不會跳出 if。
第二段示例如下:
location /proxy { set $a 32; if ($a = 32) { set $a 56; echo "a = $a"; } set $a 76; proxy_pass http://127.0.0.1:$server_port/$a; } location ~ /(\d+) { echo $1; }
結果如下:
➜ nginx git:(master) curl 172.28.128.4/proxy a = 76
Rewrite 階段的過程和上面一樣,不同是這一次 if block 中有了 content 階段的指令,所以會執行 echo,不會執行到proxy_pass
。
Rewrite 階段的break
可以終止 rewrite 階段的執行。
location /proxy { set $a 32; if ($a = 32) { set $a 56; break; echo "a = $a"; } set $a 76; proxy_pass http://127.0.0.1:$server_port/$a; } location ~ /(\d+) { echo $1; }
以上程式碼的結果是
➜ nginx git:(master) curl 172.28.128.4/proxy a = 56
在 rewrite 階段中,執行完if ($a = 32)
之後執行set $a 56
,此時下一行是break
,然後 rewrite 階段就停止了,進行下一階段。set $a 76
並沒有被執行到。所以最後$a
的值是 76。
ngx_proxy 會繼承 outter scope,但是很多模組並不會這樣,這個地方挺坑人的,我就是在這裡被坑到的。
location /proxy { set $a 32; if ($a = 32) { echo "python"; } echo "hello"; echo "java"; }
參考這段配置,正常來說,所有的echo
都會執行,即如果不存在if
的話,這段配置的結果應該是hello \n java
。但是這裡結果會是:
➜ nginx git:(master) ✗ curl 172.28.128.4/proxy python
可以看到echo
並沒有繼承 outter 。
順便說一下我寫的那段配置吧。簡化之後如下:
location /proxy { if ($request_method = POST) { access_by_lua 'lua code...'; } access_by_lua_file file/location; }
我期望如果進 if 和不進 if,都會執行我的access_by_lua_file
,但事實看來,進入 if 之後並不會再出來,而且access_by_lua
和access_by_lua_file
像 echo 一樣,if 內並不會繼承外面的access_by_lua_file
。所以如果 if 命中,那麼access_by_lua_file
永遠不會執行到。
最後一個例子是會繼承 outter 的:
location /proxy { set $a 32; if ($a = 32) { return 404; } set $a 76; proxy_pass http://127.0.0.1:$server_port/$a; more_set_headers "X-Foo: $a"; } location ~ /(\d+) { echo $1; }
結果如下:
$ curl 172.28.128.4/proxy HTTP/1.1 404 Not Found Server: nginx/0.8.54 (without pool) Date: Mon, 14 Feb 2011 05:24:00 GMT Content-Type: text/html Content-Length: 184 Connection: keep-alive X-Foo: 32
可以看到,這個模組的 more_set_headers 指令是預設繼承 outter 的。
所以,官方給出的建議是儘量不要使用 if 指令,比如說有些地方其實可以使用try_files 。
如果用,那麼儘量只在 if block 內使用 rewrite 模組的指令。因為大家都是在這一個 phase 裡面的,不會有 surprise 了。
在某些情況下,這些需要 if 的指令可以用嵌入的第三方模組來完成,比如ngx_lua perl 等。
實在要用的話,做好充足的測試。