该文章内容发布已经超过一年,请注意检查文章中内容是否过时。
随着微服务架构的流行,许多高性能 rpc 框架应运而生,由阿里开源的 dubbo 框架 go 语言版本的 dubbo-go 也成为了众多开发者不错的选择。本文将介绍 dubbo-go 框架的基本使用方法,以及从 export 调用链的角度进行 server 端源码导读,希望能引导读者进一步认识这款框架。
有了上一篇文章《dubbo-go 源码笔记(一)Server服务暴露过程详解》 的铺垫,可以大致上类比客户端服务类似于服务端启动过程。其中最大的区别是服务端通过zk注册服务,发布自己的ivkURL并订阅事件开启监听;而服务端应该是通过zk注册组件,拿到需要调用的serviceURL,更新invoker并重写用户的RPCService,从而实现对远程过程调用细节的封装。
helloworld提供的demo:profiles/client.yaml
可看到配置文件与之前讨论过的server端非常类似,其refrences部分字段就是对当前服务要主调的服务的配置,其中详细说明了调用协议、注册协议、接口id、调用方法、集群策略等,这些配置都会在之后与注册组件交互,重写ivk、调用的过程中使用到。
user.go
main.go
官网提供的helloworld demo的源码。可看到与服务端类似,在user.go内注册了rpc-service,以及需要rpc传输的结构体user。
在main函数中,同样调用了config.Load()函数,之后就可以直接通过实现好的rpc-service:userProvider 直接调用对应的功能函数,即可实现rpc调用。
可以猜到,从hessian注册结构、SetConsumerService,到调用函数.GetUser()期间,用户定义的rpc-service也就是userProvider对应的函数被重写,重写后的GetUser函数已经包含了实现了远程调用逻辑的invoker。
接下来,就要通过阅读源码,看看dubbo-go是如何做到的。
config/config_loader.go :Load()
在main函数中调用的config.Load()函数,进而调用了loadConsumerConfig,类似于之前讲到的server端配置读入函数。
在loadConsumerConfig函数中,进行了三步操作:
其中重要的就是for循环里面的引用和实例化,两步操作,会在接下来展开讨论。
至此,配置已经被写入了框架。
上述的ref.Refer完成的就是这部分的操作。
图(一)
和server端类似,存在注册url和服务url,dubbo习惯将服务url作为注册url的sub。
config/reference_config.go: Refer()
这个函数中,已经处理完从Register配置到RegisterURL的转换,即图(一)中部分:
接下来,已经拿到的url将被传递给RegistryProtocol,进一步refer。
registry/protocol/protocol.go: Refer
可详细阅读上述注释,这个函数完成了从url到invoker的全部过程
(一)首先获得Registry对象,默认是之前实例化的zkRegistry,和之前server获取Registry的处理很类似。 (二)通过构造一个新的directory,异步拿到之前在zk上注册的server端信息,生成invoker (三)在zk上注册当前service (四)集群策略,获得最终invoker
这一步完成了图(一)中所有余下的绝大多数操作,接下来就需要详细的查看directory的构造过程:
图(二)
上述的 extension.GetDefaultRegistryDirectory(®istryUrl, reg)
函数,本质上调用了已经注册好的NewRegistryDirectory
函数:
registry/directory/directory.go: NewRegistryDirectory()
首先构造了一个注册directory,开启携程调用其subscribe函数,传入serviceURL。
这个directory目前包含了对应的zkRegistry,以及传入的URL,他cacheInvokers的部分是空的。
进入dir.subscribe(url.SubURL)这个异步函数:
registry/directory/directory.go: subscribe()
重点来了,他调用了zkRegistry的Subscribe方法,与此同时将自己作为ConfigListener传入
我认为这种传入listener的设计模式非常值得学习,而且很有java的味道。
针对等待zk返回订阅信息这样的异步操作,需要传入一个Listener,这个Listener需要实现Notify方法,进而在作为参数传入内部之后,可以被异步地调用Notify,将内部触发的异步事件“传递出来”,再进一步处理加工。
层层的Listener事件链,能将传入的原始serviceURL通过zkConn发送给zk服务,获取到服务端注册好的url对应的二进制信息。
而Notify回调链,则将这串byte[]一步一步解析、加工;以事件的形式向外传递,最终落到directory上的时候,已经是成型的newInvokers了。
具体细节不再以源码形式展示,可参照上图查阅源码。
至此已经拿到了server端注册好的真实invoker。
完成了图(一)中的部分:
经过上述操作,已经拿到了server端Invokers,放入了directory的cacheinvokers数组里面缓存。
后续的操作对应本文2.2.2的第四步,由directory生成带有特性集群策略的invoker
Join函数的实现就是如下函数:
cluster/cluster_impl/failover_cluster_invokers.go: newFailoverClusterInvoker()
dubbo-go框架默认选择failover策略,既然返回了一个invoker,我们查看一下failoverClusterInvoker的Invoker方法,看他是如何将集群策略封装到Invoker函数内部的:
cluster/cluster_impl/failover_cluster_invokers.go: Invoker()
看了很多Invoke函数的实现,所有类似的Invoker函数都包含两个方向,一个是用户方向的invcation,一个是函数方向的底层invokers。
而集群策略的invoke函数本身作为接线员,把invocation一步步解析,根据调用需求和集群策略,选择特定的invoker来执行
proxy函数也是这样,一个是用户方向的ins[] reflect.Type, 一个是函数方向的invoker。
proxy函数负责将ins转换为invocation,调用对应invoker的invoker函数,实现连通。
而出于这样的设计,可以在一步步Invoker封装的过程中,每个Invoker只关心自己负责操作的部分,从而使整个调用栈解耦。
妙啊!!!
至此,我们理解了failoverClusterInvoker 的Invoke函数实现,也正是和这个集群策略Invoker被返回,接受来自上方的调用。
已完成图(一)中的:
拿到invokers后,可以回到:
config/refrence_config.go: Refer()函数了。
我们有了可以打通的invokers,但还不能直接调用,因为invoker的入参是invocation,而调用函数使用的是具体的参数列表。需要通过一层proxy来规范入参和出参。
接下来新建一个默认proxy,放置在c.proxy内,以供后续使用
至此,完成了图(一)中最后的操作
上面完成了config.Refer操作
回到config/config_loader.go: loadConsumerConfig()
下一个重要的函数是Implement,他完的操作较为简单:旨在使用上面生成的c.proxy代理,链接用户自己定义的rpcService到clusterInvoker的信息传输。
函数较长,只选取了重要的部分:
common/proxy/proxy.go: Implement()
正如之前所说,proxy的作用是将用户定义的函数参数列表,转化为抽象的invocation传入Invoker,进行调用。
其中已标明有三处较为重要的地方:
这里直接调用用户定义的rpcService的函数GetUser,这里实际调用的是经过重写入的函数代理,所以就能实现远程调用了。
在阅读dubbo-go源码的过程中,我能发现一条清晰的invoker-proxy嵌套链,我希望通过图的形式来展现:
作者简介 李志信 (GitHubID LaurenceLiZhixin),中山大学软件工程专业在校学生,擅长使用 Java/Go 语言,专注于云原生和微服务等技术方向。