spring cloud zuul使用記錄(2)路由接入流程以及併發重新整理問題
最近在看spring cloud zuul(版本Finchley.SR1)的原始碼,一不小心還看到了個bug(我認為是哈),更神奇的是,這個bug一年前已經有人提了issue,並提交了PR(竟然搶在我之前了)。但是現在還沒有合併進來,7天前被管理員放進了icebox,這是什麼操作?我不太清楚?是說會拿出來合併麼?還是啥?哪位有經驗的同學知道麻煩告訴我。那這整個事情是怎麼樣的呢?這都得從Zuul路由管理與SpringMVC的請求接入說起
起源
我們知道在配置zuul property的時候,當配置了一個route的path之後,zuul就會自動讀取這些路由規則並進行配置。那這一切是怎麼做到的呢?我們首先看我們啟用@EnableZuulProxy啟動zuul之後spring具體做了什麼:
這個配置類引入了一個ZuulProxyMarkerConfiguration
而這個類只是引入了一個Marker Bean,通過find usage我們看到
這個autoConfiguration類是通過spring.factories來注入的自動配置。而這個類他繼承了ZuulServerAutoConfiguration:
這個類中我們注意的是一個SimpleRouteLocator,這個類注入了zuulProperties:
這個類就是讀取的配置檔案中的zuul相關的properties,那注入這個properties的SimpleRouteLocator就很有可能是生成route的地方。
SimpleRouteLocator類中的程式碼也表明了我的判斷。到這裡我們知道了配置是怎麼對映到route規則的,當然僅僅這一點遠遠不夠,我們知道,當我們定義了一個route規則之後,我們可以直接請求訪問這個route的path來達到我們想要到達的serviceId或者url而不需要定義任何controller,這個spring是如何做到的呢?
引路人
針對上面的問題,我重新回到之前提到的幾個配置類,發現瞭如下資訊:
我們知道本身Zuul是通過servlet來做的入口,而我們上圖看到的這個ZuulController
我們可以發現他就是一個Servlet的包裝,通過將請求代理給ZuulServlet來實現zuul的功能,可見在這個Controller前面肯定需要一個元件去把請求forward給它,這個元件很有可能就是之前看到的ZuulHandlerMapping,因為它的初始化使用到了zuulController
通過檢視ZuulHandlerMapping繼承關係和註釋我們看到了他繼承了AbstractUrlHandlerMapping抽象類,熟悉SpringMVC的同學知道,對於請求入口或者我們自己編寫的Controller方法,SpringMVC會生成HandlerMapping示例,DispatcherServlet通過遍歷spring上下文中已經存在的HandlerMapping來進行http請求的查詢匹配,執行鏈路的組建和請求的執行,我給大家列出來DispatcherServlet中的程式碼,具體的呼叫鏈路和原理有興趣的同學可以下來看看:
針對ZuulHandlerMapping中的程式碼,我們目前只需要知道,對於每次request請求進來,dispatcherServlet都會呼叫ZuulHandlerMapping的lookupHandler方法,來查詢是否有合適的zuul route規則,如果有就將請求匯入給ZuulController,那麼我們再來仔細看看具體的方法:
之前一通常規操作,然後通過一個volatile變數dirty判斷目前的route是否有變更,如果有就重新註冊路由資訊並且重置dirty變數為false,最後呼叫父類的loopupHandler。dirty變數預設為true,就保證在請求進來的時候肯定會有一個初始化的過程,那我們進入registerHandlers方法看看
這裡我們就明白了,這個方法對當前所有的routes資訊都呼叫父類的registerHandler來註冊能處理的path。從而完成了整個呼叫鏈路的匹配與搭建。ZuulHandlerMapping就像是一個引路人一樣指引每一個能被zuul處理的request到ZuulController中。一切看起來都非常美好對吧。但是善於思考的同學又會有新的問題了,dirty只會在初始化的時候使用麼?routes可以中途重新整理麼?答案是可以的。
Bad Smell
我注意到ZuulHandlerMapping類中有這樣一個方法:
通過find usage我們知道這個方法會在一些EventListener中被呼叫
通過上述兩份程式碼和查閱Spring Cloud Zuul的文件我知道,如果你想要使得你的RouteLocator能夠可以更新,那就讓你的RouteLocator類實現RefreshableRouteLocator介面並實現refresh方法,然後在每次需要更新的時候向spring 上下文釋出RoutesRefreshedEvent就行了,剩下的一切就交給剛剛看到的程式碼和spring做就行了。這一切看上去也很perfect。但是我總覺得哪裡不對,感覺聞到了怪怪的味道。這邊再給大家仔細列一下程式碼:
當一個更新事件發起的時候,setDirty方法會先設定dirty為true,然後呼叫routeLocator的refresh方法,這沒問題。在一個請求進來的時候,會檢查dirty是否為true,如果有,則重新註冊path和handler,這似乎也沒問題,還用了double check。但是這兩個加在一起,是否存在這樣的場景,當一次routeLocator refresh的時間比較長而這時候zuul的請求load比較高的時候,一個請求進來發現此時需要重新註冊handler,但這是routes資訊並沒有完成重新整理,或者說根本沒有開始重新整理,那這時候註冊的,還是重新整理前的老的資料,也就是說,更新之後的路由資訊完完全全沒有被註冊到springmvc的處理鏈路中,整個閘道器並不會處理新增加的path,或者還會接入已經刪除的path,這是個bug!歸納起來,就是當registerHandler的呼叫執行緒優先於routeLocator的refresh的呼叫,那麼路由資料的更新就會失效並且這是不可恢復的!我在github上查詢有關dirty的issue,也發現了下面的記錄
https://github.com/spring-cloud/spring-cloud-netflix/pull/2259
這個issue跟我描述的基本上一毛一樣,並且也提了PR,也就是本文最開始所提到(有木有哪位老鐵告訴我啥叫icebox啊)。
當然BB是不夠的,我下面會通過一個例子來闡述這個bug:
證據
下面所講的程式碼都已經提交的github:
https://github.com/ro9er/zuul-dirty-bug-sample
首先我們定義一個RefreshableRouteLocator
這個實現其實很簡單,用Entiry抽象了路由資訊,並且在每次重新整理的時候重新完成Entiry到Route的對映工作,並且實現了SimpleRouteLocator的getRoutes和getMatchingRoute方法,getRoutes在我們之前看到的ZuulHandlerMapping中registerHandlers中被呼叫,用來註冊handler資訊。getMatchingRoute方法在Spring Cloud Zuul實現的PreDecorationFilter中被呼叫,用來確定一個具體的route,並設定到跳轉規則中,這裡就不具體展開了。這裡我在refresh方法中註釋了執行緒sleep10秒的操作,後面我會開啟它。當這個routeLocator初始化的時候只有一個/baidu路由規則跳轉到百度
然後我實現了一個Controller:
這個controller暴露一個重新整理路由資訊,這裡我們看到它會向我之前定義的routeLocator增加一條/163跳轉網易的規則,並且發出一個RoutesRefreshedEvent,從而觸發路由規則觸發流程。然後我們來看看整個呼叫場景:
通過上面整個場景流程我們知道,在在開始啟動的時候,只有/baidu規則有效,/163會直接404,在我們重新整理路由之後,在此訪問/163,成功跳轉到網易,證明我們的重新整理機制是生效了。那麼我們現在來複現bug,為了讓這個bug比較容易的復現,我在refresh方法中開啟了執行緒sleep 10s的操作,使得我們的重新整理路由操作會延遲執行:
再次重複之前的流程,重複的步驟我就不貼圖了,我們知道在增加了這個執行緒sleep的情況下,我們的refreshRoutes介面會變慢,當我們在這個介面執行的過程中我們呼叫一個/163,會因為dirty重新出發regsiterHandler,並且返回404(顯然的,因為現在根本沒有增加163這個規則),然後我們在refreshRoutes返回之後再次執行/163:
從上述的呼叫可以發現,重新整理之後新的路由並沒有生效,而且這個除非你重新呼叫一次refresh,不然不可能恢復。然鵝,就算你呼叫refresh,也不一定能夠恢復,因為有可能下次request進來又把你沖掉了。
解決方案
問題明確了,怎麼解決呢?如之前PR中所說的,可以把setDirty中的dirty賦值操作放到最後:
這個修改應該就能解決這個問題,但是現在並沒有合併進來,還有沒有其他辦法呢?
我的辦法是增加一個Listener,並且通過Ordered介面保證第一個執行,在訊息處理裡面手動觸發refresh,不過弊端就是refresh會呼叫兩次
親測可用。
結語
到此整個bug的出現我大概已經說明清楚了,並且順帶把zuul的handler mapping流程也梳理了一遍,大家有什麼問題歡迎留言,希望能跟大家一起交流,共同進步。
相關文章
- Spring Cloud Zuul記錄介面響應資料SpringCloudZuul
- Spring cloud(5)-路由閘道器(Zuul)SpringCloud路由Zuul
- 8、Spring Cloud ZuulSpringCloudZuul
- springcloud學習筆記(六)Spring Cloud ZuulSpringGCCloud筆記Zuul
- Spring Cloud 之 Zuul.SpringCloudZuul
- Spring Cloud Zuul API服務閘道器之請求路由SpringCloudZuulAPI路由
- 記錄開發過程一個路由問題路由
- Spring Cloud 專題之四:Zuul閘道器SpringCloudZuul
- Spring Cloud Zuul 閘道器SpringCloudZuul
- spring cloud微服務分散式雲架構Spring Cloud ZuulSpringCloud微服務分散式架構Zuul
- Spring相關問題記錄Spring
- Spring Cloud Zuul中使用Swagger彙總API介面文件SpringCloudZuulSwaggerAPI
- Spring Cloud Zuul 閘道器(一)SpringCloudZuul
- (十六) 整合spring cloud雲架構 -使用spring cloud Bus重新整理配置SpringCloud架構
- Spring Cloud Gateway 路由轉發之After(Before)路由斷言工廠使用SpringCloudGateway路由
- vue2問題記錄Vue
- Spring Cloud Gateway 整合Eureka路由轉發SpringCloudGateway路由
- idea 使用問題記錄Idea
- craco使用問題記錄
- cJSON使用問題記錄JSON
- Spring Cloud Eureka 學習記錄SpringCloud
- spring cloud gateway 原始碼解析(2)動態路由SpringCloudGateway原始碼路由
- 記錄在使用Django開發過程中遇到的問題No.2Django
- ElasticSearch 文件併發處理以及文件路由Elasticsearch路由
- thinkphp6 強制路由不生效問題 以及Url路由去掉應用目錄PHP路由
- 使用ffmpeg合併影片檔案的一些問題記錄
- 最全面的改造Zuul閘道器為Spring Cloud Gateway(包含Zuul核心實現和Spring Cloud Gateway核心實現)ZuulSpringCloudGateway
- 線上賬務系統餘額併發更新問題記錄
- Spring Cloud 2021.0.1 移除了Hystrix、Zuul等Netflix元件SpringCloudZuul元件
- Spring Cloud正式移除Hystrix、Zuul等Netflix OSS元件SpringCloudZuul元件
- Spring Cloud Zuul與閘道器中介軟體SpringCloudZuul
- Spring Cloud Stream事件路由 - spring.ioSpringCloud事件路由
- Spring Data Redis兩個問題:記憶體洩露和併發 - europaceSpringRedis記憶體洩露
- composer使用常見問題記錄
- weex使用中的問題記錄
- Django路由使用問題Django路由
- Spring Cloud OpenFeign呼叫流程SpringCloud
- spring cloud構建網際網路分散式微服務雲平臺-路由閘道器(zuul)SpringCloud分散式微服務路由Zuul