Dubbo-go 服务代理模型

本文涉及的产品
MSE Nacos/ZooKeeper 企业版试用,1600元额度,限量50份
任务调度 XXL-JOB 版免费试用,400 元额度,开发版规格
云原生网关 MSE Higress,422元/月
简介: 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/

相关实践学习
【AI破次元壁合照】少年白马醉春风,函数计算一键部署AI绘画平台
本次实验基于阿里云函数计算产品能力开发AI绘画平台,可让您实现“破次元壁”与角色合照,为角色换背景效果,用AI绘图技术绘出属于自己的少年江湖。
从 0 入门函数计算
在函数计算的架构中,开发者只需要编写业务代码,并监控业务运行情况就可以了。这将开发者从繁重的运维工作中解放出来,将精力投入到更有意义的开发任务上。
相关文章
|
5月前
|
JSON 中间件 Go
Go 网络编程:HTTP服务与客户端开发
Go 语言的 `net/http` 包功能强大,可快速构建高并发 HTTP 服务。本文从创建简单 HTTP 服务入手,逐步讲解请求与响应对象、URL 参数处理、自定义路由、JSON 接口、静态文件服务、中间件编写及 HTTPS 配置等内容。通过示例代码展示如何使用 `http.HandleFunc`、`http.ServeMux`、`http.Client` 等工具实现常见功能,帮助开发者掌握构建高效 Web 应用的核心技能。
337 61
|
3月前
|
数据采集 编解码 监控
Go语言实战案例:使用channel实现生产者消费者模型
本文是「Go语言100个实战案例 · 网络与并发篇」第4篇,通过实战案例详解使用 Channel 实现生产者-消费者模型,涵盖并发控制、任务调度及Go语言并发哲学,助你掌握优雅的并发编程技巧。
|
5月前
|
开发框架 安全 前端开发
Go Web开发框架实践:模板渲染与静态资源服务
Gin 是一个功能强大的 Go Web 框架,不仅适用于构建 API 服务,还支持 HTML 模板渲染和静态资源托管。它可以帮助开发者快速搭建中小型网站,并提供灵活的模板语法、自定义函数、静态文件映射等功能,同时兼容 Go 的 html/template 引擎,具备高效且安全的页面渲染能力。
|
5月前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:使用 Gin 快速构建 Web 服务
Gin 是一个高效、轻量级的 Go 语言 Web 框架,支持中间件机制,非常适合开发 RESTful API。本文从安装到进阶技巧全面解析 Gin 的使用:快速入门示例(Hello Gin)、定义 RESTful 用户服务(增删改查接口实现),以及推荐实践如参数校验、中间件和路由分组等。通过对比标准库 `net/http`,Gin 提供更简洁灵活的开发体验。此外,还推荐了 GORM、Viper、Zap 等配合使用的工具库,助力高效开发。
|
11月前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
8月前
|
存储 JSON Go
PHP 日志系统的最佳搭档:一个 Go 写的远程日志收集服务
为了不再 SSH 上去翻日志,我写了个 Go 小脚本,用来接收远程日志。PHP 负责记录日志,Go 负责存储和展示,按天存储、支持 API 访问、可远程管理,终于能第一时间知道项目炸了。
177 10
|
缓存 弹性计算 API
用 Go 快速开发一个 RESTful API 服务
用 Go 快速开发一个 RESTful API 服务
|
9月前
|
机器学习/深度学习 人工智能 测试技术
扩散模型版CS: GO!世界模型+强化学习:2小时训练登顶Atari 100K
《Diffusion for World Modeling: Visual Details Matter in Atari》提出了一种名为DIAMOND的方法,将扩散模型应用于世界模型构建。该方法在Atari 100K基准测试中仅用2小时训练时间就达到了前所未有的性能水平,平均人类归一化分数达1.46,超过人类水平。DIAMOND通过条件生成、网络预条件和高效采样等设计,提升了视觉细节捕捉、模型稳定性和计算效率。未来研究方向包括连续控制领域应用和更长记忆机制的整合。
241 10
|
Go UED
Go Web服务中如何优雅平滑重启?
在生产环境中,服务升级时如何确保不中断当前请求并应用新代码是一个挑战。本文介绍了如何使用 Go 语言的 `endless` 包实现服务的优雅重启,确保在不停止服务的情况下完成无缝升级。通过示例代码和测试步骤,详细展示了 `endless` 包的工作原理和实际应用。
280 3
|
JSON Go UED
Go Web服务中如何优雅关机?
在构建 Web 服务时,优雅关机是一个关键的技术点,它确保服务关闭时所有正在处理的请求都能顺利完成。本文通过一个简单的 Go 语言示例,展示了如何使用 Gin 框架实现优雅关机。通过捕获系统信号和使用 `http.Server` 的 `Shutdown` 方法,我们可以在服务关闭前等待所有请求处理完毕,从而提升用户体验,避免数据丢失或不一致。
198 1

热门文章

最新文章