路徑對齊實戰(zhàn))
1. 從一次線上404說起context-path與網關路由的碰撞事情發(fā)生在某個再普通不過的周三下午。我們團隊維護的一套微服務系統(tǒng)剛完成一輪迭代把其中一個核心業(yè)務服務從單體拆成了獨立服務部署到測試環(huán)境后前端同學反饋所有經過網關訪問這個服務的接口全部返回404但繞過網關直接訪問服務實例的端口接口又是通的。這個現象非常典型——直連能通、過網關就404幾乎可以斷定問題出在網關的轉發(fā)環(huán)節(jié)而不是業(yè)務服務本身。我們這套系統(tǒng)用的是 Spring Cloud Gateway 做統(tǒng)一入口服務注冊發(fā)現走的是注冊中心路由配置一部分是靜態(tài)的 YAML一部分是動態(tài)下發(fā)的。出問題的這個服務恰好是唯一一個在application.yml里配置了server.servlet.context-path的服務。排查過程不算復雜但背后牽扯到的知識點卻相當密集網關的斷言Predicate怎么匹配路徑、StripPrefix過濾器怎么裁剪前綴、context-path在服務端是怎么參與路徑解析的、注冊中心里注冊的到底是哪個路徑。這幾個環(huán)節(jié)只要有一個對不上404 就會如約而至。我把這次排查的完整鏈路、每個環(huán)節(jié)的判斷依據、以及最后落地的幾種修復方案整理出來希望能幫到正在被同類問題折磨的同學。這篇文章適合三類人看一是剛接觸 Spring Cloud Gateway、對路由匹配機制還不太熟的同學二是正在做服務拆分、被路徑問題卡住的開發(fā)者三是想系統(tǒng)梳理網關路徑轉發(fā)鏈路的架構同學。我會盡量把每個為什么講透而不是只丟一個結論讓你抄。2. 先搞清楚404到底是誰返回的遇到404第一反應不應該是改配置而是先確認這個404是誰吐出來的。是網關自己沒匹配到路由返回的還是網關匹配到了路由、轉發(fā)給下游服務后下游服務返回的這兩種情況的排查方向完全不同。2.1 網關層404與下游404的區(qū)分方法最直接的辦法是看響應體和響應頭。Spring Cloud Gateway 在找不到匹配路由時默認返回的是一個比較干凈的404響應體通常為空或者只有極簡內容而且不會帶有下游服務特有的響應頭。而下游服務返回的404往往會帶上該服務自己的錯誤結構比如統(tǒng)一的 JSON 錯誤碼、Content-Type是application/json、甚至帶有服務名標識。另一個更可靠的手段是打開網關的日志。把org.springframework.cloud.gateway這個包的日志級別調到DEBUG你會看到類似Route matched或者No RoutePredicateHandlerMapping found這樣的關鍵信息。如果日志里壓根沒有路由匹配成功的記錄那就是網關層沒匹配上如果匹配上了、轉發(fā)了但下游返回404那問題就在下游的路徑解析上。我們當時的情況是網關日志顯示路由匹配成功了請求也確實轉發(fā)到了下游服務但下游返回了404。這就把問題范圍一下子縮小到了轉發(fā)過去的路徑和下游期望的路徑不一致上。2.2 用直連對比法快速定位我習慣用一個笨但極其有效的辦法拿到網關轉發(fā)時實際使用的目標 URL然后手動用這個 URL 去直連下游服務。怎么拿到實際轉發(fā)的 URL在網關日志里DEBUG級別會打印出轉發(fā)目標地址或者臨時加一個全局過濾器把ServerWebExchange里的GATEWAY_REQUEST_URL_ATTR打出來。假設網關配置的路由是/api/order/**轉發(fā)到order-service而order-service配置了context-path: /order。那么網關轉發(fā)過去的路徑可能是/api/order/list但下游服務實際期望的是/order/api/order/list或者/order/list——具體是哪個取決于StripPrefix有沒有配、配了幾位。直連測試時你分別拼出這幾種路徑去請求哪個返回200哪個就是下游真正期望的路徑。這一步做完問題基本就水落石出了。提示直連測試時記得帶上和網關轉發(fā)時一致的請求頭尤其是Host和Content-Type有些服務對Host有校驗會干擾判斷。3. context-path在服務端到底做了什么很多同學對context-path的理解停留在給接口加個前綴但它在 Servlet 容器層面到底怎么參與路徑解析其實直接決定了網關該怎么配。這里必須把原理講清楚否則你只是記住了要加個 StripPrefix換個場景又會懵。3.1 Servlet容器中的contextPath與servletPath在 Servlet 規(guī)范里一個請求的路徑被拆成三部分contextPath、servletPath、pathInfo。假設請求是/order/api/list而應用配置了context-path: /order那么容器解析結果是contextPath /orderservletPath /api/list在 Spring MVC 里通常整體作為映射路徑處理pathInfo一般為空。關鍵點在于Spring MVC 的DispatcherServlet在做 HandlerMapping 時用的是去掉contextPath之后的路徑。也就是說你的 Controller 上寫RequestMapping(/api/list)它匹配的是/api/list而不是/order/api/list。context-path是容器層面的前綴不參與 Controller 的映射匹配。這就解釋了一個常見困惑為什么我配了context-pathController 里卻不用寫這個前綴因為容器已經幫你剝掉了。3.2 網關轉發(fā)時路徑是怎么拼的網關這邊路由配置里的uri指向的是服務實例地址比如lb://order-service。當請求/api/order/list進來網關匹配到路由后默認會把完整的原始路徑轉發(fā)給下游除非你配置了StripPrefix之類的過濾器去裁剪。所以默認情況下下游收到的是/api/order/list。但下游服務配了context-path: /order容器會認為contextPath應該是/order可實際請求路徑開頭是/api對不上。這時候容器會怎么處理它會認為這個請求不屬于本應用的 context直接返回404。這就是我們那次問題的根因——網關轉發(fā)的路徑沒有包含下游的 context-path 前綴導致下游容器拒絕處理。3.3 一個容易混淆的等價關系這里有個等價關系必須記住下游服務配了context-path: /order等價于所有接口的訪問路徑前面都要加/order。網關要做的就是保證轉發(fā)過去的路徑以/order開頭。有兩種達成方式一是路由的Path斷言本身就帶/order前綴轉發(fā)時原樣帶過去二是斷言不帶但通過RewritePath或SetPath過濾器在轉發(fā)前把前綴補上。選哪種取決于你的路由規(guī)劃風格后面會詳細對比。4. 路由斷言、StripPrefix與context-path的三方博弈把這三個東西放在一起看很多404的成因就一目了然了。它們之間的關系可以用一句話概括斷言決定什么樣的請求進來StripPrefix 決定轉發(fā)前砍掉幾段context-path 決定下游要求路徑以什么開頭。三者必須自洽。4.1 StripPrefix的工作原理與計數陷阱StripPrefix1的意思是把請求路徑按/分割后砍掉最前面的1段。注意是段不是字符。/api/order/list按/分割后是[, api, order, list]開頭的空串不算有效段StripPrefix1砍掉api剩下/order/list。計數陷阱在于很多人以為StripPrefix1是砍掉第一個斜杠前的內容其實它砍的是第一個有效路徑段。如果你的斷言是/api/**那StripPrefix1正好砍掉api但如果斷言是/api/v1/**你想砍掉api/v1兩段就得寫StripPrefix2。數錯段數轉發(fā)路徑就會多一段或少一段404 隨之而來。4.2 三種典型配置組合的對照我把常見的配置組合和結果整理成表方便你對照自己的場景斷言 PathStripPrefix下游 context-path下游收到路徑結果/api/order/**0不配/order/api/order/list404前綴不對/api/order/**1/order/order/list200正確/api/order/**2/order/list404砍多了/order/**0/order/order/list200正確/api/**1/order/order/list若原路徑為/api/order/list200正確看這張表你會發(fā)現核心不是 StripPrefix 配幾而是轉發(fā)后的路徑是否以 context-path 開頭。只要滿足這個條件怎么配都行。理解了這一點你就不會再死記context-path 要配 StripPrefix1這種口訣了。4.3 為什么直連能通會誤導人直連測試時你手動輸入的路徑往往已經帶了/order前綴因為你從服務日志或接口文檔里看到的就是這個所以能通。但網關轉發(fā)時用的是另一套路徑拼接邏輯兩者不一致就出現了直連通、過網關404的詭異現象。這不是矛盾而是你測試時用的路徑和網關實際轉發(fā)的路徑根本不是一個。排查時一定要以網關實際轉發(fā)的 URL為準而不是以你腦子里以為的 URL 為準。5. 完整排查鏈路從404到定位根因這一節(jié)我把當時的排查步驟完整還原你可以照著這個順序走一遍基本能覆蓋90%以上的同類問題。5.1 第一步確認404來源打開網關 DEBUG 日志搜索路由匹配記錄。如果沒匹配上檢查斷言的Path是否寫對、路由是否真的加載了動態(tài)路由的話檢查配置中心有沒有下發(fā)成功。如果匹配上了進入下一步。5.2 第二步打印實際轉發(fā)URL臨時加一個全局過濾器優(yōu)先級設低一點Ordered.LOWEST_PRECEDENCE在filter方法里打印exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)。這個屬性里就是網關最終要請求的完整 URL。把它記下來。5.3 第三步直連驗證下游期望路徑用上一步拿到的 URL 的 path 部分直連下游服務實例。如果404說明下游期望的路徑和這個不一樣。這時候去看下游服務的context-path配置以及 Controller 的實際映射路徑反推出下游真正期望的完整路徑。5.4 第四步反推網關配置該怎么改對比網關實際轉發(fā)路徑和下游期望路徑差異在哪就改哪。如果是少了 context-path 前綴要么給斷言加上這個前綴要么用RewritePath補上如果是多了前綴用StripPrefix砍掉。改完重啟網關再走一遍驗證。5.5 第五步回歸測試所有相關路由改完一個服務的路由后別忘了檢查同一網關下其他服務的路由有沒有受影響。尤其是當你調整了全局過濾器或公共路由配置時很容易誤傷。我們當時就因為改了一個StripPrefix的默認值導致另一個服務的路徑被多砍了一段又排查了半小時。注意動態(tài)路由場景下改完配置后要確認配置中心推送成功、網關確實刷新了路由。有些團隊的路由刷新有緩存或延遲改完立刻測可能還是舊配置白白浪費時間。6. 幾種修復方案的取舍與落地定位到根因后修復方案不止一種。選哪種取決于你的路由規(guī)劃風格和團隊約定。我把幾種方案擺出來說說各自的適用場景和坑。6.1 方案一斷言帶context-path前綴不裁剪這是最直觀的方案。路由斷言直接寫成/order/**轉發(fā)時不裁剪下游收到/order/list正好匹配 context-path。優(yōu)點是配置簡單、路徑一目了然缺點是網關對外暴露的路徑帶上了服務內部的前綴如果這個前綴是內部命名、對外不友好就不太合適。另外如果多個服務都想用統(tǒng)一的對外前綴比如都走/api這個方案就不好統(tǒng)一了。6.2 方案二斷言用對外前綴StripPrefix裁剪后補context-path這是最常見的方案。斷言寫/api/order/**StripPrefix1砍掉api剩下/order/list正好匹配。但如果對外前綴有多段比如/api/v1/order/**就要StripPrefix2。這個方案的坑在于段數容易數錯而且一旦對外前綴調整所有相關路由的 StripPrefix 都要跟著改維護成本偏高。6.3 方案三用RewritePath精確重寫RewritePath用正則做路徑重寫最靈活也最精確。比如filters: - RewritePath/api/order/(?segment.*), /order/$\{segment}這個配置把/api/order/xxx重寫成/order/xxx不管后面跟什么都能正確拼接。優(yōu)點是意圖明確、不受段數影響缺點是要寫正則對不熟悉正則的同學有一定門檻而且正則寫錯會導致更難排查的問題。6.4 方案四統(tǒng)一在網關層剝離context-path如果團隊約定所有服務都不配context-path把前綴統(tǒng)一交給網關管理那問題從根上就不存在了。這是最干凈的方案但需要推動所有服務改造存量系統(tǒng)遷移成本高。新項目建議優(yōu)先考慮這種約定能省掉大量路徑對齊的麻煩。我個人在項目里的選擇是新服務一律不配 context-path對外路徑由網關統(tǒng)一規(guī)劃存量服務如果已經配了用 RewritePath 精確重寫而不是用 StripPrefix 數段數。這樣既避免了段數陷阱也讓路徑映射關系在配置里一眼可見。7. 幾個容易忽略的細節(jié)與踩坑記錄排查完這次問題后我又陸續(xù)遇到過幾個相關的坑一并記下來都是文檔里不太會寫、但實際很常見的。7.1 注冊中心里注冊的路徑帶不帶context-path有些注冊中心的健康檢查或服務元數據里會帶上 context-path有些不會。如果你的網關是基于服務實例的元數據來拼路徑的比如自定義了負載均衡策略那注冊的路徑信息就會影響轉發(fā)結果。我們有一次就是因為服務注冊時上報的路徑帶了 context-path而網關又額外補了一次導致路徑里出現了兩個/order直接404。排查時記得看一眼注冊中心里這個服務的元數據。7.2 大小寫與末尾斜杠的隱形差異/Order和/order在默認配置下是不等價的/order/list和/order/list/也可能被下游區(qū)別對待。網關轉發(fā)時如果路徑末尾多了或少了一個斜杠下游的路徑匹配就可能失敗。這類問題在日志里看起來路徑明明一樣但就是404非常折磨人。建議在網關層加一個規(guī)范化過濾器統(tǒng)一處理末尾斜杠和大小寫。7.3 全局過濾器順序對路徑的影響如果你同時用了StripPrefix和RewritePath它們的執(zhí)行順序會影響最終路徑。過濾器是按Order排序執(zhí)行的順序不對可能先重寫再裁剪結果完全不是你想要的。排查時把相關過濾器的Order值打出來確認執(zhí)行順序符合預期。7.4 本地能通、測試環(huán)境不通的環(huán)境差異本地開發(fā)時很多人圖省事不配 context-path或者用不同的路由配置導致本地一切正常一上測試環(huán)境就404。這類問題的根源是環(huán)境間配置不一致。建議把路由配置納入版本管理各環(huán)境用同一份基礎配置只覆蓋必要的差異項減少這類環(huán)境玄學。8. 把路徑對齊這件事做成規(guī)范踩過幾次坑之后我在團隊里推了一套簡單的路徑規(guī)范執(zhí)行下來同類問題基本絕跡了。核心就三條第一對外路徑統(tǒng)一由網關規(guī)劃服務內部不感知對外前綴第二新服務不配 context-path如果非要配必須在服務文檔里顯式標注并在網關路由配置里加注釋說明第三所有涉及路徑裁剪或重寫的路由必須寫清楚轉發(fā)前路徑和轉發(fā)后路徑的對照注釋方便后來人一眼看懂。這三條看起來簡單但真正落地后路徑相關的404從每月好幾次降到了幾乎為零。路徑問題本質上不是技術難題而是約定和溝通問題——只要轉發(fā)前后的路徑關系在配置里寫得明明白白誰來看都不會懵。最后分享一個我常用的小技巧在網關里加一個只在測試環(huán)境生效的調試端點輸入任意路徑返回它會匹配到哪條路由、最終轉發(fā)成什么 URL。這個端點幫我們省下了大量改配置-重啟-測試的循環(huán)時間定位路徑問題的效率提升非常明顯。如果你也在維護網關強烈建議做一個類似的工具投入很小回報很大。