导读

导读:随着微服务架构的风行,许多高性能 rpc 框架应运而生,由阿里开源的 dubbo 框架 go 语言版本的 dubbo-go 也成为了泛滥开发者不错的抉择。本文将介绍 dubbo-go 框架的根本应用办法,以及从 export 调用链的角度进行 server 端源码导读,心愿能疏导读者进一步意识这款框架。下周将发表本文的姊妹篇:《从 client 端源码导读 dubbo-go 框架》。

序言

近日浏览了局部dubbo-go源码
https://github.com/dubbogo/dubbo-go

当拿到一款框架之后,一种不错的源码浏览形式大抵如下:从运行最根底的helloworld demo 源码开始,再查看配置文件,开启各种依赖服务(比方zk、consul),开启服务端,再到通过client调用服务端,打印残缺申请日志和回包。调用胜利之后,再依据框架的设计模型,从配置文件解析开始,自顶向下递浏览整个框架的调用栈。
对于C/S模式的rpc申请来说,整个调用栈被拆成了client和server两局部,所以能够别离从server端的配置文件解析浏览到server端的监听启动,从client端的配置文件解析浏览到一次invoker Call 调用。这样一次残缺申请就清晰了起来。

1. 运行官网提供的helloworld-demo

官网demo

1.1 dubbo-go 2.7版本 QuickStart

1. 开启一个go-server服务
  • 将仓库clone 到本地
$ git clone https://github.com/dubbogo/dubbo-samples.git
  • 进入dubbo目录
$ cd dubbo-samples/golang/helloworld/dubbo

进入目录后可看到四个文件夹,别离反对go和java的client以及server,咱们尝试运行一个go的server
进入app子文件夹内,能够看到外面保留了go文件。

$ cd go-server/app
  • sample文件构造:

可在go-server外面看到三个文件夹:app、assembly、profiles
其中app文件夹下保留go源码,assembly文件夹下保留可选的针对特定环境的build脚本,profiles下保留配置文件。对于dubbo-go框架,配置文件十分重要,没有文件将导致服务无奈启动。

  • 设置指向配置文件的环境变量

因为dubbo-go框架依赖配置文件启动,让框架定位到配置文件的形式就是通过环境变量来找。对于server端须要两个必须配置的环境变量:CONF_PROVIDER_FILE_PATH、APP_LOG_CONF_FILE,别离应该指向服务端配置文件、日志配置文件。
在sample外面,咱们能够应用dev环境,即profiles/dev/log.yml profiles/dev/server.yml 两个文件。
在app/下,通过命令行中指定好这两个文件:

$ export CONF_PROVIDER_FILE_PATH="../profiles/dev/server.yml"
$ export APP_LOG_CONF_FILE="../profiles/dev/log.yml"
  • 设置go代理并运行服务
$ go run .

如果提醒timeout,则须要设置goproxy代理

$ export GOPROXY="http://goproxy.io"

再运行go run 即可开启服务

2. 运行zookeeper

装置zookeeper,并运行zkServer, 默认为2181端口

3. 运行go-client调用server服务
  • 进入go-client的源码目录
$ cd go-client/app
  • 同理,在/app下配置环境变量
$ export CONF_CONSUMER_FILE_PATH="../profiles/dev/client.yml"
$ export APP_LOG_CONF_FILE="../profiles/dev/log.yml"

配置go代理

$ export GOPROXY="http://goproxy.io"
  • 运行程序
$ go run .

即可在日志中找到打印出的申请后果

response result: &{A001 Alex Stocks 18 2020-10-28 14:52:49.131 +0800 CST}

同样,在运行的server中,也能够在日志中找到打印出的申请:

req:[]interface {}{"A001"}
rsp:main.User{Id:"A001", Name:"Alex Stocks", Age:18, Time:time.Time{...}

祝贺!一次基于dubbo-go的rpc调用胜利。

4. 常见问题
    1. 当日志开始局部呈现profiderInit和ConsumerInit均失败的日志。查看环境变量中配置门路是否正确,配置文件是否正确。
    1. 当日志中呈现register失败的状况,个别为向注册核心注册失败,查看注册核心是否开启,查看配置文件中对于register的端口是否正确。
    1. sample的默认开启端口为20000,确保启动前无占用

1.2 配置环境变量

export APP_LOG_CONF_FILE="../profiles/dev/log.yml"export CONF_CONSUMER_FILE_PATH="../profiles/dev/client.yml"

1.3 服务端源码

1. 目录构造

dubbo-go框架的example提供的目录如下:

  • app/ 文件夹下寄存源码,能够本人编写环境变量配置脚本buliddev.sh
  • assembly/ 文件夹下寄存不同平台的构建脚本
  • profiles/ 文件夹下寄存不同环境的配置文件
  • target/ 文件夹下寄存可执行文件
2. 要害源码

源码搁置在app/文件夹下,次要蕴含server.go 和user.go 两个文件,顾名思义,server.go用于应用框架开启服务以及注册传输协定,user.go则定义了rpc-service构造体,以及传输协定的构造。
user.go

func init() {    config.SetProviderService(new(UserProvider))    // ------for hessian2------    hessian.RegisterPOJO(&User{})}type User struct {    Id   string    Name string    Age  int32    Time time.Time}type UserProvider struct {}func (u *UserProvider) GetUser(ctx context.Context, req []interface{}) (*User, error) {

能够看到,user.go中存在init函数,是服务端代码中最先被执行的局部。User为用户自定义的传输构造体,UserProvider为用户自定义的rpc_service。
蕴含一个rpc函数,GetUser。
当然,用户能够自定义其余的rpc性能函数。
在init函数中,调用config的SetProviderService函数,将以后rpc_service注册在框架config上。
能够查看dubbo官网文档提供的设计图

service层上面就是config层,用户服务会逐层向下注册,最终实现服务端的裸露。
rpc-service注册结束之后,调用hessian接口注册传输构造体User。
至此init函数执行结束
server.go

// they are necessary://      export CONF_PROVIDER_FILE_PATH="xxx"//      export APP_LOG_CONF_FILE="xxx"func main() {    hessian.RegisterPOJO(&User{})    config.Load()    initSignal()}func initSignal() {    signals := make(chan os.Signal, 1)    ...

之后执行main函数
main函数中只进行了两个操作,首先应用hessian注册组件将User构造体注册(与之前略有反复),从而能够在接下来应用getty打解包。
之后调用config.Load函数,该函数位于框架config/config_loader.go内,这个函数是整个框架服务的启动点,上面会具体讲这个函数内重要的配置处理过程。执行完Load()函数之后,配置文件会读入框架,之后依据配置文件的内容,将注册的service实现到配置构造里,再调用Export裸露给特定的registry,进而开启特定的service进行对应端口的tcp监听,胜利启动并且裸露服务。
最终开启信号监听initSignal()优雅地完结一个服务的启动过程。

1.4 客户端源码

客户端蕴含client.go和user.go两个文件,其中user.go与服务端完全一致,不再赘述。
client.go

// they are necessary://      export CONF_CONSUMER_FILE_PATH="xxx"//      export APP_LOG_CONF_FILE="xxx"func main() {    hessian.RegisterPOJO(&User{})    config.Load()    time.Sleep(3e9)    println("\n\n\nstart to test dubbo")    user := &User{}    err := userProvider.GetUser(context.TODO(), []interface{}{"A001"}, user)    if err != nil {  panic(err)    }    println("response result: %v\n", user)    initSignal()}

main函数和服务端也相似,首先将传输构造注册到hessian上,再调用config.Load()函数。在下文会介绍,客户端和服务端会依据配置类型执行config.Load()中特定的函数loadConsumerConfig()和loadProviderConfig(),从而达到“开启服务”、“调用服务”的目标。
加载完配置之后,还是通过实现服务,减少函数proxy,申请registry,reloadInvoker指向服务端ip等操作,重写了客户端实例userProvider的对应函数,这时再通过调用GetUser函数,能够间接通过invoker,调用到曾经开启的服务端,实现rpc过程。
上面会从server端和client端两个角度,具体解说服务启动、registry注册、调用过程:

1.5 自定义配置文件(非环境变量)办法

1.5.1 服务端自定义配置文件
  1. var providerConfigStr = xxxxx// 配置文件内容,能够参考log 和 client

    • 在这里你能够定义配置文件的获取形式,比方配置核心,本地文件读取
  2. config.Load()之前设置配置,例如:
func main() {    hessian.RegisterPOJO(&User{})    providerConfig := config.ProviderConfig{}    yaml.Unmarshal([]byte(providerConfigStr), &providerConfig)    config.SetProviderConfig(providerConfig)    defaultServerConfig := dubbo.GetDefaultServerConfig()    dubbo.SetServerConfig(defaultServerConfig)    logger.SetLoggerLevel("warn") // info,warn    config.Load()    select {    }}
1.5.2 客户端自定义配置文件
  1. var consumerConfigStr = xxxxx// 配置文件内容,能够参考log 和 client

    • 在这里你能够定义配置文件的获取形式,比方配置核心,本地文件读取
  2. config.Load()之前设置配置,例如:
func main() {     p := config.ConsumerConfig{}     yaml.Unmarshal([]byte(consumerConfigStr), &p)     config.SetConsumerConfig(p)     defaultClientConfig := dubbo.GetDefaultClientConfig()     dubbo.SetClientConf(defaultClientConfig)     logger.SetLoggerLevel("warn") // info,warn     config.Load()     user := &User{}     err := userProvider.GetUser(context.TODO(), []interface{}{"A001"}, user)     if err != nil {         log.Print(err)         return     }  log.Print(user)}

2. server端:

服务裸露过程波及到屡次原始rpcService的封装、裸露,网上其余文章的图感觉太过抽象,我简要的绘制了一个用户定义服务的数据流图

2.1 加载配置

2.1.1 框架初始化

在加载配置之前,框架提供了很多已定义好的协定、工厂等组件,都会在对应模块init函数内注册到extension模块上,以供接下来配置文件中进行选用。
其中重要的有:

  1. 默认函数代理工厂
    common/proxy/proxy_factory/default.go
func init() {    extension.SetProxyFactory("default", NewDefaultProxyFactory)}

他的作用是将原始rpc-service进行封装,造成proxy_invoker,更易于实现近程call调用,详情可见其invoke函数。

  1. 注册核心注册协定
    registry/protocol/protocol.go
func init() {    extension.SetProtocol("registry", GetProtocol)}

他负责在将invoker裸露给对应注册核心,比方zk注册核心。

  1. zookeeper注册协定
    registry/zookeeper/zookeeper.go
func init() {    extension.SetRegistry("zookeeper", newZkRegistry)}

他合并了base_resiger,负责在服务裸露过程中,将服务注册在zookeeper注册器上,从而为调用者提供调用办法。

  1. dubbo传输协定
    protocol/dubbo/dubbo.go
func init() {    extension.SetProtocol(DUBBO, GetProtocol)}

他负责监听对应端口,将具体的服务裸露,并启动对应的事件handler,将近程调用的event事件传递到invoker外部,调用本地invoker并取得执行后果返回。

  1. filter包装调用链协定
    protocol/protocolwrapper/protocol_filter_wrapper.go
func init() {    extension.SetProtocol(FILTER, GetProtocol)}

他负责在服务裸露过程中,将代理invoker打包,通过配置好的filter造成调用链,并交付给dubbo协定进行裸露。
上述提前注册好的框架已实现的组件,在整个服务裸露调用链中都会用到,会依据配置取其所需。

2.1.2 配置文件

服务端须要的重要配置有三个字段:services、protocols、registries
profiles/dev/server.yaml:

registries :  "demoZk":    protocol: "zookeeper"    timeout    : "3s"    address: "127.0.0.1:2181"services:  "UserProvider":    # 能够指定多个registry,应用逗号隔开;不指定默认向所有注册核心注册    registry: "demoZk"    protocol : "dubbo"    # 相当于dubbo.xml中的interface    interface : "com.ikurento.user.UserProvider"    loadbalance: "random"    warmup: "100"    cluster: "failover"    methods:    - name: "GetUser"      retries: 1      loadbalance: "random"protocols:  "dubbo":    name: "dubbo"    port: 20000

其中service指定了要裸露的rpc-service名("UserProvider),裸露的协定名("dubbo"),注册的协定名("demoZk"),裸露的服务所处的interface,负载平衡策略,集群失败策略,调用的办法等等。
其中两头服务的协定名须要和registries下的mapkey对应,裸露的协定名须要和protocols下的mapkey对应
能够看到上述例子中,应用了dubbo作为裸露协定,应用了zookeeper作为两头注册协定,并且给定了端口。如果zk须要设置用户名和明码,也能够在配置中写好。

2.1.3 配置文件的读入和查看

config/config_loader.go:: Load()
在上述example的main函数中,有config.Load()函数的间接调用,该函数执行细节如下:

// Load Dubbo Initfunc Load() {    // init router    initRouter()    // init the global event dispatcher    extension.SetAndInitGlobalDispatcher(GetBaseConfig().EventDispatcherType)    // start the metadata report if config set    if err := startMetadataReport(GetApplicationConfig().MetadataType, GetBaseConfig().MetadataReportConfig); err != nil {  logger.Errorf("Provider starts metadata report error, and the error is {%#v}", err)  return    }    // reference config    loadConsumerConfig()    // service config    loadProviderConfig()    // init the shutdown callback    GracefulShutdownInit()}

在本文中,咱们重点关怀loadConsumerConfig()和loadProviderConfig()两个函数
对于provider端,能够看到loadProviderConfig()函数代码如下:

前半部分是配置的读入和查看,进入for循环后,是单个service的裸露起始点。
后面提到,在配置文件中曾经写好了要裸露的service的种种信息,比方服务名、interface名、method名等等。在图中for循环内,会将所有service的服务顺次实现。
for循环的第一行,依据key调用GetProviderService函数,拿到注册的rpcService实例,这里对应上述提到的init函数中用户手动注册的本人实现的rpc-service实例:

这个对象也就成为了for循环中的rpcService变量,将这个对象注册通过Implement函数写到sys(ServiceConfig类型)上,设置好sys的key和协定组,最终调用了sys的Export办法。
此处对应流程图的局部:

至此,框架配置构造体曾经拿到了所有service无关的配置,以及用户定义好的rpc-service实例,他触发了Export办法,旨在将本人的实例裸露进来。这是Export调用链的起始点。

2.2 原始service封装入proxy_invoker

config/service_config.go :: Export()
接下来进入ServiceConfig.Export()函数:
这个函数进行了一些细碎的操作,比方为不同的协定调配随机端口,如果指定了多个核心注册协定,则会将服务通过多个核心注册协定的registryProtocol裸露进来,咱们只关怀对于一个注册协定是如何操作的。还有一些操作比方生成调用url和注册url,用于为裸露做筹备。

2.2.1 首先通过配置生成对应registryUrl和serviceUrl

registryUrl是用来向核心注册组件发动注册申请的,对于zookeeper的话,会传入其ip和端口号,以及附加的用户名明码等信息
这个regUrl目前只存有注册(zk)相干信息,后续会补写入ServiceIvk,即服务调用相干信息,外面蕴含了办法名,参数等...

2.2.2 对于一个注册协定,将传入的rpc-service实例注册在common.ServiceMap

这个Register函数将服务实例注册了两次,一次是以Interface为key写入接口服务组内,一次是以interface和proto为key写入特定的一个惟一的服务。
后续会从common.Map外面取出来这个实例。

2.2.3 获取默认代理工厂,将实例封装入代理invoker
// 拿到一个proxyInvoker,这个invoker的url是传入的regUrl,这个中央将下面注册的service实例封装成了invoker// 这个GetProxyFactory返回的默认是common/proxy/proxy_factory/default.go// 这个默认工厂调用GetInvoker取得默认的proxyInvoker,保留了以后注册urlinvoker := extension.GetProxyFactory(providerConfig.ProxyFactory).GetInvoker(*regUrl)// 裸露进去 生成exporter,开启tcp监听// 这里就该跳到registry/protocol/protocol.go registryProtocol 调用的Export,将以后proxyInvoker导出exporter = c.cacheProtocol.Export(invoker)

这一步的GetProxyFactory("default")办法获取默认代理工厂,通过传入上述结构的regUrl,将url封装入代理invoker。
能够进入common/proxy/proxy_factory/default.go::ProxyInvoker.Invoke()函数里,看到对于common.Map取用为svc的局部,以及对于svc对应Method的理论调用Call的函数如下:

到这里,下面GetInvoker(*regUrl)返回的invoker即为proxy_invoker,他封装好了用户定义的rpc_service,并将具体的调用逻辑封装入了Invoke函数内。

为什么应用Proxy_invoker来调用?
我认为,通过这个proxy_invoke调用用户的性能函数,调用形式将更加抽象化,能够在代码中看到,通过ins和outs来定义入参和出参,将整个调用逻辑抽象化为invocation构造体,而将具体的函数名的抉择,参数向下传递,reflect反射过程封装在invoke函数内,这样的设计更有利于之后近程调用。我认为这是dubbo Invoke调用链的设计思维。

至此,实现了图中对应的局部:

2.3 registry协定在zkRegistry上裸露下面的proxy_invoker

下面,咱们执行到了exporter = c.cacheProtocol.Export(invoker)
这里的cacheProtocol为一层缓存设计,对应到原始的demo上,这里是默认实现好的registryProtocol
registry/protocol/protocol.go:: Export()
这个函数内结构了多个EventListener,十分有java的设计感。
咱们只关怀服务裸露的过程,先疏忽这些监听器。

2.3.1 获取注册url和服务url

2.3.2 获取注册核心实例zkRegistry

一层缓存操作,如果cache没有须要从common外面从新拿zkRegistry

2.3.3 zkRegistry调用Registry办法,在zookeeper上注册dubboPath

上述拿到了具体的zkRegistry实例,该实例的定义在
registry/zookeeper/registry.go

该构造体组合了registry.BaseRegistry构造,base构造定义了注册器根底的性能函数,比方Registry、Subscribe等,但在这些默认定义的函数外部,还是会调用facade层(zkRegistry层)的具体实现函数,这一设计模型能在保障已有性能函数不须要反复定义的同时,引入外层函数的实现,相似于构造体继承却又复用了代码。
这一设计模式我认为值得学习。
咱们查看上述registry/protocol/protocol.go:: Export()函数,间接调用了:

// 1. 通过zk注册器,调用Register()函数,将已有@root@rawurl注册到zk上    err := reg.Register(*registeredProviderUrl)

将已有RegistryUrl注册到了zkRegistry上。
这一步调用了baseRegistry的Register函数,进而调用zkRegister的DoRegister函数,进而调用:

在这个函数里,将对应root发明一个新的节点

并且写入具体node信息,node为url通过encode的后果,蕴含了服务端的调用形式。
这部分的代码较为简单,具体能够看baseRegistry的 processURL()函数。
至此,将服务端调用url注册到了zookeeper上,而客户端如果想获取到这个url,只须要传入特定的dubboPath,向zk申请即可。目前client是能够获取到拜访形式了,但服务端的特定服务还没有启动,还没有开启特定协定端口的监听,这也是registry/protocol/protocol.go:: Export()函数接下来要做的事件:

2.3.4 proxy_invoker 封装入wrapped_invoker,失去filter调用链
// invoker封装入warppedInvokerwrappedInvoker := newWrappedInvoker(invoker, providerUrl)// 通过为invoker减少filter调用链,再应用dubbo协定Export,开启service并且返回了Exporter 。// export_1cachedExporter = extension.GetProtocol(protocolwrapper.FILTER).Export(wrappedInvoker)

新建一个WrappedInvoker,用于之后链式调用
拿到提前实现并注册好的ProtocolFilterWrapper,调用Export办法,进一步裸露。
protocol/protocolwrapped/protocol_filter_wrapper.go:Export()

protocol/protocolwrapped/protocol_filter_wrapper.go:buildInvokerChain

可见,依据配置的内容,通过链式调用的结构,层层将proxy_invoker包裹在调用链的最底部,最终返回一个调用链invoker。
对应图中局部:

至此,咱们曾经拿到filter调用链,期待将这个chain裸露到特定端口,用于相应申请事件

2.3.5 通过dubbo协定裸露wrapped_invoker

protocol/protocolwrapped/protocol_filter_wrapper.go:Export()

// 通过dubbo协定Export  dubbo_protocol调用的 export_2    return pfw.protocol.Export(invoker)

回到上述Export函数的最初一行,调用了dubboProtocol的Export办法,将上述chain真正裸露。
该Export办法的具体实现在
protocol/dubbo/dubbo_protocol.go: Export()

这一函数做了两个事件:结构触发器、启动服务

  1. 将传入的Invoker调用chain进一步封装,封装成一个exporter,再将这个export放入map保留。留神!这里昂exporter放入了SetExporterMap中,在上面服务启动的时候,会以注册事件监听器的模式将这个exporter取出!
  2. 调用dubboProtocol的openServer办法,开启一个针对特定端口的监听

如上图所示,一个Session被传入,开启对应端口的事件监听。
至此结构出了exporter,实现图中局部:

2.4 注册触发动作

上述只是启动了服务,但还没有看到触发事件的细节,点进下面的s.newSession能够看到,dubbo协定为一个getty的session默认应用了如下配置

其中很重要的一个配置是EventListener,传入的是dubboServer的默认rpcHandler
protocol/dubbo/listener.go:OnMessage()
rpcHandler有一个实现好的OnMessage函数,依据getty的API,当client调用该端口时,会触发OnMessage

// OnMessage notified when RPC server session got any message in connectionfunc (h *RpcServerHandler) OnMessage(session getty.Session, pkg interface{}) {

这一函数实现了在getty session接管到rpc 调用后的一系列解决:

  • 传入包的解析

  • 依据申请包结构申请url

  • 拿到对应申请key,找到要被调用的exporter

  • 拿到对应的Invoker

  • 结构invocation

  • 调用

  • 返回

整个被调过程零打碎敲。实现了从getty.Session的调用事件,到通过层层封装的invoker的调用。
至此,一次rpc调用得以正确返回。

3. 小结

  • 对于Invoker的层层封装
    能把一次调用形象成一次invoke,能把一个协定形象成针对invoke的封装,能把针对一次invoke所做出的特定扭转封装到invoke函数外部,能够升高模块之间的耦合性。层层封装逻辑更加清晰
  • 对于URL的形象
    对于dubbo的统一化申请对象URL的极度形象是我之前没有见过的... 我认为这样封装能保障申请参数列表的简化和统一。但我认为在开发的过程中,滥用极度形象的接口可能造成...debug的艰难?以及不晓得哪些字段是以后曾经封装好的,哪些字段是无用的。
  • 对于协定的了解
    之前了解的协定还是太过具体化了,而对于dubbo-go对于dubboProtocol的协定,我认为是基于getty的进一步封装,他定义了客户端和服务端,对于getty的session应该有哪些特定的操作,从而保障主和谐被调的协定一致性,而这种保障也是一种协定的体现,是由dubbo协定来标准的。

如果你有任何疑难,欢送钉钉扫码退出交换群【钉钉群号 23331795】:

作者简介

李志信 (GitHubID LaurenceLiZhixin),中山大学软件工程业余在校学生,善于应用 Java/Go 语言,专一于云原生和微服务等技术方向。