前言
随着瓜子業務的不斷發展,系統規模在逐漸擴大,目前在瓜子的私有雲上已經運作着數百個 Dubbo 應用,上千個 Dubbo 執行個體。瓜子各部門業務迅速發展,版本沒有來得及統一,各個部門都有自己的用法。随着第二機房的建設,Dubbo 版本統一的需求變得越發迫切。幾個月前,公司發生了一次與 Dubbo 相關的生産事故,成為了公司 基于社群 Dubbo 2.7.3 版本更新的誘因。
接下來,我會從這次線上事故開始,講講我們這段時間所做的 Dubbo 版本更新的曆程以及我們規劃的 Dubbo 後續多機房的方案。
一、Ephermal節點未及時删除導緻provider不能恢複注冊的問題修複
事故背景
在生産環境,瓜子内部各業務線共用一套zookeeper叢集作為dubbo的注冊中心。2019年9月份,機房的一台交換機發生故障,導緻zookeeper叢集出現了幾分鐘的網絡波動。在zookeeper叢集恢複後,正常情況下dubbo的provider應該會很快重新注冊到zookeeper上,但有一小部分的provider很長一段時間沒有重新注冊到zookeeper上,直到手動重新開機應用後才恢複注冊。
排查過程
首先,我們統計了出現這種現象的dubbo服務的版本分布情況,發現在大多數的dubbo版本中都存在這種問題,且發生問題的服務比例相對較低,在github中我們也未找到相關問題的issues。是以,推斷這是一個尚未修複的且在網絡波動情況的場景下偶現的問題。
接着,我們便将出現問題的應用日志、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節點。
繼續對
doRegister(url)
的代碼進行進一步排查,我們發現在
CuratorZookeeperClient.createEphemeral(path)
方法中有這麼一段邏輯:在
createEphemeral(path)
捕獲了
NodeExistsException
,建立ephemeral節點時,若此節點已存在,則認為ephemeral節點建立成功。這段邏輯初看起來并沒有什麼問題,且在以下兩種常見的場景下表現正常:
- Session未過期,建立Ephemeral節點時原節點仍存在,不需要重新建立
- Session已過期,建立Ephemeral節點時原節點已被zookeeper删除,建立成功
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-1387dubbo注冊恢複問題issues:
https://github.com/apache/dubbo/issues/5125二、瓜子的dubbo更新曆程
上文中的問題修複方案已經确定,但我們顯然不可能在每一個dubbo版本上都進行修複。在咨詢了社群dubbo的推薦版本後,我們決定在dubbo2.7.3版本的基礎上,開發内部版本修複來這個問題。并借這個機會,開始推動公司dubbo版本的統一更新工作。
為什麼要統一dubbo版本
- 統一dubbo版本後,我們可以在此版本上内部緊急修複一些dubbo問題(如上文的dubbo注冊故障恢複失效問題)。
- 瓜子目前正在進行第二機房的建設,部分dubbo服務也在逐漸往第二機房遷移。統一dubbo版本,也是為dubbo的多機房做鋪墊。
- 有利于我們後續對dubbo服務的統一管控。
- dubbo社群目前的發展方向與我們公司現階段對dubbo的一些訴求相吻合,如支援gRPC、雲原生等。
為什麼選擇dubbo2.7.3
- 我們了解到,在我們之前攜程已經與dubbo社群合作進行了深度合作,攜程内部已全量更新為 2.7.3 的社群版本,并在協助社群修複了 2.7.3 版本的一些相容性問題。感謝攜程的同學幫我們踩坑~
- dubbo2.7.3版本在當時雖然是最新的版本,但已經釋出了2個月的時間,從社群issues回報來看,dubbo2.7.3相對dubbo2.7之前的幾個版本,在相容性方面要好很多。
- 我們也咨詢了dubbo社群的同學,推薦更新版本為2.7.3。
内部版本定位
基于社群dubbo2.7.3版本開發的dubbo内部版本屬于過渡性質的版本,目的是為了修複線上provider不能恢複注冊的問題,以及一些社群dubbo2.7.3的相容性問題。瓜子的dubbo最終還是要跟随社群的版本,而不是開發自已的内部功能。是以我們在dubbo内部版本中修複的所有問題均與社群保持了同步,以保證後續可以相容更新到社群dubbo的更高版本。
相容性驗證與更新過程
我們在向dubbo社群的同學咨詢了版本更新方面的相關經驗後,于9月下旬開始了dubbo版本的更新工作。
-
初步相容性驗證
首先,我們梳理了一些需要驗證的相容性case,針對公司内部使用較多的dubbo版本,與dubbo2.7.3一一進行了相容性驗證。經驗證,除dubboX外,dubbo2.7.3與其他dubbo版本均相容。dubboX由于對dubbo協定進行了更改,與dubbo2.7.3不相容。
-
生産環境相容性驗證
在初步驗證相容性通過後,我們與業務線合作,挑選了一些重要程度較低的項目,在生産環境對dubbo2.7.3與其他版本的相容性進行了進一步驗證。并在内部版本修複了一些相容性問題。
-
推動公司dubbo版本更新
在10月初,完成了dubbo相容性驗證後,我們開始在各個業務線推動dubbo的更新工作。截止到12月初,已經有30%的dubbo服務的完成了版本更新。按照排期,預計于2020年3月底前完成公司dubbo版本的統一更新。
相容性問題彙總
在推動更新dubbo2.7.3版本的過程整體上比較順利,當然也遇到了一些相容性問題:
-
建立zookeeper節點時提示沒有權限
dubbo配置檔案中已經配置了zookeeper的使用者名密碼,但在建立zookeeper節點時卻抛出
的異常,這種情況分别對應兩個相容性問題:KeeperErrorCode = NoAuth
- issues: https://github.com/apache/dubbo/issues/5076 dubbo在未配置配置中心時,預設使用注冊中心作為配置中心。通過注冊中心的配置資訊初始化配置中心配置時,由于遺漏了使用者名密碼,導緻此問題。
- https://github.com/apache/dubbo/issues/4991 dubbo在建立與zookeeper的連接配接時會根據zookeeper的address複用之前已建立的連接配接。當多個注冊中心使用同一個address,但權限不同時,就會出現
的問題。NoAuth
- curator版本相容性問題
- dubbo2.7.3與低版本的curator不相容,是以我們預設将curator版本更新至4.2.0
<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替換為其他的排程架構。
- openFeign與dubbo相容性問題 https://github.com/apache/dubbo/issues/3990
dubbo的ServiceBean監聽spring的ContextRefreshedEvent,進行服務暴露。openFeign提前觸發了ContextRefreshedEvent,此時ServiceBean還未完成初始化,于是就導緻了應用啟動異常。
參考社群的pr,我們在内部版本修複了此問題。
-
RpcException相容性問題
dubbo低版本consumer不能識别dubbo2.7版本provider抛出的
。是以,在consumer全部更新到2.7之前,不建議将provider的org.apache.dubbo.rpc.RpcException
改為com.alibaba.dubbo.rpc.RpcException
org.apache.dubbo.rpc.RpcException
-
qos端口占用
dubbo2.7.3預設開啟qos功能,導緻一些混部在實體機的dubbo服務更新時出現qos端口占用問題。關閉qos功能後恢複。
-
自定義擴充相容性問題
業務線對于dubbo的自定義擴充比較少,是以在自定義擴充的相容性方面暫時還沒有遇到比較難處理的問題,基本上都是變更package導緻的問題,由業務線自行修複。
-
skywalking agent相容性問題
我們項目中一般使用skywalking進行鍊路追蹤,由于skywalking agent6.0的plugin不支援dubbo2.7,是以統一更新skywalking agent到6.1。
三、dubbo多機房方案
瓜子目前正在進行第二機房的建設工作,dubbo多機房是第二機房建設中比較重要的一個話題。在dubbo版本統一的前提下,我們就能夠更順利的開展dubbo多機房相關的調研與開發工作。
初步方案
我們咨詢了dubbo社群的建議,并結合瓜子雲平台的現狀,初步确定了dubbo多機房的方案。
- 在每個機房内,部署一套獨立的zookeeper叢集。叢集間資訊不同步。這樣就沒有了zookeeper叢集跨機房延遲與資料不同步的問題。
- dubbo服務注冊時,僅注冊到本機房的zookeeper叢集;訂閱時,同時訂閱兩個機房的zookeeper叢集。
- 實作同機房優先調用的路由邏輯。以減少跨機房調用導緻的不必要網絡延遲。
同機房優先調用
dubbo同機房優先調用的實作比較簡單,相關邏輯如下:
- 瓜子雲平台預設将機房的标志資訊注入容器的環境變量中。
- provider暴露服務時,讀取環境變量中的機房标志資訊,追加到待暴露服務的url中。
- consumer調用provider時,讀取環境變量中的機房标志資訊,根據路由政策優先調用具有相同标志資訊的provider。
針對以上邏輯,我們簡單實作了dubbo通過環境變量進行路由的功能,并向社群送出了pr。
dubbo通過環境變量路由pr:
https://github.com/apache/dubbo/pull/5348本文作者:李錦濤,任職于瓜子二手車基礎架構部門,負責瓜子微服務架構相關工作。目前主要負責公司内 Dubbo 版本更新與推廣、 Skywalking 推廣工作。