dubbo协议_Dubbo协议解析与OPPO自研ESA RPC框架实践
1. 背景
Dubbo是一款高性能、輕量級的開源Java RPC框架,誕生于2012年,2015年停止研發,后來重啟并發布了2.7及連續多個版本。Dubbo自開源以來,許多大公司都以此為微服務架構基石,甚至在官方停止維護的幾年中,熱度依然不減。
但最近幾年云原生技術開始成為主流,與Dubbo框架的核心設計理念有不相容之處,再加上公司安全治理的需求,OPPO互聯網技術團隊開發了面向云原生、 Mesh友好的ESA RPC框架。
2.Dubbo協議解析
協議是兩個網絡實體進行通信的基礎,數據在網絡上從一個實體傳輸到另一個實體,以字節流的形式傳遞到對端。Dubbo協議由服務提供者與消費者雙端約定,需要確定的是一次有意義的傳輸內容在讀到何時結束,因為一個一個byte傳輸過來,需要有一個結束。而且數據在網絡上的傳輸,存在粘包和半包的情況,能夠應對這個問題的辦法就是協議能夠準確的識別,當粘包發生時不會多讀,當半包發生時會繼續讀取。
2.1 Dubbo Header內容
Dubbo header的長度總共16字節,128位,如下圖所示:
- Magic(16 bits) : 協議魔數,標識Dubbo 數據包。
- Req/Res(1 bit) : 標識請求或者相應。請求:1,相應:0。
- Two Way(1 bit) : 僅在 Req/Res 為1(請求)時才有用,標記是否期望從服務器返回值。如果需要來自服務器的返回值,則設置為1。
- Event(1 bit) : 標識是否是事件消息,例如,心跳事件。如果這是一個事件,則設置為1。
- SerializationId(5 bits) : 序列化id。
- Status(8 bits) : 僅在 Req/Res 為0(響應)時有用,用于標識響應的狀態。
- RequstId(64 bits) : 標識唯一請求。類型為long。
- Data length(32 bits) : 序列化后的內容長度(變長部分,即不包含header),按字節計數。通過payload參數指定,默認為8M。
2.2 Dubbo body內容
Dubbo 數據包的body 部分內容,分為請求包與響應包。
如果是請求包,則包含的部分有:
- dubbo協議版本號(2.0.2);
- 接口名;
- 接口版本號;
- 方法名;
- 方法參數類型;
- 方法參數;
- 附件(Attachment):
- 接口分組(group);
- 接口版本號(version);
- 接口名;
- 自定義附件參數;
如果是響應包,則包含的內容有:
- 返回值類型(byte):
- 返回空值(2);
- 正常返回值(1);
- 異常(0);
- 返回值;
通過對dubbo協議的解析,我們可以知道,dubbo協議是一個Header定長的變長協議。這也在我們ESA RPC實踐過程中提供了一些思路。
2.3 Dubbo協議優缺點
2.3.1 優點
Dubbo協議的設計非常緊湊、簡單,盡可能的減少傳輸包大小,能用一個bit表示的字段,不會用一個byte。
2.3.2 不足
- 請求body中某些字段重復傳遞(如接口名,接口版本號),即body內容與附件attachment 中存在重復字段,增大傳輸數據包大小;
- 對于ServiceMesh 場景很不友好。在ServiceMesh 場景中,會將原sdk中的大部分功能遷移至SideCar 中實現,這里以服務發現為例。Dubbo 中的服務發現,是通過接口名
(interfaceName)、接口分組(group)、接口版本號(version)三者定位一個唯一服務,也是服務發現的關鍵要素,但是我們從dubbo body內容可知,必須要將完整的數據包全部解析(attachment位于body末),才能獲取到這三個要素,這是完全沒必要的。 - 沒有預留字段,擴展性不足。
3. Dubbo的現狀
Dubbo自開源以來,在業內造成了巨大的影響,許多公司甚至大廠都以此為微服務架構基石,甚至在Dubbo官方停止維護的幾年中,熱度依然不減,足以證明其本身的優秀。
在這過程中,Dubbo協議的內容一直沒有太大變化,主要是為了兼容性考慮,但其他內容,隨著Dubbo的發展變化卻是很大。這里我們主要聊一聊dubbo從2.7.0版本以后的情況。
3.1 Dubbo 2.7.x版本總覽
這是dubbo自2.7.0版本以來,各個版本的簡要功能說明,以及升級建議。可以看到dubbo官方推薦生產使用的只有2.7.3 和2.7.4.1兩個版本。但這兩個推薦版本,也有不能滿足需求的地方。
由于dubbo在2.7.3 和2.7.4.1 這兩個版本中改動巨大,使得這兩個版本無法向下兼容,這讓基于其他版本做的一些dubbo擴展幾乎無法使用。升級dubbo的同時,還需要將以前的擴展全部檢查修改一遍,這帶來很大工作量。而且除了我們自身團隊的一些公共擴展外,全公司其他業務團隊很可能還有自己的一些擴展,這無疑增大了我們升級dubbo的成本。
4. ESA RPC最佳實踐
最近幾年云原生技術開始成為主流,與Dubbo框架的核心設計理念也有不相容之處,再加上公司安全治理的需求,我們需要一款面向云原生、 Mesh友好的RPC框架。
在這個背景下,OPPO互聯網技術團隊從2019年下半年開始動手設計開發ESA RPC,到2020年一季度,ESA RPC 第一版成功發布。下面我們簡單介紹下ESA RPC的一些主要功能。
4.1 實例級服務注冊與發現
ESA RPC通過深度整合發布平臺,實現實例級服務注冊與發現,如圖所示:
應用發布時,相應的發布平臺會將實例信息注冊到OPPO自研的注冊中心ESA Registry(應用本身則不再進行注冊),注冊信息包含應用名、ip、端口、實例編號等等,消費者啟動時只需通過應用編號訂閱相關提供者即可。
既然服務注冊部分是由發布平臺完成,開發者在發布應用時,就需要填寫相關信息,即相關的暴露協議以及對應的端口,這樣發布平臺才可以正確注冊提供者信息。
4.2 客戶端線程模型優化
ESA RPC全面擁抱java8的CompletableFuture ,我們將同步和異步的請求統一處理,認為同步是特殊的異步。而Dubbo,由于歷史原因,最初dubbo使用的jdk版本還是1.7,所以在客戶端的線程模型中,為了不阻塞IO線程,dubbo增加了一個Cached線程池,所有的IO消息統一都通知到這個Cached線程池中,然后再切換回相應的業務線程,這樣可能會造成當請求并發較高時,客戶端線程暴漲問題,進而導致客戶端性能低下。
所以我們在ESA RPC客戶端優化了線程模型,將原有的dubbo客戶端cached線程池取消,改為如下圖模型:
具體做法:
- 當前業務線程發出遠程調用請求后,生成CompletableFuture 對象,并傳遞至IO線程,等待返
回; - IO線程收到返回內容后,找到與之對應的CompletableFuture 對象,直接賦予其返回內容;
- 業務線程通過自己生成的CompletableFuture 對象獲取返回值;
4.3 智能Failover
對于一些高并發的服務,可能會因傳統Failover 中的重試而導致服務雪崩。ESA RPC對此進行優化,采用基于請求失敗率的Failover ,即當請求失敗率低于相應閾值時,執行正常的failover重試策略,而當失敗率超過閾值時,則停止進行重試,直到失敗率低于閾值再恢復重試功能。
ESA RPC采用RingBuffer 的數據結構記錄請求狀態,成功為0,失敗為1。用戶可通過配置的方式指定該RingBuffer 的長度,以及請求失敗率閾值。
4.4 ServiceKeeper
ESA ServiceKeeper (以下簡稱ServiceKeeper ),屬于OPPO自研的基礎框架技術棧ESA Stack系列的一員。ServiceKeeper 是一款輕量級的服務治理框架,通過攔截并代理原始方法的方式織入限流、并發數限制、熔斷、降級等功能。
ServiceKeeper 支持方法和參數級的服務治理以及動態動態更新配置等功能,包括:
- 方法隔離
- 方法限流
- 方法熔斷
- 方法降級
- 參數級隔離、限流、熔斷
- 方法重試
- 接口分組
- 動態更新配置,實時生效
ESA RPC中默認使用ServiceKeeper 來實現相關服務治理內容,使用起來也相對簡單。
Step 1
application.properties 文件中開啟ServiceKeeper 功能。
# 開啟服務端 esa.rpc.provider.parameter.enable-service-keeper=true# 開啟客戶端 esa.rpc.consumer.parameter.enable-service-keeper=trueStep 2
新增service-keeper.properties 配置文件,并按照如下規則進行配置:
# 接口級配置規則:{interfaceName}/{version}/{group}.{serviceKeeper params},示例: com.oppo.dubbo.demo.DemoService/0.0.1/group1.maxConcurrentLimit=20 com.oppo.dubbo.demo.DemoService/0.0.1/group1.failureRateThreshold=55.5 com.oppo.dubbo.demo.DemoService/0.0.1/group1.forcedOpen=55.5 ...#方法級動態配置規則:{interfaceName}/{version}/{group}.{methodName}.{serviceKeeper params},示例: com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.maxConcurrentLimit=20 com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.maxConcurrentLimit=20 com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.failureRateThreshold=55.5 com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.forcedOpen=false com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.limitForPeriod=600 ...#參數級動態配置規則:{interfaceName}/{version}/{group}.{methodName}.參數別名.配置名稱=配置值列表,示例: com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.arg0.limitForPeriod={LiSi:20,ZhangSan:50} ...4.5 連接管理
ESA RPC中,一個消費者與一個提供者,默認只會創建一個連接,但是允許用戶通過配置創建多個,配置項為connections (與dubbo保持一致)。ESA RPC的連接池通過公司內部一個全異步對象池管理庫commons pool來達到對連接的管理,其中連接的創建、銷毀等操作均為異步執行,避免阻塞線程,提升框架整體性能。
需要注意的是,這里的建連過程,有一個并發問題要解決: 當客戶端在高并發的調用建連方法時,如何保證建立的連接剛好是所設定的個數呢?為了配合 Netty 的無鎖理念,我們也采用一個無鎖化的建連過程來實現,利用 ConcurrentHashMap 的putIfAbsent 方法:
AcquireTask acquireTask = this.pool.get(idx); if (acquireTask == null) {acquireTask = new AcquireTask();AcquireTask tmpTask = this.pool.putIfAbsent(idx, acquireTask);if (tmpTask == null) {acquireTask.create(); //執行真正的建連操作} }4.6 gRPC協議支持
由于ESA RPC默認使用ESA Regsitry 作為注冊中心,由上述實例注冊部分可知,服務注冊通過發布平
臺來完成,所以ESA RPC對于gRPC協議的支持具有天然的優勢,即服務的提供者可以不接入任何sdk,甚至可以是其他非java語言,只需要通過公司發布平臺發布應用后,就可以注冊至注冊中心,消費者也就可以進行訂閱消費。
這里我們以消費端為例,來介紹ESA RPC客戶端如何請求gRPC服務端。
proto文件定義:
syntax = "proto3";option java_multiple_files = false; option java_outer_classname = "HelloWorld"; option objc_class_prefix = "HLW";package esa.rpc.grpc.test.service;// The greeting service definition. service GreeterService {// Sends a greetingrpc sayHello (HelloRequest) returns (HelloReply) {} }service DemoService {// Sends a greetingrpc sayHello (HelloRequest) returns (HelloReply) {} }// The request message containing the user's name. message HelloRequest {string name = 1; }// The response message containing the greetings message HelloReply {string message = 1; }然后maven中添加proto代碼生成插件:
<build><extensions><extension><groupId>kr.motd.maven</groupId><artifactId>os-maven-plugin</artifactId><version>1.5.0.Final</version></extension></extensions><plugins><plugin><groupId>org.xolstice.maven.plugins</groupId><artifactId>protobuf-maven-plugin</artifactId><version>0.5.0</version><configuration><protocArtifact>com.google.protobuf:protoc:3.11.0:exe:${os.detected.classifier} </protocArtifact><pluginId>grpc-java</pluginId><pluginArtifact>esa.rpc:protoc-gen-grpc-java:1.0.0- SNAPSHOT:exe:${os.detected.classifier}</pluginArtifact></configuration><executions><execution><goals><goal>compile</goal><goal>compile-custom</goal></goals></execution></executions></plugin></plugins></build>如上proto定義文件,通過protobuf:compile和protobuf:compile-custom則會生成如下代碼:
可以看到,自動生成的代碼中我們額外生成了相應的java接口。
在dubbo客戶端我們就可以直接使用這個接口進行遠程調用,使用方式:
@Reference(...,protocol="grpc") private DemoService demoService;4.7 ESA RPC性能
這里僅舉一例,展示ESA RPC性能。
5. ESA RPC未來規劃
5.1 ESA RPC如何進行平滑遷移?
由于歷史原因,現公司內部大量使用的是Dubbo作為RPC框架,以及zookeeper注冊中心,如何能夠保證業務的平滑遷移,一直是我們在思考的問題。這個問題想要解答,主要分為以下兩點。
5.1.1 代碼層面
在代碼層面,ESA RPC考慮到這個歷史原因,盡可能的兼容dubbo,盡可能降低遷移成本。但ESA RPC畢竟作為一款新的RPC框架,想要零成本零改動遷移是不可能的,但在沒有dubbo擴展的情況下,改動很小。
5.1.2 整體架構
這一點我們舉例說明,當業務方遷移某一應用至ESA RPC框架時,該應用中消費ABCD四個接口,但這些接口的服務提供者應用并未升級至ESA RPC,接口元數據信息均保存至zookeeper注冊中心當中,而ESA RPC推薦使用的ESA Registry注冊中心中沒有這些提供者信息,這就導致了消費者無法消費這些老的提供者信息。
針對這一問題,后續我們ESA Stack系列會提供相應的數據同步工具,將原zookeeper注冊中心中的服務元數據信息同步到我們ESA Registry中,而zookeeper中的這些信息暫時不刪除(以便老的接口消費者能夠消費),等待均升級完成后,即可停用zookeeper注冊中心。
5.2 自研RPC協議
在上面Dubbo協議解析過程中,我們分析了Dubbo協議的優缺點,了解了Dubbo協議的不足。所以后續的版本升級過程中,自研RPC協議是一個不可忽視的內容。自研RPC協議需要充分考慮安全、性能、Mesh支持、可擴展、兼容性等因素,相信通過自研RPC協議可以使我們的ESA RPC更上一層樓。
5.3 其他
- 多協議暴露
- 同機房優先路由
- 類隔離
- ...
在這篇文章中,我們主要分享了Dubbo協議的分析以及ESA RPC的實踐內容,后續OPPO互聯網技術團隊會繼續分享更多ESA RPC的動態。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的dubbo协议_Dubbo协议解析与OPPO自研ESA RPC框架实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php websocket 帧封装,sw
- 下一篇: matlab里面连接器是什么,连接器