Dubbo-go 服务代理模型

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 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 最直观的理解,是介于用户应用运行时和网络之间的一层流量中间件。

image.gif1.png

上图可以解释基于 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.png


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 请求,并动态进行服务发现流程,将请求流量转发至集群内对应服务提供者。可以想到,泛化调用在这个过程中将扮演重要角色。

3.png

我们沿着之前 Dapr 的思路,如上图所示,将视角从整个调用链路转移到单个实例上,可以看到一个实例可以接受泛化请求,并也可以发起泛化请求,在泛化过程中不涉及序列化过程。这个我们所关注的实例,就是一个网关的抽象表现。


拥有了这样的网关,我们可以实现客户端无感的跨集群调用。在必要的情况下,可以在客户端所在环境进行代理注册。

4.png

这样的网关是单向的,可以处理从外部进入内部的流量,如果希望双向打通,跨集群的统一化注册中心将是必要的。在这种情况下,网关需要根据流量查询多个注册中心的信息,从而保证链路正确。

5.png

总结


HSF 是阿里集团 RPC/服务治理 领域的标杆,Go 语言又因为其高并发,云原生的特性,拥有广阔的发展前景和实践场景,服务代理模型只是一种落地场景,除此之外,还有更多的应用场景值得我们在研发的过程中去探索和总结。


Dubbo/HSF 生态、Dubbo-go 技术体系将携手用户一同打磨与实践。


欢迎加入 Dubbo-go 社区群,钉钉搜群号 23331795


作者介绍:李志信,来自阿里中间件团队,Apache Dubbo PMC,Dubbo-go 3.0 负责人,Dapr 贡献者。专注于云原生中间件的研发与开源,以及边缘计算工作。

拓展链接:


1、参考例子

https://github.com/apache/dubbo-samples/blob/master/dubbo-samples-generic/dubbo-samples-generic-impl/dubbo-samples-generic-impl-provider/src/main/java/org/apache/dubbo/samples/generic/call/impl/GenericImplOfHelloService.java


2、gRPC 反射服务

https://github.com/grpc/grpc/blob/master/doc/server-reflection.md


3、Bindings 构造块

https://docs.dapr.io/developing-applications/building-blocks/bindings/

相关实践学习
基于函数计算一键部署掌上游戏机
本场景介绍如何使用阿里云计算服务命令快速搭建一个掌上游戏机。
建立 Serverless 思维
本课程包括: Serverless 应用引擎的概念, 为开发者带来的实际价值, 以及让您了解常见的 Serverless 架构模式
相关文章
|
1月前
|
监控 算法 Go
Golang深入浅出之-Go语言中的服务熔断、降级与限流策略
【5月更文挑战第4天】本文探讨了分布式系统中保障稳定性的重要策略:服务熔断、降级和限流。服务熔断通过快速失败和暂停故障服务调用来保护系统;服务降级在压力大时提供有限功能以保持整体可用性;限流控制访问频率,防止过载。文中列举了常见问题、解决方案,并提供了Go语言实现示例。合理应用这些策略能增强系统韧性和可用性。
95 0
|
1月前
|
XML Dubbo Java
【Dubbo3高级特性】「框架与服务」服务的异步调用实践以及开发模式
【Dubbo3高级特性】「框架与服务」服务的异步调用实践以及开发模式
46 0
|
7天前
|
缓存 Java Go
如何用Go语言构建高性能服务
【6月更文挑战第8天】Go语言凭借其并发能力和简洁语法,成为构建高性能服务的首选。本文关注使用Go语言的关键设计原则(简洁、并发、错误处理和资源管理)、性能优化技巧(减少内存分配、使用缓存、避免锁竞争、优化数据结构和利用并发模式)以及代码示例,展示如何构建HTTP服务器。通过遵循这些原则和技巧,可创建出稳定、高效的Go服务。
|
10天前
|
关系型数据库 Go 开发工具
|
26天前
|
缓存 负载均衡 网络协议
使用Go语言开发高性能服务的深度解析
【5月更文挑战第21天】本文深入探讨了使用Go语言开发高性能服务的技巧,强调了Go的并发性能、内存管理和网络编程优势。关键点包括:1) 利用goroutine和channel进行并发处理,通过goroutine池优化资源;2) 注意内存管理,减少不必要的分配和释放,使用pprof分析;3) 使用非阻塞I/O和连接池提升网络性能,结合HTTP/2和负载均衡技术;4) 通过性能分析、代码优化、缓存和压缩等手段进一步提升服务性能。掌握这些技术能帮助开发者构建更高效稳定的服务。
|
1月前
|
负载均衡 算法 Go
Golang深入浅出之-Go语言中的服务注册与发现机制
【5月更文挑战第4天】本文探讨了Go语言中服务注册与发现的关键原理和实践,包括服务注册、心跳机制、一致性问题和负载均衡策略。示例代码演示了使用Consul进行服务注册和客户端发现服务的实现。在实际应用中,需要解决心跳失效、注册信息一致性和服务负载均衡等问题,以确保微服务架构的稳定性和效率。
31 3
|
1月前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第2天】Go语言的并发编程基于CSP模型,强调通过通信共享内存。核心概念是goroutines(轻量级线程)和channels(用于goroutines间安全数据传输)。常见问题包括数据竞争、死锁和goroutine管理。避免策略包括使用同步原语、复用channel和控制并发。示例展示了如何使用channel和`sync.WaitGroup`避免死锁。理解并发原则和正确应用CSP模型是编写高效安全并发程序的关键。
46 4
|
1月前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第1天】Go语言基于CSP理论,借助goroutines和channels实现独特的并发模型。Goroutine是轻量级线程,通过`go`关键字启动,而channels提供安全的通信机制。文章讨论了数据竞争、死锁和goroutine泄漏等问题及其避免方法,并提供了一个生产者消费者模型的代码示例。理解CSP和妥善处理并发问题对于编写高效、可靠的Go程序至关重要。
36 2
|
1月前
|
Go 微服务
4. 参考 go 代码——服务注册与发现
4. 参考 go 代码——服务注册与发现
|
1月前
|
存储 负载均衡 监控
【Go 语言专栏】构建高可靠性的 Go 语言服务架构
【4月更文挑战第30天】本文探讨了如何利用Go语言构建高可靠性的服务架构。Go语言凭借其高效、简洁和并发性能,在构建服务架构中备受青睐。关键要素包括负载均衡、容错机制、监控预警、数据存储和服务治理。文章详细阐述了实现这些要素的具体步骤,通过实际案例分析和应对挑战的策略,强调了Go语言在构建稳定服务中的作用,旨在为开发者提供指导。