Bootstrap

Dubbo-go 源码笔记(一)Server 端开启服务过程(1)

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)

}

复制代码

Server 端

========

服务暴露过程涉及到多次原始 rpcService 的封装、暴露,网上其他文章的图感觉太过笼统,在此,简要地绘制了一个用户定义服务的数据流图:

1. 加载配置

========

1)框架初始化

=======

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

其中重要的有:

  • 默认函数代理工厂:common/proxy/proxy_factory/default.go

func init() {

extension.SetProxyFactory(“default”, NewDefaultProxyFactory)

}

复制代码

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

  • 注册中心注册协议

registry/protocol/protocol.go

func init() {

extension.SetProtocol(“registry”, GetProtocol)

}

复制代码

它负责将 invoker 暴露给对应注册中心,比如 zk 注册中心。

  • zookeeper 注册协议:registry/zookeeper/zookeeper.go

func init() {

extension.SetRegistry(“zookeeper”, newZkRegistry)

}

复制代码

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

  • dubbo 传输协议:protocol/dubbo/dubbo.go

func init() {

extension.SetProtocol(DUBBO, GetProtocol)

}

复制代码

它负责监听对应端口,将具体的服务暴露,并启动对应的事件 handler,将远程调用的 event 事件传递到 invoker 内部,调用本地 invoker 并获得执行结果返回。

  • filter 包装调用链协议:protocol/protocolwrapper/protocol_filter_wrapper.go

func init() {

extension.SetProtocol(FILTER, GetProtocol)

}

复制代码

它负责在服务暴露过程中,将代理 invoker 打包,通过配置好的 filter 形成调用链,并交付给 dubbo 协议进行暴露。

上述提前注册好的框架已实现的组件,在整个服务暴露调用链中都会用到,会根据配置取其所需。

2)配置文件

======

服务端需要的重要配置有三个字段:services、protocols、registries。

profiles/dev/server.yml:

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 需要设置用户名和密码,也可以在配置中写好。

3)配置文件的读入和检查

============

config/config_loader.go:: Load()

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

// Load Dubbo Init

func 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. 原始 service 封装入 proxy_invoker

=================================

config/service_config.go :: Export()

接下来进入 ServiceConfig.Export() 函数.

这个函数进行了一些细碎的操作,比如为不同的协议分配随机端口,如果指定了多个中心注册协议,则会将服务通过多个中心注册协议的 registryProtocol 暴露出去,我们只关心对于一个注册协议是如何操作的。还有一些操作比如生成调用 url 和注册 url,用于为暴露做准备。

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

=====================================

registryUrl 是用来向中心注册组件发起注册请求的,对于 zookeeper 的话,会传入其 ip 和端口号,以及附加的用户名密码等信息。

这个 regUrl 目前只存有注册(zk)相关信息,后续会补写入 ServiceIvk,即服务调用相关信息,里面包含了方法名,参数等…

2)对于一个注册协议,将转入的 rpc-service 实例注册在 common.ServiceMap

===================================================

这个 Register 函数将服务实例注册了两次,一次是以 Interface 为 key 写入接口服务组内,一次是以 interface 和 proto 为 key 写入特定的一个唯一的服务。

后续会从 common.Map 里面取出来这个实例。

3)获取默认代理工厂,将实例封装入代理 invoker

===========================

// 拿到一个proxyInvoker,这个invoker的url是传入的regUrl,这个地方将上面注册的service实例封装成了invoker

// 这个GetProxyFactory返回的默认是common/proxy/proxy_factory/default.go

// 这个默认工厂调用GetInvoker获得默认的proxyInvoker,保存了当前注册url

invoker := 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 调用链的设计思想。

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

3. registry 协议在 zkRegistry 上暴露上面的 proxy_invoker

=================================================

上面,我们执行到了 exporter = c.cacheProtocol.Export(invoker)。

这里的 cacheProtocol 为一层缓存设计,对应到原始的 demo 上,这里是默认实现好的 registryProtocol。

registry/protocol/protocol.go:: Export()

这个函数内构造了多个 EventListener,非常有 java 的设计感。

我们只关心服务暴露的过程,先忽略这些监听器。

1)获取注册 url 和服务 url

==================

2)获取注册中心实例 zkRegistry

=====================

一层缓存操作,如果 cache 没有需要从 common 里面重新拿 zkRegistry。

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() 函数:t.tb.cn/6Xje4bijnsI…。

至此,将服务端调用 url 注册到了 zookeeper 上,而客户端如果想获取到这个 url,只需要传入特定的 dubboPath,向 zk 请求即可。目前 client 是可以获取到访问方式了,但服务端的特定服务还没有启动,还没有开启特定协议端口的监听,这也是 registry/protocol/protocol.go:: Export() 函数接下来要做的事情。

4)proxy_invoker 封装入 wrapped_invoker,得到 filter 调用链

===================================================

// invoker封装入warppedInvoker

wrappedInvoker := newWrappedInvoker(invoker, providerUrl)

// 经过为invoker增加filter调用链,再使用dubbo协议Export,开启service并且返回了Exporter 。

// export_1

cachedExporter = 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 暴露到特定端口,用于相应请求事件。

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()。

这一函数做了两个事情:构造触发器、启动服务。

  • 将传入的 Invoker 调用 chain 进一步封装,封装成一个 exporter,再将这个 export 放入 map 保存。注意!这里把 exporter 放入了 SetExporterMap中,在下面服务启动的时候,会以注册事件监听器的形式将这个 exporter 取出!

  • 调用 dubboProtocol 的 openServer 方法,开启一个针对特定端口的监听。

如上图所示,一个 Session 被传入,开启对应端口的事件监听。

至此构造出了 exporter,完成图中部分:

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 connection

func (h *RpcServerHandler) OnMessage(session getty.Session, pkg interface{}) {

复制代码

这一函数实现了在 getty session 接收到 rpc 调用后的一系列处理:

  • 传入包的解析

  • 返回

整个被调整过程一气呵成。实现了从 getty.Session 的调用事件,到经过层层封装的 invoker 的调用。

至此,一次 rpc 调用得以正确返回。

小结

==

  • 关于 Invoker 的层层封装

能把一次调用抽象成一次 invoke;能把一个协议抽象成针对 invoke 的封装;能把针对一次 invoke 所做出的特定改变封装到 invoke 函数内部,可以降低模块之间的耦合性。层层封装逻辑更加清晰。

最后

金三银四马上就到了,希望大家能好好学习一下这些技术点

学习视频:

大厂面试真题:

age(session getty.Session, pkg interface{}) {

复制代码

这一函数实现了在 getty session 接收到 rpc 调用后的一系列处理:

  • 传入包的解析

  • 返回

整个被调整过程一气呵成。实现了从 getty.Session 的调用事件,到经过层层封装的 invoker 的调用。

至此,一次 rpc 调用得以正确返回。

小结

==

  • 关于 Invoker 的层层封装

能把一次调用抽象成一次 invoke;能把一个协议抽象成针对 invoke 的封装;能把针对一次 invoke 所做出的特定改变封装到 invoke 函数内部,可以降低模块之间的耦合性。层层封装逻辑更加清晰。

最后

金三银四马上就到了,希望大家能好好学习一下这些技术点

学习视频:

[外链图片转存中…(img-KqranJft-1714287201465)]

大厂面试真题:

[外链图片转存中…(img-m48xg5Qj-1714287201466)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

;