nginx中一個請求匹配到多個location時的優先順序問題,馬失前蹄了

2023-10-14 18:00:23

背景

為什麼講這麼小的一個問題呢?因為今天在進行系統上線的時候遇到了這個問題。

這次的上線動作還是比較大的,由於組織架構拆分,某個接入層服務需要在兩個部門各自獨立部署,以避免頻繁的跨部門溝通,提升該接入層服務的變更效率。

該接入層服務之前是使用cookie + 記憶體session機制的,這次要獨立部署,首先是將這種記憶體session機制改成分散式對談(使用redis),總之,就是做成無狀態的。

再其次,就是將原來的流量閘道器nginx,升級成為openresty。openresty使用lua程式碼,判斷請求應該分發到我們部門的接入層服務,還是另一個部門的接入層服務。

升級成openresty,這塊涉及到兩件事情,一個是openresty的安裝,再一個是修改了原來的nginx.conf。今天升級後,大部分業務驗證ok,唯獨兩三個業務報錯,由於是app報錯,所以是要了對方的使用者名稱密碼在我手機上登入,在電腦上charles抓包,發現報錯的原因,竟然是個這。

處理過程

先給大家看下,nginx中原始的設定:

location ~ (^/(cgi-bin|servlet|chart)/|\.jsp$)
{
    proxy_pass http://backendServer;
    include proxy.conf;
}

這個location會匹配/servlet/json這樣的請求,我們這次就是對這個請求做了改造,用lua判斷應該反向代理到什麼地方,如下:

我們最終的openresty(就是增強版本的ng,組態檔都是基本相容的)設定大概如下:

location ~ /servlet/json {
    set $target_url '';

    rewrite_by_lua_block {
    	...
    	proxy_pass http://$target_url;
    }
}

location /Api/   這個是之前就有的,本次沒動
{
    proxy_pass http://ApiGatewayServer/;
    include proxy.conf;
}
location ~ (^/(cgi-bin|chart)/|\.jsp$)
{
    proxy_pass http://info-tech-department-upstream;
    include proxy.conf;
}

在我們上述這個設定裡,http://www.test.com/servlet/json 這樣的請求,就會匹配上location /servlet/jsonhttp://www.test.com/Api 這樣的請求,就會匹配上location /Api,但是,我抓包後,發現竟然報錯的請求長這樣:

http://www.test.com/Api/servlet/json

而檢視openresty紀錄檔,發現其匹配上了location /servlet/json,而不是預期的location /Api/,這自然就是問題所在了。

http://www.test.com/Api/servlet/json 這樣一個請求,能匹配上下面這個location,我覺得正常:

location /Api/   這個是之前就有的,本次沒動
{
    proxy_pass http://ApiGatewayServer/;
    include proxy.conf;
}

但是,竟然也匹配上了下面這個location:

location ~ /servlet/json {
    set $target_url '';

    rewrite_by_lua_block {
    	...
    	proxy_pass http://$target_url;
    }
}

此時,我才開始搜尋,這個 location 後面的 ~ 符號的確切意思,我知道這個表示正則,但是沒想到,這種請求路徑/Api/servlet/json包含了/servlet/json也能匹配上。

我剛開始以為是這種匹配上了多個,那我是不是換下順序就好了,把/Api那個location放到了檔案最前面:

location /Api/   這個是之前就有的,本次沒動
{
    proxy_pass http://ApiGatewayServer/;
    include proxy.conf;
}

location ~ /servlet/json {
    set $target_url '';

    rewrite_by_lua_block {
    	...
    	proxy_pass http://$target_url;
    }
}

結果還是沒效果。

沒效果的話,我最終解決的辦法就是,修改location ~ /servlet/json為只匹配/servlet/json開頭的那些請求。

最終的改動如下:

// ^在正則中,一般表示匹配一行的開頭,所以,我這裡加了^
location ~ ^/servlet/json {

}

終於ok了。

說起來,確實是對原nginx的組態檔遷移出的這個問題,人家以前是:

location ~ (^/(cgi-bin|servlet|chart)/|\.jsp$)
{
    proxy_pass http://backendServer;
    include proxy.conf;
}

這個是匹配/cgi-bin、/servlet、/chart開頭的請求,或者是jsp結尾的請求,我一遷移,就把意思整錯了。

那這塊的匹配機制到底是怎樣的呢?

location匹配機制

官網:

https://nginx.org/en/docs/http/ngx_http_core_module.html

語法如下:

Syntax:	location [ = | ~ | ~* | ^~ ] uri { ... }
location @name { ... }
Default:	—
Context:	server, location

按檔案的說法,location 和 uri之間,可以什麼都沒有,如我們上面的:

location /Api/   

這種呢,就算是字首匹配。

比如,

location / {
    [ configuration B ]
}

/index.html 這種url就可以匹配。

當然,也可以在location和uri中間加如下幾種符號:

  • =

    完全匹配,比如,

    location = / {
        [ configuration A ]
    }
    

​ 只能匹配「/」 這個請求,其他請求都不能匹配,這個優先順序最高

  • ~ (uri部分為:大小寫敏感的正則)或者 ~* (uri部分:大小寫不敏感的正則)

    這種就是正則匹配,也就是我們前面的

    location ~ /servlet/json {
    

    這種,居然就匹配上了/Api/servlet/json,我不是很理解,但大家要謹慎。好好學習下這塊的正規表示式怎麼寫。

  • ^~

    這種,一會再講。

匹配邏輯:

首先,對uri進行normalize,也就是,比如url有特殊字元的話,一般瀏覽器會自動編碼成%XX這種,另外,可能url中也有相對路徑,或者有重複的斜槓,都要處理。

The matching is performed against a normalized URI, after decoding the text encoded in the 「%XX」 form, resolving references to relative path components 「.」 and 「..」, and possible compression of two or more adjacent slashes into a single slash.

接下來,nginx首先會找出整個server塊中,字首匹配的所有location(就是location和uri中間啥都不加的那種),然後挨個匹配,找出最長字首匹配的那個location,在我們前面的例子中,就會找到:

location /Api/  

但是,找到了之後,不會結束;還要繼續找正則型別的location,這次就是按照檔案的順序,從上到下找,

如果找到了,直接使用;如果沒找到,則使用最長字首的那個。

這次,在我們的例子中,就會找到如下部分,且直接使用這個location,不再繼續找。

location ~ /servlet/json {
    set $target_url '';

    rewrite_by_lua_block {
    	...
    	proxy_pass http://$target_url;
    }
}

所以,這裡的邏輯就是:

1、先找 = 這種完全匹配的,找到就結束;
2、開始找字首匹配這種的,沒找到就算了,找到了也只是做個標記,把最長字首那個location記錄下來,假設為location A;
3、開始找正則這種型別的location,從上到下找,找到就直接停止,就用這個;沒找到就繼續找下一個正則型別;如果最終,完全沒找到正則型別的,就用第二步裡找到的location A

當然了,對於這個機制,有個小例外,就是有一種符號,可以打破這種機制:

^~

這個符號加在字首型別的location上,如果最長字首的那個location,加了這個符號,直接結束,不找正則型別的了。原文:

If the longest matching prefix location has the 「^~」 modifier then regular expressions are not checked.

這個符號,感覺很容易誤用,一開始沒研究之前,我以為^是一行的開頭的意思,危險。

我以前,以為字首這種優先順序很高,沒想到,比正則要低,被正則壓著打啊。

推薦工具

https://nginx.viraptor.info/

location ~ /servlet/json {
	proxy_pass http://localhost:8082;
}
location /Api/
{
	proxy_pass http://localhost:8083;
}

這邊有個網站,可以貼設定進去,檢測到底匹配上哪個:

參考資料

大家也可以看下參考檔案:

https://stackoverflow.com/questions/5238377/nginx-location-priority