Nginx location 配置踩坑過程分享

NO IMAGE

這是五個小時與一個字元的戰鬥

是的,作為一個程式設計師,你往往發現,有的時候你花費了數小時,數天,甚至數星期來查詢問題,但最終可能只花費了數秒,改動了數行,甚至幾個字元就解決了問題。這次給大家分享一個困擾了我很久,我花了五個小時才查詢出問題原因,最終只新增了一個字元解決了的問題。

問題描述

我們的業務系統比較複雜,但最終提供給使用者的訪問介面比較單一,都是使用 Nginx 來做一個代理轉發,而這個代理轉發,往往需要匹配很多種不同型別的 URL 轉給不同的服務。這就使得我們的 Nginx 配置檔案變得很複雜,粗略估計了下,我們有近20個 upstream,有近60個 location 匹配。這些配置按照模組分佈在不同的檔案中,雖然複雜,但是仍然在我們的努力下執行的良好。直到有一天,有位同事給我反映說偶爾有些 URL 會出現 404 的問題。一開始沒太在意,因為他也說不準是哪一種 URL 才遇到這個問題。

問題查詢

後來,慢慢的查詢,找到了一些規律,一開始只知道是 tomcat 那邊返回 404了,想到 Nginx 都代理給了 tomcat,一開始就懷疑是程式的問題,不會想到是 Nginx。

我開始查詢程式碼的問題,我在本地的開發環境,嘗試了很久,我使用 8080 埠訪問,不論如何都是正確的結果,可是生產環境就是不行。然後我就聽信了某坑友同事的理論,重啟解決 95% 的問題,重灌解決 100%的問題,我嘗試重啟了 tomcat 和 Nginx,依然不行,然後是重灌,你猜結果如何????? ——想啥呢?當然也是不行!

後來就開始懷疑是生產環境和開發環境的差異,去伺服器上訪問 8080 埠,仍然是可以的。可是一經過 Nginx 代理,就不行。這個時候才開始懷疑是 Nginx 出了什麼問題。

Nginx 怎麼會出問題呢,業務系統中 URL 模式 /helloworld/* ,這樣的 URL 我們都是統一處理的。怎麼會出現一些行,一些不行呢。問題表現為 A URL (/helloworld/nn/hello/world)沒問題,而 B URL(/helloworld/ii/hello/world) 有問題。

所以到目前為止,基本可以肯定是 Nginx 的 location 上出了一些問題。

問題解決

因篇幅有限,為了直面本次問題的核心,我不再貼出完整的 Nginx 配置,我簡化此次問題的模型。請看如下 Nginx 配置,這是我們之前的會導致問題的錯誤配置模型。

worker_processes  1;
error_log  logs/error.log;
events {
worker_connections  1024;
}
http {
include       mime.types;
default_type  application/octet-stream;
log_format  main  '$remote_addr - $request_time - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log  logs/access.log main;
sendfile        on;
keepalive_timeout  65;
gzip  on;
server {
listen       80;
server_name  localhost;
location / {
root   html;
index  index.html index.htm;
}
location = /helloworld {
return 602;
}
location /helloworld {
return 603;
}
## 生產環境中如下兩個 location 在另外一個檔案中,通過 include 包含進來
location /ii {
return 604;
}
location ~ /ii/[^\/] /[^\/]  {
return 605;
}
##
location ~ ^/helloworld/(scripts|styles|images).* {
return 606;
}
}
}

注意,這裡有幾點需要說明一下,生產環境的 Nginx 伺服器配置檔案比這裡要複雜很多,而且是按模組分佈在不同的檔案中的。這裡簡化模型後,使用 Http 響應狀態碼 60x 來區分到底被哪個 location 匹配到了。
我針對當時的情況,做了大量嘗試,最終的簡化版本如下:

嘗試1:http://localhost/helloworld ==> 602 符合預期
嘗試2:http://localhost/helloworld/hello ==> 603 符合預期
嘗試3:http://localhost/ii ==> 604 符合預期
嘗試4:http://localhost/ii/oo ==> 604 符合預期
嘗試5:http://localhost/ii/pp/kk ==> 605 符合預期
嘗試6:http://localhost/ii/pp/kk/ll ==> 605 符合預期
嘗試7:http://localhost/helloworld/scripts/aaa.js ==> 606 符合預期
嘗試8:http://localhost/helloworld/ii/hello/world ==> 605 不符合預期,預期為【603】

上面這些嘗試支援讀者自行試驗,Nginx 配置檔案是完整可用的,我本地 Nginx 的版本是1.6.2

問題就在這裡:我這裡是事後,把這些匹配 location 標記成了不同的響應碼,才方便查詢問題。當發現這個不符合預期後,我還是難以理解,為何我一個以 /helloworld 開頭的 URL 會被匹配到 605 這個以 /ii 開頭的 location 裡面來。在當時的生產環境中,以 /ii 的配置統一放在另外一個檔案中,這裡是很難直觀的察覺出來這個 /ii 跟訪問的 URL 裡面的 /ii 的關係。

我不得不重新編譯了 Nginx ,加上了除錯引數,修改配置項,看除錯日誌了。

這裡不再講如何給 Nginx 加除錯的編譯引數,可自行檢視相關文件。修改配置項很簡單,只需要在

error_log  logs/error.log;

後面加上 debug 就可以了。

打出詳細除錯日誌後,訪問

http://localhost/helloworld/ii/hello/world

我得到了這樣的一段日誌(省略掉了前後無用的日誌,只保留有意義的一段):

2015/02/02 15:38:48 [debug] 5801#0: *60 http request line: "GET /helloworld/ii/hello/world HTTP/1.1"
2015/02/02 15:38:48 [debug] 5801#0: *60 http uri: "/helloworld/ii/hello/world"
2015/02/02 15:38:48 [debug] 5801#0: *60 http args: ""
2015/02/02 15:38:48 [debug] 5801#0: *60 http exten: ""
2015/02/02 15:38:48 [debug] 5801#0: *60 http process request header line
2015/02/02 15:38:48 [debug] 5801#0: *60 http header: "User-Agent: curl/7.37.1"
2015/02/02 15:38:48 [debug] 5801#0: *60 http header: "Host: localhost"
2015/02/02 15:38:48 [debug] 5801#0: *60 http header: "Accept: */*"
2015/02/02 15:38:48 [debug] 5801#0: *60 http header done
2015/02/02 15:38:48 [debug] 5801#0: *60 event timer del: 4: 1422862788055
2015/02/02 15:38:48 [debug] 5801#0: *60 rewrite phase: 0
2015/02/02 15:38:48 [debug] 5801#0: *60 test location: "/"
2015/02/02 15:38:48 [debug] 5801#0: *60 test location: "ii"
2015/02/02 15:38:48 [debug] 5801#0: *60 test location: "helloworld"
2015/02/02 15:38:48 [debug] 5801#0: *60 test location: ~ "/ii/[^\/] /[^\/] "
2015/02/02 15:38:48 [debug] 5801#0: *60 using configuration "/ii/[^\/] /[^\/] "
2015/02/02 15:38:48 [debug] 5801#0: *60 http cl:-1 max:1048576
2015/02/02 15:38:48 [debug] 5801#0: *60 rewrite phase: 2
2015/02/02 15:38:48 [debug] 5801#0: *60 http finalize request: 605, "/helloworld/ii/hello/world?" a:1, c:1
2015/02/02 15:38:48 [debug] 5801#0: *60 http special response: 605, "/helloworld/ii/hello/world?"
2015/02/02 15:38:48 [debug] 5801#0: *60 http set discard body
2015/02/02 15:38:48 [debug] 5801#0: *60 posix_memalign: 00007FC3BB816000:4096 @16
2015/02/02 15:38:48 [debug] 5801#0: *60 HTTP/1.1 605
Server: nginx/1.6.2
Date: Mon, 02 Feb 2015 07:38:48 GMT
Content-Length: 0
Connection: keep-alive

可以看到,Nginx 測試了幾次 location 匹配,最終選擇了

~ "/ii/[^\/] /[^\/] "

這個作為最終的匹配項。到這裡問題就完全展現出來了,我們本來的意思,是要以 /ii 開頭,後面有兩個或者更多的 / 分割的 URL 模型才匹配,但是這裡的正規表示式匹配寫的不夠精準,導致了匹配錯誤。正規表示式沒有限制必須從開頭匹配,所以才會匹配到 /helloworld/ii/hello/world 這樣的 URL 。

解決辦法就是在這個正規表示式前面加上 ^ 來強制 URL 必須以 /ii 開頭才能匹配.

/ii/[^\/] /[^\/] 

變成

^/ii/[^\/] /[^\/] 

至此,這個坑被填上了,消耗的是五個小時和一個字元。

相信很多人在寫 Nginx 的location 的時候都會 location ~ /xxx 或者 location /iii 這樣簡單了事,但是我想說的是能儘量精確就儘量精確,否則出現問題的時候,非常難以查詢。

有關 Nginx 的 location 匹配規則,可以檢視: http://nginx.org/en/docs/http/ngx_http_core_module.html

問題總結

這個問題看似簡單,卻也隱含了不少問題,值得我們深思。

計算機或者軟體出的問題往往是確定的,你發現他捉摸不定的時候,往往是沒有觀察到問題點
追蹤一個問題,如果有一個必現方式,一定要緊追不捨,這就是所謂線索
當你實在是找不到問題所在的時候,要懷疑一下之前被自己排除掉的可能性
藉助各個元件的詳細除錯日誌來查詢問題,往往能得到意想不到的效果
程式設計師的價值不是用行數,字數,提交數衡量的!

本文作者: 王振威
文章出自: Coding 官方技術部落格
如需轉載,請註明作者與出處,謝謝