聊聊攜程升級Dubbo的踩坑歷程

一、什麼是 CDubbo

攜程從 2017 年 11 月左右開始調研,真正落地是在 2018 年 4 月發佈的 CDubbo 0.1.1 版本。在攜程內部,我們管他叫 CDubbo,言下之意就是攜程版的 Dubbo。考慮到以後升級的問題,CDubbo SDK 是對 Dubbo SDK 的擴展和包裝,保留了 Dubbo 所有的擴展和配置能力。

目前,生產環境已經從第一個 0.1.1 版本,到目前的 0.13.3 版本,歷經十幾個版本的迭代,服務端有 156 個應用,客戶端 170 個應用,生產實例數 2000 個左右。

二、升級 2.7.3 的幾個理由

2.5.10 版本在攜程只用了一年半左右,業務的應用也不算很多,這麼快就大版本升級,主要是遇到了下面的幾個問題。

1)2.5.10 的異步也是有阻塞的

2.5.10 版本只支持客戶端異步,而且是基於 JDK 1.6 的 Future,並不是真正意義上的異步,本質上還是阻塞的,只不過是從 DubboClientHandler 線程切換到了業務線程。

2)支持服務端異步

對於微服務來說,一般又會調用外部服務,在網絡 IO 比較多的場景下異步服務的優勢會很明顯,可以充分利用 CPU 資源,提高系統吞吐量,降低響應時間。部分機票、酒店等業務同學明確表示需要服務端異步。

3)現階段不兼容問題帶來的副作用較小

不兼容問題大概率是服務端和客戶端的版本不一致,比如服務端 2.7.0,客戶端 2.5.10。考慮到攜程目前 CDubbo 服務的比例還不太高,早一點升級對業務的影響會比較小。

4)爲之後的升級做鋪墊

攜程業務場景很廣泛,部分業務已經明確表示需要 2.7.0 的服務端異步,也有業務在嘗試 3.0 的 Reactive 了。如果先升級到 2.7.0,以後再升級 3.0 會比較容易些,如果直接從 2.5.10 升級到 3.0 版本,可能升級不過去,或者無法透明升級。

5)支持三中心

2.5.10 只有註冊中心,註冊數據和配置數據對註冊中心的壓力比較大。2.7.0 對模型重構,拆分成註冊中心、元數據中心、配置中心,職責劃分更合理。爲了接入公司的測試平臺,需要用到服務的元數據信息,2.7.0 正好提供了這個能力。

三、第一階段升級及踩坑歷程

注:爲了表達方便,後續提到的 Apache 代表 org.apache 的 package,Alibaba 代表 com.alibaba 的 package。

第一階段升級 2.7.0 是在 2019 年 3 月份左右,大概花了三週的時間,我們先來看下所遇到的幾個問題吧。

3.1 變更 package 導致的不兼容

2.7.0 把 package 從 com.alibaba 改成了 org.apache,雖然對低版本做了兼容,但是還是會發現部分 class 找不到了,例如:Alibaba 的 DubboComponentScan 就已經被刪掉了。取而代之的是 Apache 的 DubboComponentScan,不過這個問題在編譯時就會報錯了。

3.2 Apache 的 Constants 常量類被拆分

升級到 2.7.0 版本之後,Alibaba package 的 Constants 還是沒變,但是如果要用新功能升級到 Apache package,你會發現 Constants 被拆分成 RegistryConstants, CommonConstants, RemotingConstants 等多個常量類。新的常量類只是分散到不同的 class 中,只要換個引用就可以解決了。

3.3 Apache 的 Router 接口新增了部分方法

如果擴展是基於 Alibaba 的 Router 接口,Dubbo 已經做了默認實現,應該不會存在兼容性問題。這次,我們直接換成了 Apache 的 Router 接口,因爲新加了 isRuntime、isForce、getPriority 方法編譯時就報錯了。

3.4 Apache 的 ProxyFactory 接口新增了 getProxy 方法

我們這次升級是把 Alibaba 的 ProxyFactory 換到了 Apache 的 package 下,2.7.0 版本中對該接口新增了 getProxy 方法,編譯時會報錯。如果不需要擴展這部分功能,可以通過 delegate 機制保留默認實現就可以了。

3.5 限制 ApplicationConfig 必須全局唯一

2.5.10 版本對於 ApplicationConfig 沒有限制,服務端起多個服務時可以配置獨立的 ApplicationConfig。但是從 2.7.0 開始 ApplicationConfig 就會要求全局唯一,如果一個應用定義了多個不同的 ApplicationConfig 就會報錯。

Apache 的 ConfigManager 的 setApplication 會檢查是否 duplicate。

3.6 JDK 1.8

2.7.0 爲了支持真正的異步,用到了 JDK 1.8 的 CompletableFuture,也用到了 1.8 的 Supplier、Consumer 等操作符。如果業務的應用還是基於 JDK 1.7 打包的,升級後就會導致發佈失敗。由於我們這次是公司層面的整體升級,就需要所有業務應用都升級到 1.8 纔可以發佈。

3.7 默認升級到 Netty4

爲了接入公司的 CAT 監控系統,需要把 Codec 的監控埋點數據通過 ThreadLocal 傳遞下去。

但是,2.7.0 把 Netty 的版本從默認的 Netty3 升級到了 Netty4,這兩個版本的線程模型是不一樣的,Netty3 的 decode 是在 New IO worker 線程,Netty4 是 NettyServerWorker 線程,導致原有邏輯的監控埋點數據傳不過來。爲了暫時解決這個問題,我們把默認的 Netty 版本降回了 Netty3。

注:CAT 是點評開源的實時應用監控平臺,目前在攜程也有落地,在 Github (https://github.com/dianping/cat)上也已經超過 1 萬顆星。

3.8 異步請求一直 hang 住

擴展 2.5.10 版本的時候,爲了支持對客戶端異步的埋點,我們對 RpcContext 的 Future 重新包裝了,用戶拿到的 Future 已經是被我們包裝過的 FutureAdapter 了。

在 2.7.0 版本中, AsyncRpcResult 在 recreate 的時候也會給 RpcContext 設置 Future。導致用戶拿到的 Future 跟實際的不是同一個,客戶端一直拿不到響應,請求被 hang 住。

2.7.0 對這部分的重構很好,支持了異步 Filter 鏈,通過 ListenableFilter 回調機制比現在的代碼結構更清晰,可以把同步和異步埋點的邏輯進行統一整合。

3.9 服務端無法指定客戶端的調用方式

Issue:https://github.com/apache/dubbo/issues/3650

如果服務端設置了默認 ASYNC,升級到 2.7.0 版本後客戶端會拿不到響應。例如:服務端配置了 async=true,客戶端默認配置。

<dubbo:service interface="..." async="true">
<dubbo:reference interface="...">

2.5.10 版本的客戶端,通過 client.sayHello() 會返回 null,RpcContext 的 Future 可以拿到響應。

基於 2.7.0 版本測試下來, client.sayHello 拿到了響應,但是 RpcContext 的 Future 卻是 null。

經過研究發現,2.7.0 版本在 ClusterUtils 的 mergeUrl 過程中把服務端傳遞過來的 ASYNC_KEY 給刪掉了,所以客戶端仍然以同步方式去調用。

這個是新老版本兼容性的 Bug,已經在 2.7.2 修復了,驗證下來沒問題了。

3.10 @Service 註解無法設置 parameters 參數

Issue:https://github.com/apache/dubbo/issues/3778

用戶通過 Annotation 方式啓動服務,在 @Service 註解的 parameters 屬性,服務端啓動的時候拿不到用戶配置的參數。

@Service(parameters = {"someKey","someValue"})
public class DemoServiceImpl implements DemoService {
}

並且報了下面這樣的錯誤。

Caused by: 
org.springframework.beans.ConversionNotSupportedException: 
Failed to convert property value of type 'java.lang.String[]' to required type 'java.util.Map' for property 'parameters'; 
nested exception is java.lang.IllegalStateException: 
Cannot convert value of type 'java.lang.String[]' to required type 'java.util.Map' for property 'parameters': 
no matching editors or conversion strategy found

2.7.0 版本對這部分機制進行了重構,BeanDefinitionBuilder 把這段 parameters 的參數轉換代碼給漏掉了。加上這段邏輯之後,我們測試下來已經 OK 了,這個問題已經在 2.7.2 解決掉了。

private AbstractBeanDefinition buildServiceBeanDefinition() {
    BeanDefinitionBuilder builder =rootBeanDefinition(ServiceBean.class);
    ...
    // Convert parameters into map
   builder.addPropertyValue("parameters",convertParameters(serviceAnnotationAttributes.getStringArray("parameters")));
}

3.11 當客戶端發現服務時出現異常,即使服務端啓動後也不會恢復

Issue:https://github.com/apache/dubbo/issues/4068

API 方式比 XML 和 Annotation 更靈活,可以在不重啓進程的情況下多次初始化客戶端。

服務端沒有啓動的情況下,通過 API 的方式啓動了客戶端,這個時候客戶端會報 No Provider 的錯誤。然後啓動服務端,客戶端通過 API 的方式再次初始化,仍然會報 No Provider 的錯誤。

ReferenceConfigCache cache = ReferenceConfigCache.getCache();
DemoService demoService = (DemoService) cache.get(reference);

通過翻閱 ReferenceConfig 的代碼,服務發現的時候可能會拋異常導致直接跳出 init 過程,但是 initialized 標誌位已經被置爲 true 了,導致下次不會再重新初始化。

修復方案:只有在 init 方法的最後,客戶端代理創建完成纔會設置 initialized 爲 true。

這個問題已經在 2.7.2 版本修復,驗證下來已經 OK 了。

3.12客戶端服務發現失敗,重試會有 OOM 的風險

Issue:https://github.com/apache/dubbo/issues/4107

如果服務端沒有啓動的情況下啓動了客戶端,客戶端會報 No Provider 的錯誤,如果一直不停的重試可能會有 OOM 的風險。

Dubbo 在創建代理的時候會緩存 urls,每次啓動失敗都會把 url 加到 urls,但是由於 dubbo 的 URL 是有時間戳的,就導致 urls 隊列不停的增長,甚至引起 Heap OOM 的風險。

解決方案:每次創建代理之前,都把 urls 給予清空,這個問題已經在 2.7.2 中解決了。

總結:第一輪升級過程中大概歷時 3 周左右,發現的幾個 Issue 導致我們的 Test Case 無法繼續下去,升級過程暫停了兩三個月。

四、第二階段升級及踩坑歷程

直到六月中旬,阿里團隊把上述幾個問題在 2.7.2 修復了,我們重新開始了第二輪的升級過程。

4.1 性能測試,吞吐量下降了 40%

服務端:8C24G 的物理機,響應報文大小爲 10Bytes,queue 設置爲 -1 無界隊列。

客戶端:10 臺 4C8G 的 Docker,請求報文大小也是 10Bytes。

基於原生的 2.5.10 版本,我們的壓測環境下可以達到 8 萬 QPS 左右。由於 CDubbo 擴展了熔斷、配置、監控等功能,吞吐量下降到 5.5 萬 QPS 左右。

升級到 2.7.2 版本後,最高只壓測到 3 萬多,吞吐量下降了差不多 40% 左右。

這個問題是因爲 JDK 1.8 的 Bug 導致的,JDK 1.8 的 CompletableFuture 在 get 時會等到 256 次 countDown 執行完畢,影響了性能。

Issue:https://github.com/apache/dubbo/issues/4279

總結:第二階段遇到的性能下降的問題肯定要解決後纔可以上線,問題反饋給阿里團隊後,他們需要討論新的 Hotfix 發佈機制。

五、第三階段升級及踩坑歷程

這次改變了合作模式,跟阿里團隊基於 2.7.3-SNAPSHOT 版本一起討論,一起修復,一起驗證。下面的幾個問題都是基於 SNAPSHOT 驗證過程中發現的問題,並且在正式版中修復掉了。

5.1 對於同步的請求,方法級超時不生效

Issue:https://github.com/apache/dubbo/issues/4435

如果服務級設置的 timeout 爲 1000ms,sayHello 方法設置的 timeout 爲 800ms。理論上來說,sayHello 方法的請求應該在 800ms 就會超時了,但是實際上我們發現直到 1000ms 纔會超時。

<dubbo:reference id="demoService" interface="com.ctrip.Demo" timeout="1000">
  <dubbo:method name="sayHello">
    <dubbo:parameter key="timeout" value="800"/>
  </dubbo:method>
</dubbo:reference>

同步的請求是在 AsyncToSyncInvoker 中執行了同步等待,修復前的代碼如下,取的是整個服務的超時時間,也就是 1000ms。

asyncResult.get(getUrl().getParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT), TimeUnit.MILLISECONDS);

解決方案:同步請求,除了 AsyncToSyncInvoker 在 get 時被設置了超時時間,DubboInvoker 的 CompletableFuture 也被設置了超時時間。其實,只要一個地方能夠超時就足夠了,所以 AsyncToSyncInvoker 被設置到 Integer.MAX_VALUE 永不超時,所有的超時機制都通過 CompletableFuture 實現。

5.2 異步超時的情況下,不會回調 listener 的 onError 方法,導致埋點丟失

Issue:https://github.com/apache/dubbo/issues/4152

https://github.com/apache/dubbo/issues/4306

在修復前的版本中,ProtocolFilterWrapper 的 Filter 鏈中,只處理了正常的 onResponse 響應,並沒有處理 onError 情況,就導致異常發生時不會回調 ListenableFilter 的 onError 方法。

修復後,會對正常響應和異常響應進行回調。

try {
    if (t == null) {
        listener.onResponse(r, filterInvoker, invocation);
    } else {
        listener.onError(t, filterInvoker, invocation);
    }
} catch (Throwable filterError) {
    t = filterError;
}

5.3 @Reference 註解的方式,客戶端不會初始化

Issue:https://github.com/apache/dubbo/issues/4330

基於 2.7.2 版本,如果用的 Annotation 的方式,先要把 DubboComponentScan 換成 Apache 的,不然編譯時就會因爲找不到 class 而報錯 。

如果客戶端的 @Reference 用的還是 Alibaba 的 package,所拿到的 proxy 代理是 null,導致 service.sayHello 調用時拋 NPE 的 exception。

這個問題,主要是由於 Apache 的 DubboComponentScan 沒有兼容 Alibaba 的 @Reference 註解,目前 2.7.3 正式版實現了對 Alibaba 的 @Reference 和 @Service 的兼容。

5.4 服務端 executes 限流失效

Issue:https://github.com/apache/dubbo/issues/4277

我們的測試場景把服務的 executes 設置爲 1,然後客戶端多線程發起請求到服務端。第一次發起的多線程請求,只有一個請求能通過,符合預期。第二次再發起多線程請求,所有請求都通過了,並沒有被限流。

<bean id="demoService" class="com.xxx."/>
<dubbo:service interface="com.xxx" ref="demoService" executes="1">
</dubbo:service>

這是因爲服務端拋異常的時候,除了正常請求結束後釋放掉的計數器,異常處理時又減了一次,之後的限流一直處於失效的狀態,所有請求都可以通過了。

這個問題已經在 2.7.3 解決了,解決方案就是在 onError 的時候不要重複減。

5.5 現有服務框架生成的 ListenableFutre 異步服務接口,Dubbo 無法支持

攜程現有幾千個 SOA 服務,服務端異步用的是 Guava 的 ListenableFuture,但是 2.7.0 支持的服務端異步用的是 CompletableFuture,這就導致現有服務接口遷移過來,無法支持 Dubbo 協議的服務端異步了。

針對這個問題,我們想到了幾個方案。

方案 1:讓 Dubbo 既支持 CompletableFuture 又支持 ListenableFuture

首先,需要 Dubbo 支持 ListenableFuture,這個改動成本比較高。其次,對用戶多一個選擇也會提高他們的學習成本,以及犯錯的概率。

方案 2:只支持 CompletableFuture

如果用戶從 SOA 服務遷移到 CDubbo 框架,就需要把服務接口的 Future 類型改爲 CompletableFuture。

最終跟業務溝通下來,選擇了方案 2,業務遷移到 CDubbo 的時候手工修改服務接口的 Future 類型。

5.6 服務端新版本,客戶端老版本,報 Netty3 找不到的異常

這個問題的根因是前面監控打點失敗,我們把 Netty 默認版本降回了 Netty3。服務端 2.7.3 版本,客戶端 2.5.10 版本的情況下會報 Netty3 找不到的異常。

在 2.5.10 版本中, Netty3 在 resource 配置文件中的名字叫 netty,具體如下圖:

但是,2.7.3 版本把 Netty3 在 resource 配置文件中的名字改成了 netty3,而不是 netty 了。

服務端註冊的時候會包括 Netty 版本,通過註冊中心推送到了客戶端,客戶端的 2.5.10 版本不存在 netty3 的資源文件,通過 SPI 加載的時候因爲找不到 netty3 而報錯了。

解決方案:Netty 的版本不應該被推送到客戶端,我們修改了動態配置的推送規則,不允許 Netty 參數推送到客戶端,問題就解決了。

六、兼容性測試

第三輪測試把所有的 Test Case 都通過了,接着我們手工驗證了新老版本的兼容性測試。以下場景都是基於服務端升級 2.7.3,客戶端仍然是 2.5.10 場景下的測試驗證。

6.1 註冊發現機制

服務端可以正常註冊到註冊中心,客戶端也可以發現到新版本的服務端。

6.2 同步請求是否正常

如果服務端返回的是 Response 對象,客戶端以同步的方式可以正常調用。

6.3 異步請求是否正常

如果服務端返回的是 Response 對象,客戶端以異步的方式可以正常調用。

6.4 監控打點

除了不支持 CompletableFuture,其他都正常。

6.5 服務端升級到新版本,客戶端老版本,在超時場景下的異常測試

因爲服務端拋的是 org.apache.dubbo.rpc.RpcException,這個 package 在 2.5.10 版本中是不存在的,就會報 java.lang.ClassNotFoundException 的錯誤。

這個錯誤似乎也沒法避免,這也是我們優先升級 Dubbo 2.7.3 的原因,我們要忍受這種陣痛,等全部升級完就不存在這個問題了。

七、性能壓測

兼容性測試也通過了,我們緊接着開始了第二輪壓力測試:(基於 2.7.3-SNAPSHOT)

從服務端的壓測數據來看,在低於 4 萬 QPS 的時候性能沒啥區別,在 5 萬左右的時候響應時間有所下降,主要是由於 YGC 導致的。

2.5.10 服務端

2.7.3 服務端

從客戶端的性能來看,吞吐量基本沒啥變化,響應時間在 5000QPS 的時候下降稍微有點明顯,主要也是 GC 導致的。

2.5.10 客戶端

2.7.3 客戶端

八、集成測試

到現在爲止,CDubbo 單個組件已經完成了所有 Test Case,兼容性測試也全部通過了,壓力測試的結果勉強可以接受。公司有一些中間件也會依賴 Dubbo,除了這些組件要升級 Dubbo 到 2.7.3,我們還遇到其他一些問題。

8.1 ApplicationConfig 的衝突再次出現

前面只是解決了 CDubbo 單個組件的 ApplicationConfig 衝突問題,在一個組件中保證只會有一個 ApplicationConfig。但是,不同組件在暴露本地服務的時候也需要設置 ApplicationConfig,用戶可能會只引用一個組件,也可能兩個同時引用,無法保證不同組件只初始化一個 ApplicationConfig。

看了 ApplicationConfig 的 equals 方法,可以知道衝突是因爲 name 不一致,我們只要保證 name 一致就行了。

8.2 開源和訂製版的衝突

在攜程,大部分業務用的是我們提供的開源版的 Dubbo,還有部分業務使用的是基於 Dubbo 代碼直接修改過的訂製版本。

因爲,我們這次是公司級的升級,用了訂製版 Dubbo 的應用,如果引入公司其他中間件,這些中間件又依賴了開源版的 Dubbo,就會導致業務的應用類衝突。

對於這個問題,沒有統一的解決方案,需要跟業務同事進行討論來解決。

8.3 服務端啓動時端口連不上

Issue:https://github.com/apache/dubbo/issues/4775

在集成測試時,服務端用的默認協議,客戶端通過 20880 端口發起的連接,結果客戶端報連接失敗。後來看了下服務端的 20880 端口的確沒有打開,本地打開的端口號是 20xxx。

經過調試代碼發現拿到的默認協議是 QSchedule 組件設置的 ProtocolConfig。看下 ConfigManager 的代碼,addProtocol 的時候會把第一個協議作爲默認協議緩存下來了,之後再 getDefaultProtocol 的時候拿到的並不是默認的協議了。

public void addProtocol(ProtocolConfig protocolConfig) {
    …
    if (protocols.containsKey(key)&& !protocolConfig.equals(protocols.get(key))) {
        logger.warn("…");
    } else {
        protocols.put(key, protocolConfig);
    }
}

這個問題可以把 ProtocolConfig 默認值設置爲 false,就不會被 put 到 protocols 作爲默認協議了。但是,對於不知道背景的同學可能還是會掉坑裏,不過這個問題會在 2.7.4 版本中修復。

九、感興趣的幾個話題

寫到這裏,我們已經通過了 Test Case、迴歸測試、壓力測試和集成測試,發佈了 SNAPSHOT 版本給到業務同事去試用,預計九月初會發布正式版本。

除了我們踩到的坑,下面可能也是你感興趣的話題。

9.1 註冊中心

註冊中心,我們在去年落地 2.5.10 的時候就擴展了攜程自己的註冊中心。

服務端註冊實例信息到註冊中心,每隔 5s 發送一次心跳來續約,如果註冊中心 30s 沒有收到心跳,會將其從註冊中心反註冊,並通知到客戶端。

客戶端向註冊中心發起訂閱,當註冊信息發生變化時會通過長連接推送到客戶端。

這套機制是基於 2.5.10 擴展的,在升級 2.7.3 的過程中沒有任何變更,可以完全兼容 2.7.3,服務端和客戶端都可以正常的註冊和發現。

9.2 從一箇中心拆分成三中心

1)註冊中心:前面已經提到,升級 2.7.3 沒有變更,可以完全兼容。

2)配置中心:CDubbo 的 0.2.0 版本,爲了接入攜程自己的配置中心,就已經通過 override 協議實現了動態配置方案,這套機制目前沒有發現問題,所以這次沒有對接 2.7.3 提供的動態配置推送能力。方案可以參考:

http://dubbo.apache.org/zh-cn/docs/user/demos/config-rule-deprecated.html

3)元數據中心:TCP 協議的測試不像 HTTP 協議那麼方便,服務提供者必須得自己寫個 Client 才能測試驗證,大多數業務同事都反饋過這個痛點。

在 2019 年 1 月份的時候,CDubbo 對接了攜程的測試平臺,支持 Dubbo 協議的測試。當時爲了透明升級 2.7.0 版本,就已經提前把元數據中心的代碼拷貝到內部的版本了,這次升級 2.7.3 版本很平滑,沒有發現有啥問題。

9.3 爲什麼敢於升級大版本

業界對大版本升級的普遍做法就是,等其他大廠試試看,或者等發佈幾個 hotfix 之後再考慮。攜程在這次升級過程中有一套自己的保障,事實也證明我們的單元測試和集成測試在 2.7.3 升級過程中發揮了重要作用。

1)單元和集成測試覆蓋率 93%:剛開始落地 CDubbo 的時候就非常重視測試覆蓋率。目前爲止,我們的測試覆蓋率仍然達到了 93%。

在這次升級過程中,很多藏的很深的 Bug 都是通過我們的測試代碼發現的,前面談到的升級過程中遇到的 Bug 基本都是通過測試代碼發現的。不但保證了質量,也提高了我們升級的效率。

2)Benchmark 壓測:我們有一套穩定的壓測環境,服務端是一臺 8C24G 的物理機,客戶端是 10 臺 4C8G 的 Docker 機器。服務器都比較穩定,測試結果也能真實準確的反應出性能的問題。

每次發佈新功能的時候,都要經過 Benchmark 至少 5 萬 QPS 以上持續一天的穩定性壓測。

作者介紹

顧海洋,攜程框架架構研發部技術專家,負責攜程分佈式服務化領域的工作。目前主要負責 Dubbo 在攜程的二次開發和推廣工作。

本文轉載自公衆號攜程技術中心(ID:ctriptech)

原文鏈接

https://mp.weixin.qq.com/s?__biz=MjM5MDI3MjA5MQ==&mid=2697268843&idx=1&sn=e81b115ee8a6c5b5390f33f98cf55101&chksm=8376f15fb401784945cddaec41bf891f951d6489ef10ab6b19c8d683a747682c15246ee1d83b&scene=27#wechat_redirect

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章