隨着瓜子業務的不斷髮展,系統規模在逐漸擴大,目前在瓜子的私有云上已經運行着數百個 Dubbo 應用,上千個 Dubbo 實例。瓜子各部門業務迅速發展,版本沒有來得及統一,各個部門都有本身的用法。隨着第二機房的建設,Dubbo 版本統一的需求變得愈加迫切。幾個月前,公司發生了一次與 Dubbo 相關的生產事故,成爲了公司 基於社區 Dubbo 2.7.3 版本升級的誘因。git
接下來,我會從此次線上事故開始,講講咱們這段時間所作的 Dubbo 版本升級的歷程以及咱們規劃的 Dubbo 後續多機房的方案。github
事故背景spring
在生產環境,瓜子內部各業務線共用一套zookeeper集羣做爲dubbo的註冊中心。2019年9月份,機房的一臺交換機發生故障,致使zookeeper集羣出現了幾分鐘的網絡波動。在zookeeper集羣恢復後,正常狀況下dubbo的provider應該會很快從新註冊到zookeeper上,但有一小部分的provider很長一段時間沒有從新註冊到zookeeper上,直到手動重啓應用後才恢復註冊。apache
排查過程網絡
首先,咱們統計了出現這種現象的dubbo服務的版本分佈狀況,發如今大多數的dubbo版本中都存在這種問題,且發生問題的服務比例相對較低,在github中咱們也未找到相關問題的issues。所以,推斷這是一個還沒有修復的且在網絡波動狀況的場景下偶現的問題。session
接着,咱們便將出現問題的應用日誌、zookeeper日誌與dubbo代碼邏輯進行相互印證。在應用日誌中,應用重連zookeeper成功後provider馬上進行了從新註冊,以後便沒有任何日誌打印。而在zookeeper日誌中,註冊節點被刪除後,並無從新建立註冊節點。對應到dubbo的代碼中,只有在FailbackRegistry.register(url)
的doRegister(url)
執行成功或線程被掛起的狀況下,才能與日誌中的狀況相吻合。架構
public void register(URL url) { super.register(url); failedRegistered.remove(url); failedUnregistered.remove(url); try { // Sending a registration request to the server side doRegister(url); } catch (Exception e) { Throwable t = e; // If the startup detection is opened, the Exception is thrown directly. boolean check = getUrl().getParameter(Constants.CHECK_KEY, true) && url.getParameter(Constants.CHECK_KEY, true) && !Constants.CONSUMER_PROTOCOL.equals(url.getProtocol()); boolean skipFailback = t instanceof SkipFailbackWrapperException; if (check || skipFailback) { if (skipFailback) { t = t.getCause(); } throw new IllegalStateException("Failed to register " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t); } else { logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t); } // Record a failed registration request to a failed list, retry regularly failedRegistered.add(url); } }
在繼續排查問題前,咱們先普及下這些概念:dubbo默認使用curator做爲zookeeper的客戶端,curator與zookeeper是經過session維持鏈接的。當curator重連zookeeper時,若session未過時,則繼續使用原session進行鏈接;若session已過時,則建立新session從新鏈接。而ephemeral節點與session是綁定的關係,在session過時後,會刪除此session下的ephemeral節點。app
繼續對doRegister(url)
的代碼進行進一步排查,咱們發如今CuratorZookeeperClient.createEphemeral(path)
方法中有這麼一段邏輯:在createEphemeral(path)
捕獲了NodeExistsException
,建立ephemeral節點時,若此節點已存在,則認爲ephemeral節點建立成功。這段邏輯初看起來並無什麼問題,且在如下兩種常見的場景下表現正常:框架
public void createEphemeral(String path) { try { client.create().withMode(CreateMode.EPHEMERAL).forPath(path); } catch (NodeExistsException e) { } catch (Exception e) { throw new IllegalStateException(e.getMessage(), e); } }
可是實際上還有一種極端場景,zookeeper的Session過時與刪除Ephemeral節點不是原子性的,也就是說客戶端在獲得Session過時的消息時,Session對應的Ephemeral節點可能還未被zookeeper刪除。此時dubbo去建立Ephemeral節點,發現原節點仍存在,故不從新建立。待Ephemeral節點被zookeeper刪除後,便會出現dubbo認爲從新註冊成功,但實際未成功的狀況,也就是咱們在生產環境遇到的問題。分佈式
此時,問題的根源已被定位。定位問題以後,經咱們與 Dubbo 社區交流,發現考拉的同窗也遇到過一樣的問題,更肯定了這個緣由。
問題的復現與修復
定位到問題以後,咱們便開始嘗試本地復現。因爲zookeeper的Session過時但Ephemeral節點未被刪除的場景直接模擬比較困難,咱們經過修改zookeeper源碼,在Session過時與刪除Ephemeral節點的邏輯中增長了一段休眠時間,間接模擬出這種極端場景,並在本地復現了此問題。
在排查問題的過程當中,咱們發現kafka的舊版本在使用zookeeper時也遇到過相似的問題,並參考kafka關於此問題的修復方案,肯定了dubbo的修復方案。在建立Ephemeral節點捕獲到NodeExistsException
時進行判斷,若Ephemeral節點的SessionId與當前客戶端的SessionId不一樣,則刪除並重建Ephemeral節點。在內部修復並驗證經過後,咱們向社區提交了issues及pr。
kafka相似問題issues:https://issues.apache.org/jira/browse/KAFKA-1387
dubbo註冊恢復問題issues:https://github.com/apache/dubbo/issues/5125
上文中的問題修復方案已經肯定,但咱們顯然不可能在每個dubbo版本上都進行修復。在諮詢了社區dubbo的推薦版本後,咱們決定在dubbo2.7.3版本的基礎上,開發內部版本修復來這個問題。並借這個機會,開始推進公司dubbo版本的統一升級工做。
爲何要統一dubbo版本
爲何選擇dubbo2.7.3
內部版本定位
基於社區dubbo2.7.3版本開發的dubbo內部版本屬於過渡性質的版本,目的是爲了修復線上provider不能恢復註冊的問題,以及一些社區dubbo2.7.3的兼容性問題。瓜子的dubbo最終仍是要跟隨社區的版本,而不是開發自已的內部功能。所以咱們在dubbo內部版本中修復的全部問題均與社區保持了同步,以保證後續能夠兼容升級到社區dubbo的更高版本。
兼容性驗證與升級過程
咱們在向dubbo社區的同窗諮詢了版本升級方面的相關經驗後,於9月下旬開始了dubbo版本的升級工做。
兼容性問題彙總
在推進升級dubbo2.7.3版本的過程總體上比較順利,固然也遇到了一些兼容性問題:
建立zookeeper節點時提示沒有權限
dubbo配置文件中已經配置了zookeeper的用戶名密碼,但在建立zookeeper節點時卻拋出KeeperErrorCode = NoAuth
的異常,這種狀況分別對應兩個兼容性問題:
dubbo在創建與zookeeper的鏈接時會根據zookeeper的address複用以前已創建的鏈接。當多個註冊中心使用同一個address,但權限不一樣時,就會出現NoAuth
的問題。
參考社區的pr,咱們在內部版本進行了修復。
curator版本兼容性問題
<dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>4.2.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>4.2.0</version> </dependency>
* 分佈式調度框架elastic-job-lite強依賴低版本的curator,與dubbo2.7.3使用的curator版本不兼容,這給dubbo版本升級工做帶來了必定阻塞。考慮到elastic-job-lite已經好久沒有人進行維護,目前一些業務線計劃將elastic-job-lite替換爲其餘的調度框架。
dubbo的ServiceBean監聽spring的ContextRefreshedEvent,進行服務暴露。openFeign提早觸發了ContextRefreshedEvent,此時ServiceBean還未完成初始化,因而就致使了應用啓動異常。
參考社區的pr,咱們在內部版本修復了此問題。
org.apache.dubbo.rpc.RpcException
。所以,在consumer所有升級到2.7以前,不建議將provider的com.alibaba.dubbo.rpc.RpcException
改成org.apache.dubbo.rpc.RpcException
瓜子目前正在進行第二機房的建設工做,dubbo多機房是第二機房建設中比較重要的一個話題。在dubbo版本統一的前提下,咱們就可以更順利的開展dubbo多機房相關的調研與開發工做。
初步方案
咱們諮詢了dubbo社區的建議,並結合瓜子云平臺的現狀,初步肯定了dubbo多機房的方案。
同機房優先調用
dubbo同機房優先調用的實現比較簡單,相關邏輯以下:
針對以上邏輯,咱們簡單實現了dubbo經過環境變量進行路由的功能,並向社區提交了pr。
dubbo經過環境變量路由pr: https://github.com/apache/dubbo/pull/5348
本文做者:李錦濤,任職於瓜子二手車基礎架構部門,負責瓜子微服務架構相關工做。目前主要負責公司內 Dubbo 版本升級與推廣、 Skywalking 推廣工做。
本文做者:李錦濤
本文爲阿里雲內容,未經容許不得轉載。