01-神魔宇宙 ***宇宙天國首部曲 彌勒天書閣 https://maitreya-books.com/ 神話從來就不是怪力亂神,而是原始先民在日常生活情況的觀察之中,所建立的真實認知。唯有相信神話中的真實,才能感受到神話的詩意隱喻背後,所具有的神聖力量。打開你的想像,打開你的相信,你才能知道神話告訴了你什麼宇宙生命的資訊。 生命起源於宇宙之中,生長於宇宙之中,結束於宇宙之中,因此,宇宙的起源就是生命背景的起源。生命形成的每一個階段,其實都在述說著生命的本能,也就是生命本就存在的一種力量,在此雖是說明一種狀況,然而將這狀況投射在生命的生活行動之中,你就會明白自己究竟有哪些不可思議的本能!

Kitex 支持 Dubbo 協議:助力多語言雲原生生態融合

2024022311:36

背 景 : Kitex 是字節跳動基礎架構服務框架團隊推出的 Go 微服務 RPC 框架,支持 Thrift、Kitex Protobuf、gRPC 等消息協議,具有高性能、強可擴展的特點。

于 2021 年 9 月正式開源後,已在多家外部企業成功落地,爲他們帶來了真實的成本、性能和穩定性收益。

很多企業用戶在使用 Kitex 改造服務的過程中,需要 Kitex 能與現有的 Dubbo 框架實現的服務進行通信,這與 CloudWeGo 社區積極拓展生態的目標不謀而合,因此 Dubbo 互通項目 codec-dubbo 應運而生。

在社區同學的熱情幫助下,目前 codec-dubbo 能做到 Kitex 與 Dubbo-Java,Kitex 與 Dubbo-Go 互通,支持 Dubbo 用戶向 Kitex 遷移。


本文將以方正證券利用 Kitex 與 codec-dubbo 成功進行服務改造爲例,對改造過程中使用到的 codec-dubbo 主要功能進行闡述,並簡要分析其中的實現細節。

企業落地案例

方正證券原有的服務采用 Java 和 Dubbo 框架編寫,兩者穩定且經過了大量場景的驗證,符合他們的生産和開發需求。以請求量較大的小方個股詳情頁爲例,高峰期的接口 QPS 在 3-4k,使用 16 台 16 Core 64G 虛擬機進行承載。

隨著雲原生架構的興起,憑借內存占用與執行效率的優勢以及天然適配雲原生,Go 逐漸成爲構建企業服務的重要技術選項。爲了更好地降本增效,綜合考慮成本、性能和穩定性等因素後,他們決定在新建應用上由 Java 轉向 Go,引入 Kitex,Hertz 等 CloudWeGo 項目進行服務開發與重構,並整體遷移至 Kubernetes 環境。

在重構過程中,codec-dubbo 憑借接近原生 Kitex + Thrift 的使用體驗以及對 Dubbo 概念的良好支持,降低了使用和理解成本,成功幫助他們解決了 Kitex <-> Dubbo 的互通問題,讓 Kitex 服務順利調用原有的 Dubbo 服務。

目前,使用了 codec-dubbo 的 Kitex 服務已成功上線,穩定運行兩個月。還是以小方個股詳情頁爲例,Kitex 和 Hertz 承載了該頁面一半左右的接口,在 QPS 不變的情況下,只需要提供 12 個 4 Core 4G Pod,降低資源占用效果顯著。

codec-dubbo 功能特性

Dubbo 協議編解碼器

Dubbo 服務主要使用 Dubbo 協議進行通信,爲了支持 Kitex <-> Dubbo 互通,我們需要在 Kitex 中實現 Dubbo 協議。得益于 Kitex 優秀的擴展性,codec-dubbo 根據 Kitex 提供的 Codec 接口實現了 DubboCodec 這一核心編解碼器,只需在初始化時注入 DubboCodec 便能使用 Dubbo 協議。

類型映射與拓展

類型映射

Dubbo 主要使用 Hessian2 序列化協議進行 Payload 的編解碼,它最大的特點是自描述序列化類型,即不依賴外部 Schema 或接口定義。序列化過程依賴編程語言類型和 Hessian2 類型之間的映射,以 Go 類型轉化爲 Java 類型爲例:


經過分析,我們發現 Hessian2 的基礎類型系統與 Thrift 基本重合。爲了保證 Kitex + codec-dubbo 的使用體驗與 Kitex + Thrift 基本一致,我們基于 Thrift IDL 來生成 Kitex Dubbo-Hessian2 腳手架代碼,此時類型轉化過程如下所示:


參考 Dubbo 官方的 dubbo-go-hessian2 類型映射,codec-dubbo 提供如下類型映射 (此處僅包含部分映射,更多注意事項請參考 codec-dubbo Readme ):



根據 codec-dubbo 提供的類型映射,我們能很輕松地將 Dubbo 接口定義轉化爲 Thrift IDL,並使用 Kitex 命令行工具生成項目腳手架代碼,最終注入 DubboCodec 完成 Kitex -> Dubbo 的通信。以下方 Dubbo 接口定義爲例:

package org.cloudwego.kitex.samples.api;
public interface GreetProvider {GreetResponse Greet(GreetRequest req) throws Exception;
public class GreetRequest implements Serializable {String req;
public GreetRequest(String req) {this.req = req;
public class GreetResponse implements Serializable {String resp;
public GreetResponse(String resp) {this.resp = resp;

對應的 api.thrift 文件如下所示,需要注意到其中的結構體定義都需要加上 JavaClassName 的注解,對應 Dubbo 接口定義中的 package + 類名。

struct GreetRequest {1: required string req,} (JavaClassName="org.cloudwego.kitex.samples.api.GreetRequest")
struct GreetResponse {1: required string resp,} (JavaClassName="org.cloudwego.kitex.samples.api.GreetResponse")
service GreetService {GreetResponse Greet(1: GreetRequest req)

使用 Kitex 命令行工具,並指定協議爲 Hessian2:

kitex -module demo-client -protocol Hessian2 ./api.thrift

之後初始化 DubboCodec 並將其注入 Kitex ,利用生成代碼編寫以下 Client 端代碼即可實現 Kitex -> Dubbo 調用:

javaClass := "org.cloudwego.kitex.samples.api.GreetProvider"cli, err := greetservice.NewClient("helloworld",// 指定想要訪問的服務端地址,也支持 ZooKeeper 服務發現client.WithHostPorts("127.0.0.1:21000"),// 配置 DubboCodecclient.WithCodec(// 指定想要調用的 Dubbo Interfacedubbo.NewDubboCodec(dubbo.WithJavaClassName(javaClass))),if err != nil {panic(err)resp, err := cli.Greet(context.Background(),&hello.GreetRequest{Req: "world"})if err != nil {klog.Error(err)returnklog.Infof("resp: %s", resp.Resp)

Kitex + codec-dubbo Server 端流程與 Client 端基本類似,具體例子可參考項目主頁。

類型拓展

Hessian2 schema-free 的特性導致 Dubbo 的實現“過于靈活”,可以使用任意類型。爲了適配 Dubbo Hessian2 的類型使用靈活性,codec-dubbo 支持類型拓展,其中主要包括自定義映射與 Java 常用類型拓展。

自定義映射

Java 的基礎類型有與之對應的包裝類型,例如 boolean 與 java.lang.Boolean。類型映射中默認將 Go 的 bool 類型映射到 Java 的 java.lang.Boolean 類型並不能覆蓋到使用 boolean 的情況。

爲了統一用戶使用體驗,讓他們在 Kitex 側只需使用 bool 類型,我們可以在 Thrift 的方法定義後面加上 hessian.argsType="boolean"注解,利用 thriftgo 的 IDL 反射功能,提前生成 IDL 元信息並注入 codec-dubbo,便可以在運行時動態地將默認映射類型 java.lang.Boolean 改寫成 boolean 。具體 Thrift 定義如下所示:

service EchoService {bool EchoBoolean(1: bool req) (hessian.argsType="boolean")

與 boolean 和 java.lang.Boolean 類似,其他 Java 基礎類型和包裝類型也能通過這種方式進行自定義映射,此時 codec-dubbo 提供的完整類型映射如下:



java 常用類型拓展

由于 Thrift 類型的局限性,我們無法直接使用 Java 類庫中提供的常用類型。爲此, codec-dubbo 在 codec-dubbo/java 包中維護了 Thrift 不支持的 Java 類型 (例如 java.lang.Object、java.util.Date) 以及與之對應的 java.thrift ,同時借助 thriftgo 提供的 idl-ref 功能,我們可以直接在 Thrift IDL 中引用這些類型並生成相應代碼。當前的 java.thrift 如下所示:

struct Object {} (JavaClassName="java.lang.Object")
struct Date {} (JavaClassName="java.util.Date")
struct Exception {} (JavaClassName="java.lang.Exception")

爲了啓用這些類型,我們需要在 Thrift IDL 中使用 include "java.thrift" 導入它們,並且在使用 Kitex 命令行工具生成代碼時添加 -hessian2 java_extension 參數來拉取該拓展包。

Kitex 命令行工具會自動下載 java.thrift ,你也可以手動下載後放到項目的根目錄。引用 java.thrift 中類型的 Thrift IDL 示例:

include "java.thrift"
service EchoService {// java.lang.Objecti64 EchoString2ObjectMap(1: map req)// java.util.Datei64 EchoDate(1: java.Date req)

方法重載

Go 原生不支持方法重載,只能通過定義多個方法來達到類似重載的效果。爲了將 Go 中的多個方法映射到 Java 中的重載方法,與自定義映射一節類似,我們在 Thrift 的方法定義後面加上 JavaMethodName 標簽,借助 thriftgo 的 IDL 反射功能在運行時動態地將 Go 側原本的方法名改寫成 JavaMethodName 指定的 Java 側中的重載方法。

以 Java 側的 EchoMethod 爲例:

String EchoMethod(Boolean req);String EchoMethod(Integer req);String EchoMethod(int req);String EchoMethod(Boolean req1, Integer req2);

我們編寫如下 Thrift 定義,即可完成 Go 與 Java 間的重載方法映射,注意到 JavaMethodName 和 hessian.argsType 可以同時使用:

service EchoService {string EchoMethodA(1: bool req) (JavaMethodName="EchoMethod")string EchoMethodB(1: i32 req) (JavaMethodName="EchoMethod")string EchoMethodC(1: i32 req) (JavaMethodName="EchoMethod", hessian.argsType="int")string EchoMethodD(1: bool req1, 2: i32 req2) (JavaMethodName="EchoMethod")

異常處理

codec-dubbo 將 Java 中的異常映射爲 Go 中的錯誤,這些錯誤統一實現以下接口:

type Throwabler interface {Error() stringJavaClassName() stringGetStackTrace() []StackTraceElement

根據 Dubbo 官方推薦的異常處理實踐以及企業用戶目前的需求,我們將異常劃分爲常見異常與自定義異常,同時兼顧用戶的基礎需求以及可擴展需求。

常見異常

codec-dubbo 在 pkg/hessian2/exception 包中提供了 Java 常見的異常,目前支持 java.lang.Exception 。

常見異常無需 Kitex 命令行工具的支持,直接引用即可,以下是 Client 端提取異常和 Server 端返回異常的示例。

Client 端提取異常

resp, err := cli.Greet(context.Background(),&hello.GreetRequest{Req: "world"})if err != nil {// FromError 返回 ThrowablerexceptionRaw, ok := hessian2_exception.FromError(err)if !ok {// 視作常規錯誤處理} else {// 若不關心 exceptionRaw 的具體類型,直接調用 Throwabler 提供的方法即可klog.Errorf("get %s type Exception", exceptionRaw.JavaClassName())// 若想獲得 exceptionRaw 的具體類型,需要進行類型轉換,但前提是已知該具體類型exception := exceptionRaw.(*hessian2_exception.Exception)

Server 端返回異常

func (s *GreetServiceImpl) Greet(ctx context.Context, req *hello.GreetRequest) (resp *hello.GreetResponse, err error) {return nil, hessian2_exception.NewException("Your detailed message")

自定義異常

Java 中的自定義異常往往會繼承一個基礎異常,這裏以 CustomizedException 爲例,CustomizedException 繼承了 java.lang.Exception:

public class CustomizedException extends Exception {private final String customizedMessage;public CustomizedException(String customizedMessage) {super();this.customizedMessage = customizedMessage;

得益于 thriftgo 支持生成嵌套結構體,爲了在 Kitex 側定義與之對應的異常,我們在 Thrift 中編寫如下定義:

exception CustomizedException {// thrift.nested=“true” 注解讓 thriftgo 生成嵌套結構體1: required java.Exception exception (thrift.nested="true")2: required string customizedMessage}(JavaClassName="org.cloudwego.kitex.samples.api.CustomizedException")

注意 exception 字段的注解 thrift.nested="true",它讓 thriftgo 生成嵌套結構體,達到類似繼承的效果。

和 Java 常用類型擴展一樣,需要在使用 kitex 腳手架工具生成代碼時添加 -hessian2 java_extension 參數來拉取拓展包,生成代碼如下:

type EchoCustomizedException struct {java.Exception `thrift:"exception,1,required" frugal:"1,required,java.Exception" json:"exception"`
CustomizedMessage string `thrift:"customizedMessage,2,required" frugal:"2,required,string" json:"customizedMessage"`

使用方法與常見異常一致,此處不再贅述。

服務注冊與發現

Dubbo 同時提供接口級與應用級服務注冊發現模型,根據企業用戶當前的生産環境需要,我們選擇優先實現基于 zookeeper 的接口級模型:Dubbo registry-zookeeper。

與我們熟知的應用級模型不同,接口級模型需要維護接口名 => 服務 (不同于微服務,更接近 Handler ) 的映射關系,一個接口名會映射到多個服務 ,這些服務可能會存在于同一個進程中。

考慮到 Dubbo 的接口級服務模型與 Kitex 的服務模型差別較大,且 Dubbo registry-zookeeper 應綁定 codec-dubbo 使用,因此不考慮修改 kitex-contrib 中原有的 registry-zookeeper,讓 dubbo registry-zookeeper 成爲 codec-dubbo 的一個子 go module 統一進行開發與維護。

綜合考慮 Dubbo 接口級服務模型、Kitex API 與用戶的使用體驗,我們提供以下的配置層次:

    registry/options.go 與 resolver/options.go 中的 WithServers 和 WithRegistryGroup 函數提供注冊中心級別的配置,分別指定 zookeeper 的地址和這些 zookeeper 所屬的組。用戶使用這些函數生成 Kitex 中 registry.Registry 和 discovery.Resolver 實例。

    服務級別的配置由 client.WithTag 與 server.WithRegistryInfo 進行傳遞,registries/common.go 提供 Tag keys ,這些 key 與 Dubbo 中的服務元數據一一對應。

resolver 示例

intfName := "org.cloudwego.kitex.samples.api.GreetProvider"res, err := resolver.NewZookeeperResolver(// 指定 zookeeper 服務器的地址,可指定多個,請至少指定一個resolver.WithServers("127.0.0.1:2181"),if err != nil {panic(err)cli, err := greetservice.NewClient("helloworld",// 配置 ZookeeperResolverclient.WithResolver(res),// 指定想要調用的 dubbo Interfaceclient.WithTag(registries.DubboServiceInterfaceKey, intfName),if err != nil {panic(err)// 使用 cli 進行 RPC 調用

registry 示例

intfName := "org.cloudwego.kitex.samples.api.GreetProvider"reg, err := registry.NewZookeeperRegistry(// 指定 zookeeper 服務器的地址,可指定多個,請至少指定一個registry.WithServers("127.0.0.1:2181"),if err != nil {panic(err)
svr := greetservice.NewServer(new(GreetServiceImpl),server.WithRegistry(reg),// 配置dubbo URL元數據server.WithRegistryInfo(&kitex_registry.Info{Tags: map[string]string{registries.DubboServiceInterfaceKey: intfName,// application請與dubbo所設置的ApplicationConfig保持一致,此處僅爲示例registries.DubboServiceApplicationKey: "application-name",}),// 啓動 svr

總 結 : Kitex 支持了 Dubbo 協議,是 CloudWeGo 助力多語言雲原生生態融合的一大步,解決了衆多企業用戶 Java 轉 Go 、 Java 與 Go 並存的痛點,歡迎大家試用和接入;如果在使用過程遇到任何問題,可以加入我們的飛書用戶群,或者在 Github 上給我們提反饋。---[作者 : 王宇軒 ,Kitex Committer/來源: InfoQ ]