简介:HSF 是阿里团体 RPC/ 服务治理 畛域的标杆,Go 语言又因为其高并发,云原生的个性,领有广大的发展前景和实际场景,服务代理模型只是一种落地场景,除此之外,还有更多的利用场景值得咱们在研发的过程中去摸索和总结。
作者 | 李志信
背景
Dubbo-go 生态包含 Dubbo-go v3.0、v1.5、pixiu 等子项目,在可扩展性上提供了灵便的定制化形式。
家喻户晓,HSF 是阿里团体 RPC/ 服务治理 畛域的标杆框架。HSF-go 是 go 语言实现的 HSF 框架,由中间件团队保护,因为 Go 语言的个性,在跨语言调用场景,云原生组件集成服务代理场景表演重要角色,目前领有 Dapr Binding 实现,并且在函数计算(FC)场景,跨云场景,脱云独立部署场景产生价值,并在钉钉、Lazada、高德等技术团队领有落地场景。HSF-go 属于 Dubbo-go 生态体系内的一环,是开源我的项目 Dubbo-go 的定制化实现。
纵观 HSF-go 的一系列和服务代理相干的场景,我心愿在这里分享一下其作为服务代理的实际与原理,欢送和大家一起交换。
HSF-go 泛化调用模型
1、泛化调用
首先理解一下 Dubbo 的泛化调用,就是不依赖二方包的状况下,通过传入办法名,办法签名和参数值,就能够调用到上游服务。
而 Golang 的泛化调用和 Java 角度略有不同,这与语言个性无关。Go 不反对类继承和办法重载,并且没有二方包的概念。Java 的二方包能够形象为一套由客户端和服务端约定好的接口信息,蕴含接口名、办法名、参数列表、具体参数定义,这些根底概念在任何 RPC 场景都是必须的,只是表现形式不同:对 Java 来说就是二方包,对 gRPC 来说就是 proto 文件以及编译产物,对兼容 Dubbo 协定的 Dubbo-go 来说,就是应用兼容 Java 版本的 Hessian 序列化接口。当然应用 Go 编写 Hessian 接口这种适配形式带来了一些困扰,就是让 Go 开发者写起来比拟头疼的,对应 Java 版本的 POJO 构造和接口存根。
上面是 Dubbo-go 生态习惯写法中,一个应用 Hessian 序列化,兼容 Java 的 Go 客户端例子。
// UserProvider 客户端存根类
type UserProvider struct {
// dubbo 标签,用于适配 go 侧客户端大写办法名 -> java 侧小写办法名,只有 dubbo 协定客户端才须要应用
GetUser func(ctx context.Context, req int32) (*User, error) `dubbo:"getUser"`
}
func init(){
// 注册客户端存根类到框架,实例化客户端接口指针 userProvider
config.SetConsumerService(userProvider)
}
// 字段须要与 Java 侧对应,首字母大写
type User struct {
UserID string
UserFullName string `hessian:"user_full_name"`
UserAge int32 // default convert to "userAge"
Time time.Time
}
func (u *User) JavaClassName() string {return "org.apache.dubbo.User" // 须要与 Java 侧 User 类名对应}
Go 相比于反对办法重载的 Java,对接口的元数据信息依赖较弱,能够更轻松地定位目标办法从而发动调用。但实质上,还是须要下面所提到的“约定好”的接口信息,从而保障能正确命中下游办法,以及保障参数解析正确。
在泛化调用的情景下,在代码上不须要引入“二方包”,在增大了自由度的同时,失去了“二方包”接口的限度,因而客户端须要在泛化调用传递参数时尽可能小心,保障传递的参数齐全和服务端提供的接口对应,从而正确调用。
泛化调用蕴含服务端泛化和客户端泛化调用。如果客户端泛化是把两头代理当做 consumer 端的反向代理,那么服务端泛化就是把两头代理当做服务 provider 端的正向代理,把申请转发到后端真正的服务提供方。服务端泛化,开发者在编写服务时,不须要申明具体的参数,框架将申请解析成通用的办法名和参数列表数组并传递至用户层,开发者编写的代码须要间接操作这些动静的数据,可参考文末的例子。而用的绝对较多的是客户端泛化,即下面聊的,客户端在代码层面并没有拿到服务端提供的接口依赖,而是通过传入办法名和参数,由框架生成泛化调用申请,从而达到和通过实在接口调用一样的成果。
泛化调用申请往往办法名为 $invoke,蕴含三个参数,别离是:
- 实在办法名;
- 参数名组成的数组;
- 参数具体值组成的数组。
以一个 HSF-go 泛化调用申请为例:
// 一个 HSF-go 的客户端泛化调用
genericService.Invoke(context.TODO(),
"getUser",
[]string{(&GoUser{}).JavaClassName(), (&GoUser{}).JavaClassName()},
[]interface{}{&GoUser{Name: "laurence"}, &GoUser{Age: 22}}
)
框架接管到这三个参数后,会结构出泛化申请,发送至服务端。
服务端在接管到泛化申请时,会在一层 filter 中过滤出以 $invoke 为办法名的申请,并结构出实在申请构造,向下层传递,从而实现调用并返回。
以上是 Dubbo 体系泛化调用的通用实现,但如果单纯站在 Go 语言的角度来设计,并不需要传递参数列表类型,服务端能够单纯通过办法名定位到办法,再将参数数组反序列化,取得实在参数。
2、泛化调用与服务运维能力
泛化调用的利用场景很宽泛,团体的开发人员接触最多的泛化调用,可能就是 MSE/HSF-ops 平台提供的服务测试能力。
团体内应用的 MSE 运维平台是一个弱小的、用于 HSF 服务治理的平台,能够在平台上配置运维、服务治理能力、进行服务测试,以及商业化版本 MSE 的压测、流量回放等操作。而其提供的服务测试能力,依赖的就是 HSF 泛化调用。当开发人员在平台上针对一个接口办法发动测试时,会传入一个 json 参数列表,平台会将 json 参数列表转化为 hessian 对象并序列化,结构出下面提到的三参数,并向目标机器发动调用,拿到测试返回值。HSF 服务会默认反对泛化调用。
除了服务测试,还能够应用泛化调用来开发服务网关、服务探活、cli 服务测试工具等。
3、泛化调用与序列化协定的关系
常见的序列化协定很多,例如 Dubbo/HSF 默认的 hessian2 序列化;还有应用宽泛的 JSON 序列化;以及 gRPC 原生反对的 protobuf(PB) 序列化等等。
提到的这三种典型的序列化计划作用相似,但在实现和开发中略有不同。PB 不可由序列化后的字节流间接生成内存对象,而 Hessian 和 JSON 都是能够的。后两者反序列化的过程不依赖“二方包”,也能够说是存根。一个更好了解的办法是,PB 能够了解为一种相似于对称加密协议,在客户端和服务端必须有存根的状况下,能力解析出对象,而 hessian 和 json 不依赖存根,这决定了 pb 的压缩成果更好。
这也能够解释为什么,应用 PB 序列化的 Triple(Dubbo3) 协定并没有被咱们罕用的服务运维平台的测试性能所反对。因为上述泛化调用模型只能结构可凭空解析的序列化类型。
如果切实要泛化调用 PB 序列化服务,解决方案还是有的,还是用对称加密举例,只有我拿到和服务端统一的“密钥“,我就能够结构出对方可解析的构造,从而发动泛化调用。这就是 gRPC 反射服务 的原理,反射服务能够让客户端在发动调用之前,拿到这份 proto 接口定义文件,从而取得对称加密的“密钥”,在这份密钥的根底上,填写好参数字段,就能像失常客户端一样发动调用了。
HSF-go 在 Dapr 场景的实际
下面次要聊了 Dubbo 体系的泛化调用模型,下面也提到了,泛化调用的利用场景十分多,也成为了 Dapr 落地的根底之一。Dapr 是阿里云单干的,微软开源的 CNCF 孵化我的项目,交融了标准化 API、组件可扩大 SPI 机制、边车架构、Serverless 等诸多先进理念,在阿里团体有 FC,跨云等许多生产落地场景。
1、Dapr Binding 模型
Dapr 标准化 API 理念是十分新鲜和实用的,其中 Bindings 结构块, 是咱们服务调用解决方案的根底。
Bindings 最直观的了解,是介于用户利用运行时和网络之间的一层流量中间件。
上图能够解释基于 Binding 的整条调用链路,由用户利用运行时调用 Dapr 标准化接口从而发动调用。由 Dapr 运行时将流量交给可扩大的 Binding 结构块,Dapr 能够这种统一化接口和可扩大能力,很不便地反对多种协定的切换,按需激活。如图中舒展进去的 HSF、Dubbo 反对。
被激活的例如 HSF-go 结构块将接管这一申请,将来自利用的标准化的申请头和申请体解析进去,生成 HSF 协定申请,Dapr 边车个别不会领有上游服务二方包,因而这一申请肯定是泛化调用申请。
当然,在申请收回之前,早已实现了服务发现过程,这是用户以及利用运行时无感的,由 Dapr 来接管和封装。下面提到的泛化申请在实现服务发现之后,即可被发送至目标机器 ip,被上游的 Inbound Binding 的 HSF-go 实现所接管和解决,这个上游的组件对应下面提到的“服务端泛化调用”,他承受任何 HSF 申请。上游将 HSF 协定解析进去,参数从泛化调用的三个参数标准化为失常申请参数后,通过 Dapr 提供的 Callback 机制传递至利用运行时。
在这一过程中,泛化调用表演了极其重要的角色,在客户端负责出流量的 HSF 协定泛化调用发动,在服务端负责入流量的泛化调用解析和传递。
我认为,Dapr 绑定的网络协议模型,是 RPC 协定进一步形象的体现。将所有的 RPC 协定形象为 metadata(元数据)和 body 两局部,用户利用 /SDK 侧只须要关怀这两局部的内容。一旦将这个形象的申请构造交给 Dapr,具体协定的生成,就由具体激活的结构块来做了,这是我认为 Dapr 提供的一种很精美的服务调用形象设计。
2、序列化数组透传的设计
下面提到的入流量与出流量组件都是泛化调用的实现,但如果细究,并不是第一节咱们提到的传统泛化调用。
传统泛化调用的入参是构造,调用过程波及到序列化过程。在 Dapr 这种边车场景下,一次残缺的 RPC 调用将会引入至多六次序列化 / 反序列化过程,这老本是微小的。
因而在设计中,并没有应用规范泛化调用过程,而是将序列化过程省略掉了,只保留了利用侧的一次序列化,Dapr 边车针对参数局部只进行透传解决。这样来,大大减少了无谓的耗费。
这样一来,在客户端 Outbound 的实现,就成了针对如下泛化调用接口的应用:
// args 参数为序列化后的 byte 数组
ProxyInvokeWithBytes(ctx context.Context, methodName string, argsTypes []string, args [][]byte) ([]byte, error)
在服务端 Inbound 的实现,也成了针对 byte 数组类型参数的泛化调用
// inbound 入参
type RawDataServiceRequest struct {
RequestContext *core.RequestContext
Method string
ArgsTypes []string
Args [][]byte // args 参数为序列化后的 byte 数组
Attachment map[string]interface{}
RequestProps []byte}
相当于在泛化调用的根底上,删除了序列化操作,将申请参数透传。
HSF-go 服务代理的设计
钉钉团队领有很多 Go 语言落地场景,在 Dubbo-go 生态我的项目的倒退过程中提供了诸多帮忙与实际。
在跨集群通信解决方案中,代理网关是必不可少的,大多数网关须要运维人员手动进行流量配置。局部网关对网络协议存在要求,例如 envoy,因而中间件团队推出基于 Http2 的 Dubbo3(Triple) 协定的起因之一,就是为了适配网关。
在跨集群 RPC 场景下,现实状况是在网关层不须要进行协定转换,并且不须要进行序列化 / 反序列化过程,并且将服务治理能力交融在网关外部,从而缩小资源耗费和运维老本。
这也提出了一种诉求,在团体内跨云场景下,咱们须要建设一个反对原生 HSF 协定的代理网关,从而容许集群内部的客户端在无感的状况下,将申请切流量至集群外部,由网关承受来自外界的 HSF 申请,并动静进行服务发现流程,将申请流量转发至集群内对应服务提供者。能够想到,泛化调用在这个过程中将表演重要角色。
咱们沿着之前 Dapr 的思路,如上图所示,将视角从整个调用链路转移到单个实例上,能够看到一个实例能够承受泛化申请,并也能够发动泛化申请,在泛化过程中不波及序列化过程。这个咱们所关注的实例,就是一个网关的形象体现。
领有了这样的网关,咱们能够实现客户端无感的跨集群调用。在必要的状况下,能够在客户端所在环境进行代理注册。
这样的网关是单向的,能够解决从内部进入外部的流量,如果心愿双向买通,跨集群的统一化注册核心将是必要的。在这种状况下,网关须要依据流量查问多个注册核心的信息,从而保障链路正确。
总结
HSF 是阿里团体 RPC/ 服务治理 畛域的标杆,Go 语言又因为其高并发,云原生的个性,领有广大的发展前景和实际场景,服务代理模型只是一种落地场景,除此之外,还有更多的利用场景值得咱们在研发的过程中去摸索和总结。
Dubbo/HSF 生态、Dubbo-go 技术体系将携手用户一起打磨与实际。
原文链接
本文为阿里云原创内容,未经容许不得转载。