注:本文已脱敏,对原文(发表在内部技术社区)有删改,代码不公开,所以只分享遇到的一些问题和技术设计方案。
背景
截止本文写作,荔枝集团后端服务技术栈主要还是围绕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>
拆分为T
和BizStatus
。
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不支持重载,即一个方法名只存在一个方法。
- 如果提供者是java项目:lz-dubbo-go需要支持调用同方法名的不同重载方法。
- 如果提供者是go项目:Java需要支持调用方法名为大写的方法,只需要将接口中的方法名改为大写即可。
改造方案
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"
)