spring cloud zuul使用記錄(2)路由接入流程以及并發(fā)刷新問題

最近在看spring cloud zuul(版本Finchley.SR1)的源代碼,一不小心還看到了個(gè)bug(我認(rèn)為是哈),更神奇的是,這個(gè)bug一年前已經(jīng)有人提了issue,并提交了PR(竟然搶在我之前了)。但是現(xiàn)在還沒有合并進(jìn)來(lái),7天前被管理員放進(jìn)了icebox,這是什么操作?我不太清楚?是說(shuō)會(huì)拿出來(lái)合并么?還是啥?哪位有經(jīng)驗(yàn)的同學(xué)知道麻煩告訴我。那這整個(gè)事情是怎么樣的呢?這都得從Zuul路由管理與SpringMVC的請(qǐng)求接入說(shuō)起

起源

我們知道在配置zuul property的時(shí)候,當(dāng)配置了一個(gè)route的path之后,zuul就會(huì)自動(dòng)讀取這些路由規(guī)則并進(jìn)行配置。那這一切是怎么做到的呢?我們首先看我們啟用@EnableZuulProxy啟動(dòng)zuul之后spring具體做了什么:

EnableZuulProxy配置類

這個(gè)配置類引入了一個(gè)ZuulProxyMarkerConfiguration

ZuulProxyMarkerConfiguration類

而這個(gè)類只是引入了一個(gè)Marker Bean,通過(guò)find usage我們看到

ZuulProxyAutoConfiguration類

這個(gè)autoConfiguration類是通過(guò)spring.factories來(lái)注入的自動(dòng)配置。而這個(gè)類他繼承了ZuulServerAutoConfiguration:

ZuulServerAutoConfiguration類

這個(gè)類中我們注意的是一個(gè)SimpleRouteLocator,這個(gè)類注入了zuulProperties:

ZuulProperties類

這個(gè)類就是讀取的配置文件中的zuul相關(guān)的properties,那注入這個(gè)properties的SimpleRouteLocator就很有可能是生成route的地方。

SimpleRouteLocator類

SimpleRouteLocator類中的代碼也表明了我的判斷。到這里我們知道了配置是怎么映射到route規(guī)則的,當(dāng)然僅僅這一點(diǎn)遠(yuǎn)遠(yuǎn)不夠,我們知道,當(dāng)我們定義了一個(gè)route規(guī)則之后,我們可以直接請(qǐng)求訪問這個(gè)route的path來(lái)達(dá)到我們想要到達(dá)的serviceId或者url而不需要定義任何controller,這個(gè)spring是如何做到的呢?

引路人

針對(duì)上面的問題,我重新回到之前提到的幾個(gè)配置類,發(fā)現(xiàn)了如下信息:

ZuulController和Mapping配置

我們知道本身Zuul是通過(guò)servlet來(lái)做的入口,而我們上圖看到的這個(gè)ZuulController

ZuulController實(shí)現(xiàn)

我們可以發(fā)現(xiàn)他就是一個(gè)Servlet的包裝,通過(guò)將請(qǐng)求代理給ZuulServlet來(lái)實(shí)現(xiàn)zuul的功能,可見在這個(gè)Controller前面肯定需要一個(gè)組件去把請(qǐng)求forward給它,這個(gè)組件很有可能就是之前看到的ZuulHandlerMapping,因?yàn)樗某跏蓟褂玫搅藌uulController

zuulHandlerMapping繼承關(guān)系與注釋

通過(guò)查看ZuulHandlerMapping繼承關(guān)系和注釋我們看到了他繼承了AbstractUrlHandlerMapping抽象類,熟悉SpringMVC的同學(xué)知道,對(duì)于請(qǐng)求入口或者我們自己編寫的Controller方法,SpringMVC會(huì)生成HandlerMapping示例,DispatcherServlet通過(guò)遍歷spring上下文中已經(jīng)存在的HandlerMapping來(lái)進(jìn)行http請(qǐng)求的查找匹配,執(zhí)行鏈路的組建和請(qǐng)求的執(zhí)行,我給大家列出來(lái)DispatcherServlet中的代碼,具體的調(diào)用鏈路和原理有興趣的同學(xué)可以下來(lái)看看:

DispatcherServlet調(diào)用入口

針對(duì)ZuulHandlerMapping中的代碼,我們目前只需要知道,對(duì)于每次request請(qǐng)求進(jìn)來(lái),dispatcherServlet都會(huì)調(diào)用ZuulHandlerMapping的lookupHandler方法,來(lái)查找是否有合適的zuul route規(guī)則,如果有就將請(qǐng)求導(dǎo)入給ZuulController,那么我們?cè)賮?lái)仔細(xì)看看具體的方法:

lookupHandler實(shí)現(xiàn)

之前一通常規(guī)操作,然后通過(guò)一個(gè)volatile變量dirty判斷目前的route是否有變更,如果有就重新注冊(cè)路由信息并且重置dirty變量為false,最后調(diào)用父類的loopupHandler。dirty變量默認(rèn)為true,就保證在請(qǐng)求進(jìn)來(lái)的時(shí)候肯定會(huì)有一個(gè)初始化的過(guò)程,那我們進(jìn)入registerHandlers方法看看

registerHandlers方法

這里我們就明白了,這個(gè)方法對(duì)當(dāng)前所有的routes信息都調(diào)用父類的registerHandler來(lái)注冊(cè)能處理的path。從而完成了整個(gè)調(diào)用鏈路的匹配與搭建。ZuulHandlerMapping就像是一個(gè)引路人一樣指引每一個(gè)能被zuul處理的request到ZuulController中。一切看起來(lái)都非常美好對(duì)吧。但是善于思考的同學(xué)又會(huì)有新的問題了,dirty只會(huì)在初始化的時(shí)候使用么?routes可以中途刷新么?答案是可以的。

Bad Smell

我注意到ZuulHandlerMapping類中有這樣一個(gè)方法:

setDirty

通過(guò)find usage我們知道這個(gè)方法會(huì)在一些EventListener中被調(diào)用

更新設(shè)置dirty調(diào)用

通過(guò)上述兩份代碼和查閱Spring Cloud Zuul的文檔我知道,如果你想要使得你的RouteLocator能夠可以更新,那就讓你的RouteLocator類實(shí)現(xiàn)RefreshableRouteLocator接口并實(shí)現(xiàn)refresh方法,然后在每次需要更新的時(shí)候向spring 上下文發(fā)布RoutesRefreshedEvent就行了,剩下的一切就交給剛剛看到的代碼和spring做就行了。這一切看上去也很perfect。但是我總覺得哪里不對(duì),感覺聞到了怪怪的味道。這邊再給大家仔細(xì)列一下代碼:

怪怪的味道

當(dāng)一個(gè)更新事件發(fā)起的時(shí)候,setDirty方法會(huì)先設(shè)置dirty為true,然后調(diào)用routeLocator的refresh方法,這沒問題。在一個(gè)請(qǐng)求進(jìn)來(lái)的時(shí)候,會(huì)檢查dirty是否為true,如果有,則重新注冊(cè)path和handler,這似乎也沒問題,還用了double check。但是這兩個(gè)加在一起,是否存在這樣的場(chǎng)景,當(dāng)一次routeLocator refresh的時(shí)間比較長(zhǎng)而這時(shí)候zuul的請(qǐng)求load比較高的時(shí)候,一個(gè)請(qǐng)求進(jìn)來(lái)發(fā)現(xiàn)此時(shí)需要重新注冊(cè)handler,但這是routes信息并沒有完成刷新,或者說(shuō)根本沒有開始刷新,那這時(shí)候注冊(cè)的,還是刷新前的老的數(shù)據(jù),也就是說(shuō),更新之后的路由信息完完全全沒有被注冊(cè)到springmvc的處理鏈路中,整個(gè)網(wǎng)關(guān)并不會(huì)處理新增加的path,或者還會(huì)接入已經(jīng)刪除的path,這是個(gè)bug!歸納起來(lái),就是當(dāng)registerHandler的調(diào)用線程優(yōu)先于routeLocator的refresh的調(diào)用,那么路由數(shù)據(jù)的更新就會(huì)失效并且這是不可恢復(fù)的!我在github上查找有關(guān)dirty的issue,也發(fā)現(xiàn)了下面的記錄

https://github.com/spring-cloud/spring-cloud-netflix/pull/2259

這個(gè)issue跟我描述的基本上一毛一樣,并且也提了PR,也就是本文最開始所提到(有木有哪位老鐵告訴我啥叫icebox?。?。

當(dāng)然BB是不夠的,我下面會(huì)通過(guò)一個(gè)例子來(lái)闡述這個(gè)bug:

證據(jù)

下面所講的代碼都已經(jīng)提交的github:

https://github.com/ro9er/zuul-dirty-bug-sample

首先我們定義一個(gè)RefreshableRouteLocator

RefreshableRouteLocator實(shí)現(xiàn)

這個(gè)實(shí)現(xiàn)其實(shí)很簡(jiǎn)單,用Entiry抽象了路由信息,并且在每次刷新的時(shí)候重新完成Entiry到Route的映射工作,并且實(shí)現(xiàn)了SimpleRouteLocator的getRoutes和getMatchingRoute方法,getRoutes在我們之前看到的ZuulHandlerMapping中registerHandlers中被調(diào)用,用來(lái)注冊(cè)handler信息。getMatchingRoute方法在Spring Cloud Zuul實(shí)現(xiàn)的PreDecorationFilter中被調(diào)用,用來(lái)確定一個(gè)具體的route,并設(shè)置到跳轉(zhuǎn)規(guī)則中,這里就不具體展開了。這里我在refresh方法中注釋了線程sleep10秒的操作,后面我會(huì)打開它。當(dāng)這個(gè)routeLocator初始化的時(shí)候只有一個(gè)/baidu路由規(guī)則跳轉(zhuǎn)到百度

然后我實(shí)現(xiàn)了一個(gè)Controller:

刷新controller方法

這個(gè)controller暴露一個(gè)刷新路由信息,這里我們看到它會(huì)向我之前定義的routeLocator增加一條/163跳轉(zhuǎn)網(wǎng)易的規(guī)則,并且發(fā)出一個(gè)RoutesRefreshedEvent,從而觸發(fā)路由規(guī)則觸發(fā)流程。然后我們來(lái)看看整個(gè)調(diào)用場(chǎng)景:

調(diào)用百度場(chǎng)景
調(diào)用163報(bào)404
調(diào)用刷新路由接口
再次調(diào)用163

通過(guò)上面整個(gè)場(chǎng)景流程我們知道,在在開始啟動(dòng)的時(shí)候,只有/baidu規(guī)則有效,/163會(huì)直接404,在我們刷新路由之后,在此訪問/163,成功跳轉(zhuǎn)到網(wǎng)易,證明我們的刷新機(jī)制是生效了。那么我們現(xiàn)在來(lái)復(fù)現(xiàn)bug,為了讓這個(gè)bug比較容易的復(fù)現(xiàn),我在refresh方法中打開了線程sleep 10s的操作,使得我們的刷新路由操作會(huì)延遲執(zhí)行:

打開線程sleep

再次重復(fù)之前的流程,重復(fù)的步驟我就不貼圖了,我們知道在增加了這個(gè)線程sleep的情況下,我們的refreshRoutes接口會(huì)變慢,當(dāng)我們?cè)谶@個(gè)接口執(zhí)行的過(guò)程中我們調(diào)用一個(gè)/163,會(huì)因?yàn)閐irty重新出發(fā)regsiterHandler,并且返回404(顯然的,因?yàn)楝F(xiàn)在根本沒有增加163這個(gè)規(guī)則),然后我們?cè)趓efreshRoutes返回之后再次執(zhí)行/163:

調(diào)用refresh
第一次調(diào)用163


第二次調(diào)用163

從上述的調(diào)用可以發(fā)現(xiàn),刷新之后新的路由并沒有生效,而且這個(gè)除非你重新調(diào)用一次refresh,不然不可能恢復(fù)。然鵝,就算你調(diào)用refresh,也不一定能夠恢復(fù),因?yàn)橛锌赡芟麓蝦equest進(jìn)來(lái)又把你沖掉了。

解決方案

問題明確了,怎么解決呢?如之前PR中所說(shuō)的,可以把setDirty中的dirty賦值操作放到最后:

dirty放到最后

這個(gè)修改應(yīng)該就能解決這個(gè)問題,但是現(xiàn)在并沒有合并進(jìn)來(lái),還有沒有其他辦法呢?

我的辦法是增加一個(gè)Listener,并且通過(guò)Ordered接口保證第一個(gè)執(zhí)行,在消息處理里面手動(dòng)觸發(fā)refresh,不過(guò)弊端就是refresh會(huì)調(diào)用兩次

增加消息處理

親測(cè)可用。

結(jié)語(yǔ)

到此整個(gè)bug的出現(xiàn)我大概已經(jīng)說(shuō)明清楚了,并且順帶把zuul的handler mapping流程也梳理了一遍,大家有什么問題歡迎留言,希望能跟大家一起交流,共同進(jìn)步。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容