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

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

起源

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

EnableZuulProxy配置類

這個配置類引入了一個ZuulProxyMarkerConfiguration

ZuulProxyMarkerConfiguration類

而這個類只是引入了一個Marker Bean,通過find usage我們看到

ZuulProxyAutoConfiguration類

這個autoConfiguration類是通過spring.factories來注入的自動配置初厚。而這個類他繼承了ZuulServerAutoConfiguration:

ZuulServerAutoConfiguration類

這個類中我們注意的是一個SimpleRouteLocator件蚕,這個類注入了zuulProperties:

ZuulProperties類

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

SimpleRouteLocator類

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

引路人

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

ZuulController和Mapping配置

我們知道本身Zuul是通過servlet來做的入口罪针,而我們上圖看到的這個ZuulController

ZuulController實現(xiàn)

我們可以發(fā)現(xiàn)他就是一個Servlet的包裝,通過將請求代理給ZuulServlet來實現(xiàn)zuul的功能黄伊,可見在這個Controller前面肯定需要一個組件去把請求forward給它泪酱,這個組件很有可能就是之前看到的ZuulHandlerMapping,因為它的初始化使用到了zuulController

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

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

DispatcherServlet調(diào)用入口

針對ZuulHandlerMapping中的代碼吮成,我們目前只需要知道,對于每次request請求進(jìn)來辜梳,dispatcherServlet都會調(diào)用ZuulHandlerMapping的lookupHandler方法粱甫,來查找是否有合適的zuul route規(guī)則,如果有就將請求導(dǎo)入給ZuulController作瞄,那么我們再來仔細(xì)看看具體的方法:

lookupHandler實現(xiàn)

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

registerHandlers方法

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

Bad Smell

我注意到ZuulHandlerMapping類中有這樣一個方法:

setDirty

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

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

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

怪怪的味道

當(dāng)一個更新事件發(fā)起的時候个粱,setDirty方法會先設(shè)置dirty為true古毛,然后調(diào)用routeLocator的refresh方法,這沒問題。在一個請求進(jìn)來的時候稻薇,會檢查dirty是否為true嫂冻,如果有,則重新注冊path和handler塞椎,這似乎也沒問題桨仿,還用了double check。但是這兩個加在一起案狠,是否存在這樣的場景服傍,當(dāng)一次routeLocator refresh的時間比較長而這時候zuul的請求load比較高的時候,一個請求進(jìn)來發(fā)現(xiàn)此時需要重新注冊handler骂铁,但這是routes信息并沒有完成刷新吹零,或者說根本沒有開始刷新,那這時候注冊的拉庵,還是刷新前的老的數(shù)據(jù)灿椅,也就是說,更新之后的路由信息完完全全沒有被注冊到springmvc的處理鏈路中钞支,整個網(wǎng)關(guān)并不會處理新增加的path茫蛹,或者還會接入已經(jīng)刪除的path,這是個bug伸辟!歸納起來麻惶,就是當(dāng)registerHandler的調(diào)用線程優(yōu)先于routeLocator的refresh的調(diào)用,那么路由數(shù)據(jù)的更新就會失效并且這是不可恢復(fù)的信夫!我在github上查找有關(guān)dirty的issue窃蹋,也發(fā)現(xiàn)了下面的記錄

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

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

當(dāng)然BB是不夠的振湾,我下面會通過一個例子來闡述這個bug:

證據(jù)

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

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

首先我們定義一個RefreshableRouteLocator

RefreshableRouteLocator實現(xiàn)

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

然后我實現(xiàn)了一個Controller:

刷新controller方法

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

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

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

打開線程sleep

再次重復(fù)之前的流程阅爽,重復(fù)的步驟我就不貼圖了,我們知道在增加了這個線程sleep的情況下荐开,我們的refreshRoutes接口會變慢付翁,當(dāng)我們在這個接口執(zhí)行的過程中我們調(diào)用一個/163,會因為dirty重新出發(fā)regsiterHandler,并且返回404(顯然的晃听,因為現(xiàn)在根本沒有增加163這個規(guī)則)百侧,然后我們在refreshRoutes返回之后再次執(zhí)行/163:

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


第二次調(diào)用163

從上述的調(diào)用可以發(fā)現(xiàn),刷新之后新的路由并沒有生效能扒,而且這個除非你重新調(diào)用一次refresh佣渴,不然不可能恢復(fù)。然鵝初斑,就算你調(diào)用refresh辛润,也不一定能夠恢復(fù),因為有可能下次request進(jìn)來又把你沖掉了见秤。

解決方案

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

dirty放到最后

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

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

增加消息處理

親測可用舟肉。

結(jié)語

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末整慎,一起剝皮案震驚了整個濱河市脏款,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌裤园,老刑警劉巖撤师,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異拧揽,居然都是意外死亡剃盾,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進(jìn)店門淤袜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痒谴,“玉大人,你說我怎么就攤上這事铡羡』担” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵烦周,是天一觀的道長尽爆。 經(jīng)常有香客問我,道長论矾,這世上最難降的妖魔是什么教翩? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮贪壳,結(jié)果婚禮上饱亿,老公的妹妹穿的比我還像新娘。我一直安慰自己闰靴,他們只是感情好彪笼,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蚂且,像睡著了一般配猫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上杏死,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天泵肄,我揣著相機(jī)與錄音捆交,去河邊找鬼。 笑死腐巢,一個胖子當(dāng)著我的面吹牛品追,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冯丙,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼肉瓦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了胃惜?” 一聲冷哼從身側(cè)響起泞莉,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎船殉,沒想到半個月后鲫趁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡利虫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年饮寞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片列吼。...
    茶點(diǎn)故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖苦始,靈堂內(nèi)的尸體忽然破棺而出寞钥,到底是詐尸還是另有隱情,我是刑警寧澤陌选,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布理郑,位于F島的核電站,受9級特大地震影響咨油,放射性物質(zhì)發(fā)生泄漏您炉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一役电、第九天 我趴在偏房一處隱蔽的房頂上張望赚爵。 院中可真熱鬧,春花似錦法瑟、人聲如沸冀膝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽窝剖。三九已至,卻和暖如春酥夭,著一層夾襖步出監(jiān)牢的瞬間赐纱,已是汗流浹背脊奋。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留疙描,地道東北人诚隙。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像淫痰,于是被迫代替她去往敵國和親最楷。 傳聞我的和親對象是個殘疾皇子捌臊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容