前兩天使用K8S的ingress配置,遇到兩個有包含關係的uri需要匹配到兩個不同的容器中的情況,找了一下具體的匹配規則,主要來自這篇文章
根據墨菲定律,可能會發生的事就一定會發生。如果對Nginx的路徑配置一直存疑,遲早會出問題。因此,搞懂很重要。
總體來說,Nginx將配置根據不同的server分成了不同的塊,每當一個請求過來時,Nginx都會根據一定的算法確定哪一個配置塊來處理該請求。其中起關鍵性作用的是server塊和location塊。前者定義了一個虛擬服務器,管理員通常會定義多個server塊,然後根據請求的域名,端口或IP決定匹配到哪一個;後者存在於server塊內,根據URI對虛擬服務器進行更加詳細的區分。二者組合起來能夠實現非常靈活的配置。
server
在server塊內,主要通過listen指令和server_name指令配置
listen
首先,Nginx查看請求的IP地址和端口,將它與每個server塊的listen匹配。
listen指令用於定義server塊需要響應的IP和端口。默認情況下,任何不包含listen指令的server塊都將被分配一個默認值:對於root用戶,將被設置爲0.0.0.0:80;對於普通用戶,將被設置爲0.0.0.0:8080。
listen指令有如下幾種可能性。
- IP + Port
- 單獨的IP,此時Port將被設爲默認值80
- 單獨的Port, 此時將會監聽該端口上的所有接口
- Unix的socket文件路徑
匹配時,算法如下
- 首先將不完整的listen指令使用默認值填充完整
- 沒有設置listen指令的,使用0.0.0.0:80替換
- 單獨的IP,端口默認設爲80
- 單獨的端口,IP默認設爲0.0.0.0
- 將IP-Port和請求的IP-Port對比
- 如果只有一個server塊匹配成功,則該server爲最終結果。如果有多個server塊匹配成功,則繼續根據server_name進行匹配。
注意喲:server_name只有在需要區分listen匹配的多個結果時纔會被使用。一個典型的例子,如果example.com被解析到192.168.1.10,此時要在80上進行匹配。則下面兩個配置永遠只會匹配到第一個。
server {
listen 192.168.1.10;
. . .
}
server {
listen 80;
server_name example.com;
. . .
}
server_name
當listen指令匹配到多個結果時,server_name就發揮作用了。
Nginx會檢查請求的Host頭部,根據如下算法,將其值與server_name指令配置內容進行對比
- 首先進行精確匹配,如果匹配數量爲1,則使用;如果精確匹配到多個,則使用第一個;如果匹配爲0,則繼續
- 匹配以*開頭的配置值,如匹配數量爲1,則使用;如匹配到多個,則使用最長那個;如果匹配爲0,則繼續
- 匹配以*結尾的配置值,如匹配數量爲1,則使用;如匹配到多個,則使用最長那個;如果匹配爲0,則繼續
- 使用正則表達式匹配,使用匹配到的第一個;如果匹配爲0,則繼續
- 到這裏還沒有匹配到,則使用該IP和Port對應的默認server塊
注意喲:一個IP和Port對應的默認server塊,就是根據listen匹配結果集的第一個,或包含了default_server選項的server塊。
例一:host1.example.com會匹配第二個server塊。滿足算法第一點。
server {
listen 80;
server_name *.example.com;
. . .
}
server {
listen 80;
server_name host1.example.com;
. . .
}
例二:www.example.org匹配第二個server塊。滿足算法第二點。
server {
listen 80;
server_name www.example.*;
. . .
}
server {
listen 80;
server_name *.example.org;
. . .
}
server {
listen 80;
server_name *.org;
. . .
}
例三:www.example.org匹配第三個server塊。滿足算法第三點。
server {
listen 80;
server_name host1.example.com;
. . .
}
server {
listen 80;
server_name example.com;
. . .
}
server {
listen 80;
server_name www.example.*;
. . .
}
例四:www.example.org匹配第二個server塊。滿足算法第四點。
server {
listen 80;
server_name example.com;
. . .
}
server {
listen 80;
server_name ~^(www|host1).*\.example\.com$;
. . .
}
server {
listen 80;
server_name ~^(subdomain|set|www|host1).*\.example\.com$;
. . .
}
location
語法
介紹算法前,先講講語法,標準語法如下
location optional_modifier location_match {
. . .
}
其中optional_modifier取下面幾種可能的值
- (none) : 即沒有optional_modifier,按照前綴進行匹配
- = : 完全匹配
- ~ : 按照大小寫敏感的正則表達式匹配
- ~* : 按照大小寫不敏感的正則表達式匹配
- ^~ : 不按照正則表達式匹配,注意,這裏是顯式地抑制正則表達式的解析
下面舉例
# 匹配/site /site/page/index.html /site/index.html等
location /site {
. . .
}
# 只能匹配 /site
location = /site {
. . .
}
# 可匹配/hello.jpg,但是不能匹配/hello.JPG
location ~ \.(jpe?g|png|gif|ico)$ {
. . .
}
# 上面的大小寫不敏感版本
location ~* \.(jpe?g|png|gif|ico)$ {
. . .
}
# 能夠匹配/customs/hello.html
location ^~ /customs {
. . .
}
匹配算法
location的匹配方式和server塊類似,都是找最優匹配,具體算法如下
- 找出所有匹配URI前綴的location塊,作爲備選
- 檢查精確匹配的項(即=修飾的項),如果有結果,則直接使用它最爲最終匹配結果。否則進行下一步
- 如果沒有精確項匹配,開始匹配不精確項。找出最長前綴匹配的項,按照如下規則檢查
- 若最長前綴匹配的項被^~修飾,則使用它作爲結果
- 若最長前綴匹配的項未被^~修飾,則此結果會被Nginx暫存起來
- 解析正則表達式(包含了大小寫敏感和不敏感),在上面的按照最長前綴匹配的項中有任何包含正則表達式的項,則進行正則表達式匹配。一旦匹配成功,則用它作爲結果
- 如果沒有正則表達式匹配成功,就使用之前被暫存的匹配項作爲結果
注意喲:這裏所說的基於前綴,意思是location指定的值和請求URI的前綴能夠匹配。比如 URI 爲 /customs/hello/halo時,location /custom
、location /custom/hello
、location ~ /.*/hello
都是能夠匹配的
注意喲:所謂最長前綴匹配項,即儘可能多地匹配URI。比如location /custom/hello
相比location /custom
,就是較長的匹配項。
注意喲:默認情況下,相對使用前綴,Nginx會優先使用正則表達式進行匹配。但在這裏,ngin首先檢查所有前綴location,從而允許我們使用=和^~修飾符來覆蓋這個原則。
注意喲:Nginx會匹配最長最具體的location,但當一個location被當做匹配結果時,正則表達式的解析就停止了,因此location之間的相對位置也會有所影響。比如例四。
下面舉例
例一:訪問 /hello/hello,匹配到的是第一個。滿足匹配算法第二點。
location = /hello/hello {
}
location ~* /.*/hello {
}
例二:訪問/hello/hello,匹配到的是第二個。滿足匹配算法第四點。
location /hello/hello {
}
location ~* /.*/hello {
}
例三:訪問/custom/hello,匹配到第一個。滿足匹配算法第五點。
location /custom/hello {
}
location /custom {
}
例四:訪問/custom/hello,匹配到第一個。在前綴匹配長度上,他們一致,在解析正則表達式時,第一個首先被解析,符合要求,這樣儘管第二個也符合要求,但此時正則表達式的解析已經停止了。
location ~ /.*/hello {
}
location ~ /custom/.* {
}
繼續上例,依舊訪問/custom/hello,還是匹配到第一個,原因同上。
location ~ /custom/.* {
}
location ~ /.*/hello {
}
location跳到其它location的情況
一般來說,匹配到一個location後,之後的工作都會在該location下完成。但有幾個特殊的場景將會重新觸發location匹配。比如如下幾個指令。
-
index
index如果用來處理請求,則始終會導致重定向。如果我們將一個精確匹配的location配置爲一個目錄,則可能將其重定向到其它位置。比如下面的配置。
# 當方位 /exact,會被重定向到/index.html,從而重定向到第二個location index index.html; location = /exact { . . . } location / { . . . } # 解決方案是關閉index並開啓autuindex location = /exact { index nothing_will_match; autoindex on; }
-
try_files
該指令告訴Nginx檢查是否存在一組命名的文件或目錄。 最後一個參數可以是Nginx將對其進行內部重定向的URI。考慮下面的例子。
root /var/www/main; location / { try_files $uri $uri.html $uri/ /fallback/index.html; } location /fallback { root /var/www/another; }
這裏,如果來的請求是/balabala,則Nginx會嘗試在/var/www/main下依次嘗試尋找balabala、balabala.html、balabala/等文件或文件夾,如果都沒有則內部重定向到/fallback/index.html。此時會匹配到第二個location塊。
-
rewrite
rewrite指定將匹配的uri重寫成新的uri,並重新匹配新的location。
# 如果uri爲/rewriteme/fallback,則重寫後的uri變成/fallback,會匹配到下面那個location root /var/www/main; location / { rewrite ^/rewriteme/(.*)$ /$1 last; try_files $uri $uri.html $uri/ /fallback/index.html; } location /fallback { root /var/www/another; }
當然在使用return指令發送301、302重定向時,也會發生類似效果,但那是標準重定向,可以看做新的請求,所以他們還是不同的。
-
error_page
error_page類似try_files形成的效果,執行的錯誤頁面路徑可能在另一個location中。
root /var/www/main; location / { error_page 404 /another/whoops.html; } location /another { root /var/www; }
kubernetes ingress
ingress是K8S中的概念,用於將請求路由到指定的服務,本質上是對nginx的包裝。所有配置的ingress都將被轉換成nginx的location塊。
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: test-ingress
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
rules:
- host: test.com
http:
paths:
- path: /foo/.*
backend:
serviceName: test
servicePort: 80
上述配置將被翻譯成如下配置
location ~* "^/foo/.*" {
...
}
本文比較關心的是ingress中配置的路徑優先級。
在Nginx中,正則表達式遵循最先匹配原則,因此爲了更加準確地進行匹配,在寫入nginx配置前,ingress首先會根據路徑的長度倒序排序,然後才寫入nginx配置。
注意喲,注意下面這種情況此時test.com/foo/bar/bar將會匹配第一個location,而不是第二個location,因爲整個ingress開啓了正則表達式。當然,如果需要匹配第二個,可以將正則表達式關閉,從而設置 location = /foo/bar/bar
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: test-ingress-3
annotations:
nginx.ingress.kubernetes.io/use-regex: "true" # 注意這裏
spec:
rules:
- host: test.com
http:
paths:
- path: /foo/bar/bar
backend:
serviceName: test
servicePort: 80
- path: /foo/bar/[A-Z0-9]{3}
backend:
serviceName: test
servicePort: 80
將會被翻譯成
location ~* "^/foo/bar/[A-Z0-9]{3}" {
...
}
location ~* "^/foo/bar/bar" {
...
}
總結
詳細瞭解nignx這些特性,在做網站或接口配置時非常有用。比如有如下需求。
在我開發的項目中,大多數接口均以/admin打頭,但只有一個接口以/swagger開頭,現在出於需要,我要將項目配置到 www.example.com/abc/admin下。
-
在詳細瞭解Nginx配置之前,我的解決方案如下
將接口的admin前綴去除,然後將location爲/abc/admin/下的請求轉發給我的服務。
location /abc/admin/ { proxy_pass http://127.0.0.1:19898/; }
-
熟悉後,我可以在不改動原來前綴的基礎上進行修改。這樣看來,上面的操作相當於把Nginx的開發轉移到了業務代碼中,非常的不好。
location /abc/admin/ { proxy_pass http://127.0.0.1:19898/admin/; } location /abc/admin/swagger/ { proxy_pass http://127.0.0.1:19898/swagger/; }