使用Spring Cloud的openFeign元件踩坑紀實

背景

2021。05。25 晚上,剛要下班回家,突然被拉到一個群裡,說是閘道器有問題,接入的一個應用介面死活註冊不上去,新業務無法使用,而且業務方說已經發布過好幾次都不生效,但是同一個應用的其他介面卻可以正常註冊。聽起來還挺詭異的,想著重啟大法,重啟了下閘道器應用(其實閘道器好久沒有迭代了,挺穩定的)。結果群裡更是炸鍋了,說原來可以用的介面也沒有了,業務完全停滯(小心臟砰砰直跳)。這下搞大了,放下小書包,開始查問題。。。

解決過程

其實剛開始完全沒有頭緒,閘道器沒有做任何改動,只是重啟應用,怎麼會導致業務方原來可以正常使用的介面也無法註冊呢?但明顯的肯定是業務方改動引起的,因為其他業務方都可以正常使用閘道器。於是分頭行動,我去查詢業務介面無法註冊閘道器的原因,業務開發同學檢視最近做過哪些變動。

因為無法註冊的問題是必現,所以我在本地除錯介面註冊閘道器程式碼,發現他們用到了 spring-cloud 套件,聯想起不久前還幫他們查過一個應用無法註冊 bean 導致啟動失敗的案例,初步判斷這個事件也跟他們使用 spring-cloud 套件有關。這個懷疑得到了開發的認可,他們確實在二個月前就使用了 spring-cloud 的 openFeign 元件,可一直相安無事呀(後來發現自從引入了 openFeign 元件後,新介面就沒註冊成功過,:sweat:),為啥閘道器重啟就導致舊介面也無法使用呢?

因為事態緊急,當務之急是恢復服務,可閘道器重啟已經不起作用了,該如何處理呢?這時開發有了一個大膽的想法,因為閘道器重啟前舊介面是正常的,那麼只要程式碼業務程式碼撤銷對 openFeign 的使用,釋出上去應該能正常使用,而為了保證業務邏輯,再把程式碼恢復到使用 openFeign 再次釋出,就能將狀態恢復到閘道器重啟前的樣子。我快速將該思路理了下,又想起了閘道器有使用到本地快取來儲存拉取到的介面服務列表,跟重啟閘道器導致的介面失效原因吻合,於是同意了他的做法。最終在經歷了兩輪發布-回滾程式碼-再發布,其中一個應用終於恢復到了閘道器重啟前狀態,業務反饋部分恢復。於是再同樣操作把受影響的另一個應用也處理完,業務終於恢復到新介面上線前的狀態,才稍微鬆了一口氣。此時距問題發生已經過去了半個小時左右。

但是此時真正原因並沒有找到,只是有了初步思路。於是群裡回覆相關人員:新專案中的一個依賴元件,和閘道器有衝突,導致服務註冊不上。臨時解決方案:去掉這個元件後,觸發服務正常註冊。此時已經晚上十點半了,具體原因只能明天詳查了(:flushed:)。

真相大白

第二天一大早來公司,想著儘快解決這個遺留問題。於是開始除錯,不得不說,業務引入了 spring-cloud 後,除錯鏈路變得更加複雜,尤其是使用了 openFeign 元件,不知道又做了啥么蛾子。在觸發註冊介面的 ServivceBeanExportedEvent 監聽器中,總是獲取不到已經初始化好的 dubbo bean。經過多次溯源,發現業務方使用了 openFeign 元件後,整個應用上下文變成了如下圖所示:

使用Spring Cloud的openFeign元件踩坑紀實

另外,在除錯過程中,很詭異的發現

ContextRefreshedEvent

被提早觸發了(該業務 Bean 沒有完成初始化的情況下)。最終,在跟蹤 openFeign 元件初始化中找到端倪:

使用Spring Cloud的openFeign元件踩坑紀實

原來在初始化 openFeign 元件的最後,會呼叫 SubContext 的 refresh()操作,最終會觸發 SubContext 發出

ContextRefreshedEvent

事件。可問題是,子 Context 發出的事件怎麼會也觸發父 Context 發出類似事件呢?原來這裡還有個知識點,在 Spring 框架中,事件(Event)是會沿著 Context 層次向上傳播(類似 Dom 模型中的事件冒泡傳播),程式碼如下:

使用Spring Cloud的openFeign元件踩坑紀實

再聯絡到 dubbo 介面匯出服務依賴的

ContextRefreshedEvent

以及 網關注冊業務介面所依賴的

ServiceBeanExportedEvent

(如下圖所示):

使用Spring Cloud的openFeign元件踩坑紀實

使用Spring Cloud的openFeign元件踩坑紀實

使用Spring Cloud的openFeign元件踩坑紀實

整個事件的起因就清楚了, 根本原因:專案依賴的 spring-cloud-openfeign 元件導致 dubbo 介面無法正常註冊到閘道器。 簡單來說就是自 Context 發出初始化完成事件,進而引發父 Context 也發出相同事件,而父 Context 此時並沒有真正初始化完成。詳細解釋:

大致依賴關係如下:(基本前提是 spring 框架在釋出事件時,會以冒泡方式沿層次架構上報)

1。 dubbo 介面 export 需要等待

ContextRefreshedEvent

出現,export 完成後發

ServiceBeanExportedEvent

2。 閘道器元件依賴的 dubbo-rest 元件會等待

ServivceBeanExportedEvent

,然後上傳 dubbo 介面資訊到閘道器。

3。專案依賴的 openFeign 元件在初始化時會生成一個新的 ApplicationContext,以當前存在的 ApplicationContext 為父 Context,形成層次關係。

4。openFeign 元件初始化完成後會進行 Context。refresh(),該操作最後會觸發

ContextRefreshedEvent

事件,進而會觸發父 Context 發出

ContextRefreshedEvent

事件,導致 dubbo 介面提前初始化,引發錯誤,因為此時閘道器 Bean 元件尚未初始化完成,無法完成註冊業務介面。

後續

找出事件的真正原因後,就面臨著給出解決方案的問題。從上面初始化鏈路來看,dubbo 服務初始化的時候,確實是 parentContext 發出的

ContextRefreshedEvent

,只不過是由於子 Context 發出而冒泡產生(事件源還是子 Context)。所以要解決這個問題,一個辦法就是修改 spring 框架增加是否冒泡引數,另一個就是 dubbo export 時判斷 Event 事件源是否為其所屬的 Context 發出,兩種方式感覺都會對框架造成影響,因為一個是要修改 dubbo 框架的服務匯出判斷邏輯,一個是 spring 框架的內建邏輯,都不太好處理。

最後我們採用的是,用 @Lazy 註解將 feignClient 的初始化延遲至使用時進行,因為這時其他 Context 下的 bean 都已經初始化完成,不會有上述提前初始化的問題。

github 上也有人遇到類似的問題,大家討論的方案也都差不多,在 dubbo 官方不打算加入對 event 冒泡做支援的情況下,可以透過

FeignClientInterface client = applicationContext。get(FeignClientInterface。class

複製程式碼

這樣的方式獲取 feignClient 例項來規避提前初始化問題(其實效果跟 @Lazy 一樣)。

最後貼上一張討論截圖:

使用Spring Cloud的openFeign元件踩坑紀實

誠如網友所提出的方法,即使判斷 event 來源自 export 服務也只能解決 dubbo 一個元件的問題,對於其他依賴於

ContextRefreshedEvent

的元件也存在同樣的問題,總不能每一個元件都自己修改一遍吧(:joy:)。我覺得還是提議 spring 框架允許設定是否允許事件冒泡來得更靠譜,這樣那些靠生成子 Context 存活的元件,也有更多的操作空間(:smirk::smirk:)。

原文連結:https://xie。infoq。cn/article/71dc2fcd5f0a5360b0f017c8a