基于dubbo-go二次开发荔枝RPC框架

原创 吴就业 116 0 2022-10-16

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/3e76a4c9cfd8451d99c8945a3bc0cc62

作者:吴就业
链接:https://wujiuye.com/article/3e76a4c9cfd8451d99c8945a3bc0cc62
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

注:本文已脱敏,对原文(发表在内部技术社区)有删改,代码不公开,所以只分享遇到的一些问题和技术设计方案。

背景

截止本文写作,荔枝集团后端服务技术栈主要还是围绕Java语言构建,go语言主要用于开发中间件,例如接入器、推送、上传。而在实现上传兼容历史api需求时,一些兼容接口需要上传支持业务侧通过rpc调用,虽然可以选择简单实现接收rpc请求和响应,但依然需要服务注册和发现。而基于dubbo-go二开,可以逐步完善后提供给更多内部中间件使用,或是提供给业务使用。

由于java版的rpc框架基于dubbo 2.6.4版本开发,而dubbo-go 1.x对应 dubbo 2.x,因此lz-dubbo-go选择基于dubbo-go 1.5.8版本开发。

难点

dubbo-go使用文档过于简单,由于go语言特性的差异,使用上还是有较大区别,特别是跨语言的调用。这对于上手并不友好,只能通过读源码去理解怎么使用。

dubbo-go没有提供http协议的实现,要实现lzHttp(内部rpc协议,基于http),需要改动的扩展点跟原先Java实现基本一样。在不改dubbo-go源码,只基于扩展点实现lzHttp,有很多限制。而不改源码是为了后续能够跟随社区升级版本,例如社区版本的一些bugfix。

序列化方面,go的json库序列化会将[]byte转为base64字符串,反序列化再将base64字符串转为[]byte。而java的json库都不会这么处理,所以lz-dubbo-go的实现上,放弃支持go和java互相调用使用[]byte类型。如果必须要做,那后续也应该是修改java版本的去兼容go,因为json序列化byte数组转base64字符串可以让数据包更小。

// 不支持
func Upload(file []byte) 
// 替代方案
func Upload(fileBase64String string)  

其它难点更多是受语言特性的影响:

反射

go反射不支持像Java那样,拿到类型名称反射获取类型,go必须通过实例去获取实例的类型。反射调用的实现上比较别扭。

泛型

go1.8之前的版本还没有泛型,无法实现像Java那样将返回值类型声明名Result<T>

但go支持多返回值,因此lz-dubbo-go将Result<T>拆分为TBizStatus

func GetUser(id int) (*User,*BizStatus)

另外,由于go特性,错误应当值处理,因此Rpc调用异常应该返回RpcError。

func GetUser(id int) (*User,*BizStatus,*RpcError)

BizStatus、RpcError本质都属于error,如果需要两个返回值分别接收BizStatus和RpcError,方法返回参数显得太多。 因此lz-dubbo-go将BizStatus、RpcError实现error接口,使用同一个返回值接收。

func GetUser(id int) (*User,error)

由于go不支持注解,实现上相比java版本,需要多出一些额外的配置,替代@Service注解、@Reference注解。

方法命名、重载

在实现与Java互相调用上,语言特性方面需要考虑的问题还有:go要求public方法首字母大写,而java方法要求首字母是小写,并且go不支持重载,即一个方法名只存在一个方法。

改造方案

dubbo-go实现的“SPI”

go并没有所谓的SPI,但go的module支持使用init方法实现自动初始化,当我们import一个module时,module中的所有init方法就会被调用执行,我们可以在init方法中实现module的初始化逻辑。

dubbo-go实现的扩展机制,正是利用了module特性。在common模块中,使用一个map存储外部注册的扩展组件,其它扩展的module通过init方法往这个map注册实现。

例如Registry(服务注册发现扩展点):

var (
   registrys = make(map[string]func(config *common.URL) (registry.Registry, error))
)

// SetRegistry sets the registry extension with @name
func SetRegistry(name string, v func(_ *common.URL) (registry.Registry, error)) {
   registrys[name] = v
}

// GetRegistry finds the registry extension with @name
func GetRegistry(name string, config *common.URL) (registry.Registry, error) {
   return registrys[name](config)
}

服务注册扩展点

dubbo-go自身提供zookeeper服务注册的实现,然而路径”/dubbo/${serviceName}/providers|consumers”是写死的,也就是namespace固定为”dubbo”。而由于荔枝rpc “dubbo”这个namespace已经被用于历史rpc协议,加上后续需要实现跨业务调用,所以不满足需求,需要自实现。

我们需要自己实现lzZookeeper,实现registry.Registry接口,并往extension模块注册key为“lzZookeeper”,value为registry.Registry的工厂方法的扩展组件。

package lzzookeeper

func init() {
    // 注册lzZookeeper
   extension.SetRegistry(LZ_ZOOKEEPER_KEY, newZookeeperRegistry)
}

func newZookeeperRegistry(url *common.URL) (registry.Registry, error) {
   // 
}

使用时,需要在我们项目中import lzzookeeper这个module。

import(
    _ "gitlab.lizhi.com/middleware/lz-dubbo-go/lzdubbo/lzzookeeper"
)

rpc协议扩展点

通过rpc协议扩展点实现lzHttp RPC协议,实现Exporter和Invoker,通过http应用层协议发起网络请求。

需要实现protocol.Protocol接口,并注册lzHttp协议扩展组件:

package lzhttp

func init() {
   extension.SetProtocol(LZHTTP, newLzHttpProtocol)
}

func newLzHttpProtocol() protocol.Protocol {
   return &LzHttpProtocol{
      BaseProtocol: protocol.NewBaseProtocol(),
   }
}

使用时,需要在我们项目中import lzhttp这个module。

import(
    _ "gitlab.lizhi.com/middleware/lz-dubbo-go/lzdubbo/lzhttp"
)

proxy扩展点

用于构造消费者代理实例(func),组装Invocation参数。

需要实现proxy.ProxyFactory接口,并注册lzProxy扩展组件:

package lzproxy

func init() {
   extension.SetProxyFactory("lzHttpProxyFactory", NewLzHttpProxyFactory)
}

func NewLzHttpProxyFactory(opts ...proxy.Option) proxy.ProxyFactory {
   return &LzHttpProxyFactory{}
}

使用时,需要在我们项目中import lzproxy这个module。

import(
    _ "gitlab.lizhi.com/middleware/lz-dubbo-go/lzdubbo/lzproxy"
)

动态配置扩展点

用于实现自动从配置中心获取注册中心zookeeper的地址、namespace配置,以及其它一些公用配置。

需要实现config_center.DynamicConfiguration接口,并注册lzConfig扩展组件:

package lzconfig

func init() {
   extension.SetConfigCenterFactory("lzConfig", NewLzConfigFactory)
}

func NewLzConfigFactory() config_center.DynamicConfigurationFactory {
   return &lzconfigFactory{}
}

使用时,需要在我们项目中import lzconfig这个module。

import(
    _ "gitlab.lizhi.com/middleware/lz-dubbo-go/lzdubbo/lzconfig"
)
#中间件

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

S3文件上传403问题排查

业务反馈有个iOS设备上传出现403问题,打点日记只能看到403,没有详细的错误信息。开了s3的日记后,也只是看到403 AccessDenied。

Go调用Lua性能压测与调优

基于go提供的基准测试能力编写并发测试用例,为排除脚本本身的性能影响,脚本只实现简单的逻辑,并实现预编译。通过调整虚拟机池策略、cpu数、并行度等,输出调用lua脚本的平均耗时、占用的内存。

被开源组件坑惨了,文件上传到MFS后MD5不一致

moosefs没有关于底层自定义二进制通信协议的文档,且各大版本api上差异很大。我们在github上找到一个go语言实现api操作moosefs的开源库,但不幸的是,这个组件实现的api版本很低,在测试阶段,我们发现上传文件到moosefs后,moosefs上存储的文件的md5与本地原文件md5不一致,如果是图片,能很明显的看出少了一块像素。

如何开发一个Java微服务项目脚手架

如果没有脚手架,每当需要创建一个新的project,我们通常会选择基于现有的project复制一份,然后修改修改。

Dubbo支持自适应等待无损下线

无损上下线是服务治理不可忽视的问题,在应⽤上下线发布过程中,如果上下线不平滑,就会出现短时间的服务调⽤报错,如连接被拒绝(`Connection refused`)、请求超时或请求异常。

Xxl-job SDK引发的OOM

由于输出的错误日记字符串长度过长,导致xxl-job-admin处理callback请求无法将日记入库。sdk会将失败的callback写入一个重试文件(xxl-job-callback.log),sdk有一个后台线程,定时每几秒会全量load重试文件到内存中...