乐趣区

关于后端:Kitex-支持-Dubbo-协议助力多语言云原生生态融合

作者:王宇轩(github: DMwangnima),Kitex Committer

一、背景

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"),
    // 配置 DubboCodec
    client.WithCodec(
        // 指定想要调用的 Dubbo Interface
        dubbo.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)
    return
}
klog.Infof("resp: %s", resp.Resp)

Kitex + codec-dubbo Server 端流程与 Client 端根本相似,具体例子可参考我的项目主页。

类型拓展

Hessian2 schema-free 的个性导致 Dubbo 的实现“过于灵便”,能够应用任意类型。为了适配 Dubbo Hessian2 的类型应用灵活性,codec-dubbo 反对类型拓展,其中次要包含自定义映射与 Java 罕用类型拓展。

自定义映射

Java 的根底类型有与之对应的包装类型,例如 booleanjava.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")
}

booleanjava.lang.Boolean 相似,其余 Java 根底类型和包装类型也能通过这种形式进行自定义映射,此时 codec-dubbo 提供的残缺类型映射如下:

java 罕用类型拓展

因为 Thrift 类型的局限性,咱们无奈间接应用 Java 类库中提供的罕用类型。为此,codec-dubbo 在 codec-dubbo/java 包中保护了 Thrift 不反对的 Java 类型 (例如java.lang.Objectjava.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.Object
    i64 EchoString2ObjectMap(1: map<string, java.Object> req)
    // java.util.Date
    i64 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 间的重载办法映射,留神到 JavaMethodNamehessian.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() string
    JavaClassName() string
    GetStackTrace() []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 返回 Throwabler
    exceptionRaw, 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 与用户的应用体验,咱们提供以下的配置档次:

  1. registry/options.go 与 resolver/options.go 中的 WithServers 和 WithRegistryGroup 函数提供注册核心级别的配置,别离指定 zookeeper 的地址和这些 zookeeper 所属的组。用户应用这些函数生成 Kitex 中 registry.Registrydiscovery.Resolver 实例。
  2. 服务级别的配置由 client.WithTagserver.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",
    // 配置 ZookeeperResolver
    client.WithResolver(res),
    // 指定想要调用的 dubbo Interface
    client.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 上给咱们提反馈。

退出移动版