泛化调用

泛化调用是 Dubbo-Go 的一种特殊调用方式,它允许中间节点在没有接口信息的情况下传递调用信息,常用于测试和网关场景。泛化调用支持 Dubbo 和 Triple 协议,但目前的序列化方案仅支持 Hessian。

背景

为了便于理解,本文档以网关使用场景为例介绍泛化调用。我们先来考虑普通调用(非泛化调用)。下图包含了消费者和提供者两个关键角色(endpoint 在下文中用来代表消费者或提供者),并且各自拥有 org.apache.dubbo.sample.User 接口的定义。假设在调用行为中需要使用 org.apache.dubbo.sample.User 接口。

img

RPC 需要通过网络介质进行传输,因此数据不能以 go struct 的形式进行传输,而必须以二进制的形式进行传输。这就要求消费者端在传输前将实现了 org.apache.dubbo.sample.User 接口的结构体序列化成二进制格式。同样的,对于提供者端来说,需要将二进制数据反序列化成结构体信息。简而言之,普通调用要求接口信息在各个 endpoint 必须拥有相同的定义,这样才能保证数据序列化和反序列化的结果符合预期

在网关场景下,网关不可能存储所有的接口定义。例如,一个网关需要转发 100 个服务的调用,每个服务需要的接口数量是 10 个。普通调用就要求网关中预先存储所有的 1000(100 * 10)个接口定义,这显然是难以实现的。那么,有没有一种办法可以在不预先存储接口定义的情况下,正确地转发调用呢?答案是肯定的,这也是泛化调用的意义所在。

原理

泛化调用的本质是将复杂结构体转换成通用结构体。这里提到的通用结构体指的是 map、string 等,网关可以顺利地解析和传递这些通用结构体。

img

目前,Dubbo-go v3 只支持 Map 泛化(默认)。我们以 User 接口为例,其定义如下。

// definition
type User struct {
ID string
name string
Age int32
}

func (u *User) JavaClassName() string {
return "org.apache.dubbo.sample.User"
}

假设调用一个服务需要传入一个 user 作为入参,其定义如下。

// an instance of the User
user := &User{
    ID: "1",
    Name: "Zhangsan",
    Age: 20,
}

那么,在使用 Map 泛化时,user 会被自动转换成 Map 格式,如下所示。

usermap := map[interface{}]interface{} {
    "iD": "1",
    "name": "zhangsan",
    "age": 20,
    "class": "org.apache.dubbo.sample.User",
}

需要注意的是

  • Map 泛化会自动将首字母小写,即 ID 会被转换成 iD。如果需要对齐 Dubbo-Java,请考虑将 ID 改为 Id;
  • Map 中自动插入了一个类字段,用于标识原接口类。

使用

泛化调用对提供者端是透明的,即提供者端无需任何显式配置即可正确处理泛化请求。

基于 Dubbo URL 的泛化调用

基于 Filter 泛化的调用对消费者是透明的,典型的应用场景是网关。这种方式需要要求 Dubbo URL 中包含泛化调用标识,如下所示。

dubbo://127.0.0.1:20000/org.apache.dubbo.sample.UserProvider?generic=true&...

该 Dubbo URL 表达的含义是

  • RPC 协议为 dubbo;
  • 127.0.0.1:20000 上的 org.apache.dubbo.sample.UserProvider 接口;
  • 使用泛化调用(generic=true)。

Consumer 端的 Filter 会根据 Dubbo URL 携带的配置自动将普通调用转换成泛化调用,但需要注意的是,这种方式下,响应结果是以泛化的格式返回的,不会自动转换成对应的对象。例如,在 map 泛化模式下,如果需要返回 User 类,那么消费者得到的是一个对应 User 类的 map。

手动泛化调用

手动泛化调用发起的请求不经过 filter,因此需要消费者端显式地发起泛化调用。典型的应用场景是测试。在 dubbo-go-samples 中,为了测试方便,使用了手动调用。

泛化调用不需要创建配置文件(dubbogo.yaml),但需要在代码中手动配置注册中心、引用等信息。初始化方法封装在了 newRefConf 方法中,如下所示。

func newRefConf(appName, iface, protocol string) config.ReferenceConfig {
registryConfig := &config.RegistryConfig{
Protocol: "zookeeper",
Address: "127.0.0.1:2181",
}

refConf := config.ReferenceConfig{
InterfaceName: iface,
Cluster: "failover",
Registry: []string{"zk"},
Protocol: protocol,
Generic: "true",
}

rootConfig := config.NewRootConfig(config.WithRootRegistryConfig("zk", registryConfig))
_ = rootConfig.Init()
_ = refConf.Init(rootConfig)
refConf. GenericLoad(appName)

return refConf
}

newRefConf 方法接收三个参数,分别是

  • appName:应用名;
  • iface:服务接口名;
  • protocol:RPC 协议,目前只支持 dubbo 和 tri(triple 协议)。

在上述方法中,为了保持函数简洁,注册中心被设置为固定值,即使用 127.0.0.1:2181 的 ZooKeeper 作为注册中心,在实际应用中可以根据实际情况自由定制。

我们可以轻松地获取一个 ReferenceConfig 实例,暂命名为 refConf。

refConf := newRefConf("example.dubbo.io", "org.apache.dubbo.sample.UserProvider", "tri")

然后我们可以对 org.apache.dubbo.sample.UserProvider 服务的 GetUser 方法发起泛化调用。

resp, err := refConf.
GetRPCService().(*generic.GenericService).
Invoke(
context. TODO(),
"GetUser",
[]string{"java. lang. String"},
[]hessian. Object{"A003"},
    )

GenericService 的 Invoke 方法接收四个参数,分别是

  • context;
  • 方法名:在本例中,表示调用 GetUser 方法;
  • 参数类型:GetUser 方法接收一个字符串类型的参数,如果目标方法接收多个参数,则可以写成 []string{"type1", "type2", ...},如果当前方法没有参数,则需要填写一个空数组 []string{}
  • 实际参数:写法与参数类型相同,如果是无参函数,依然要填入一个空数组 []hessian.Object{}

注意:在当前版本中,无参调用时会出现崩溃问题。

相关阅读:[Dubbo-go 服务代理模型]


上次修改时间:2024 年 1 月 17 日:修复损坏的链接 (6651e217e73)