Deep in dubbo
使用篇
服务发布
provider启动流程
1.ServiceConfig#export
服务提供方在启动部署时,dubbo会调用ServiceConfig#export
来激活服务发布流程,如下所示:
- Java API:
1 | // 1. 创建ServiceConfig实例 |
- XML
1 | <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
查看export
源码可知,总共有三种服务导出选项:
1 | public synchronized void export() { |
2.ServiceConfig#doExport
此方法主要是根据设置的属性进行合法性检查,主要包含是否已被导出,doExportUrls();
3.doExportUrls
4.ConfigValidationUtils#loadRegistries
此方法用来加载所有的服务注册中心对象,在dubbo中,一个service可以被注册到多个注册中心。
通过
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
5.doExportUrlsFor1Protocol
在此方法中会将所有的参数封装成
org.apache.dubbo.common.URL
对象,然后执行具体的服务导出。
具体过程分为:
1.解析MethodConfig配置(单独的方法调用参数设置)
2.泛型调用类型设置
3.拼接URL参数
4.导出具体服务
导出又分为四种范围(
scope
):SCOPE_NONE = “none”,如果设定为none,表示该服务不导出。
SCOPE_LOCAL = “local” ,如果设定为local,表示该服务导出到本地(injvm–伪协议,实现类为:
org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol
)- SCOPE_REMOTE = “remote”,如果设定为remote,表示该服务导出到远程。
如果有注册中心,发布到注册中心
如果没有注册中心,则表示服务是直连方式
从
dubbo-2.7.0
开始,新增加了WritableMetadataService
来存储dubbo 服务的元数据,元数据可以存储在远端配置中心和本地,默认是存储在本地,通过设置:METADATA_KEY = "metadata"
- DEFAULT_METADATA_STORAGE_TYPE = “local”
- REMOTE_METADATA_STORAGE_TYPE = “remote”
1
2
3WritableMetadataService metadataService = WritableMetadataService.getExtension(url.getParameter(METADATA_KEY, DEFAULT_METADATA_STORAGE_TYPE));
if (metadataService != null) { metadataService.publishServiceDefinition(url);
}- 不设置,导出到本地和远端
最终执行导出的代码如下
1
2
3
4
5
6
7
8
9
10
11
12// 扩展适配类
private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
// 扩展适配类
private static final ProxyFactory PROXY_FACTORY = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
...
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);由于
protocol
和PROXY_FACTORY
都是扩展适配类,跟踪代码我们可以发现:执行
PROXY_FACTORY.getInvoker
的时候实际上首先执行扩展接口ProxyFactory
的适配类ProxyFactory$Adaptive
的getInvoker
方法,根据URL
中参数proxy
的设置类型选择具体的代理工厂,默认使用的是javassist
,,因此又调用了org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory#getInvoker
来获取代理实现类,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JavassistProxyFactory extends AbstractProxyFactory {
...
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
// TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
// 这里使用javassist动态代理生成serviceImpl实现类的包装类`Wraaper...`
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
return new AbstractProxyInvoker<T>(proxy, type, url) {
protected Object doInvoke(T proxy, String methodName,
Class<?>[] parameterTypes,
Object[] arguments) throws Throwable {
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
...
}上面代码有2个目的:
inal Wrapper wrapper = Wrapper.getWrapper(...);
用来生成具体serviceImpl
的包装类,减少反射的性能损耗;return new AbstractProxyInvoker<T>...
返回了一个抽象的代理invoker
,并且重写了doInvoker
方法,重写之后使用包装类中的invokeMethod
来调用方法。
经过上述2步,服务提供方就将具体的实现类转换为Invoker
代理。
然后,当执行
protocol.export()
,实际上也是调用了Protocol$Adaptive#export()
方法,同时也分为两种情况如果为远程暴露,则执行
RegistryProtocol#export
如果为本地暴露,则只需
InjvmProtocol#export
由于dubbo的增强SPI
特性支持,injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
,则在调用之前会一层一层调用,ProtocolFilterWrapper
->ProtocolListenerWrapper
->QosProtocolWrapper
,最后会调用export
方法,此方法会将Invoker
转换为Exporter
对象,在org.apache.dubbo.registry.integration.RegistryProtocol#export
方法中,org.apache.dubbo.registry.integration.RegistryProtocol#doLocalExport
方法启NettyServer
来监听服务,org.apache.dubbo.registry.integration.RegistryProtocol#register
将当前的服务注册到注册中心。doLocalExport
是如何启动NettyServer
呢?
1 | private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) { |
此时URL
中的protocol
类型为默认的dubbo
,因此会执行DubboProtocol#export
进行转换,如下:
1 |
|
可以看到代码执行到openServer
,因为key=getAddress()=ip+port
,因此,同一台机器只会开启一个NettyServer
.
1 | private void openServer(URL url) { |
对于org.apache.dubbo.remoting.Transporter
的适配类选择有三种:MinaTransporter
、 NettyTransporter
、GrizzlyTransporter
,关于JavaNIO:Apache Mina、JBoss Netty、Sun Grizzly 框架对比:传送门
NettyServer启动之后,回到
org.apache.dubbo.registry.integration.RegistryProtocol#export
方法,继续执行将服务注册到注册中心,我们以Zookeeper
为例:1.首先查找所有注册中心
1 | final Registry registry = getRegistry(originInvoker); |
因为RegistryFactory
是一个SPI扩展接口,代码中设置的为zookeeper
,因此这里调用的是ZookeeperRegistryFactory
,继承自:org.apache.dubbo.registry.support.AbstractRegistryFactory#getRegistry(org.apache.dubbo.common.URL)
,在此方法中调用了createRegistry
,但是ZookeeperRegistryFactory
重写了createRegistry
,因此具体调用的是ZookeeperRegistryFactory#createRegistry
,该方法返回了一个new ZookeeperRegistry(url, zookeeperTransporter)
实例对象。
- 2.开始注册,
RegistryProtocol#register
方法执行注册动作,首先获取到我们在上一步找到的注册中心ZookeeperRegistry
,ZookeeperRegistry
执行父类org.apache.dubbo.registry.support.FailbackRegistry#register
,在该方法中会调用抽象方法:doRegister
,ZookeeperRegistry
重写了改方法,则执行ZookeeperRegistry#doRegister
,如下:
1 |
|
- 3.
toUrlPath
方法会把org.apache.dubbo.common.URL
转换格式后存储到zookeeper,如下:
1 | dubbo://172.16.44.21:20880/com.sxzhongf.deep.in.dubbo.api.service.IGreetingService?anyhost=true&application=deep-in-dubbo-first-provider&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=dubbo-sxzhongf-group&interface=com.sxzhongf.deep.in.dubbo.api.service.IGreetingService&methods=sayHello,testGeneric&pid=8480&release=2.7.5&revision=1.0.0&side=provider×tamp=1582872610313&version=1.0.0 |
转换之后的格式其实就是我们在zookeeper中看到的一样了,不过有几个目录:
dubbo
com.sxzhongf.deep.in.dubbo.api.service.IGreetingService
providers
1
2[zk: localhost:2181(CONNECTED) 2] ls /dubbo/com.sxzhongf.deep.in.dubbo.api.service.IGreetingService/providers
3A%2F%2F172.16.44.21%3A20880%2Fcom.sxzhongf.deep.in.dubbo.api.service.IGreetingService%3Fanyhost%3Dtrue%26application%3Ddeep-in-dubbo-first-provider%26default%3Dtrue%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26group%3Ddubbo-sxzhongf-group%26interface%3Dcom.sxzhongf.deep.in.dubbo.api.service.IGreetingService%26methods%3DsayHello%2CtestGeneric%26pid%3D15716%26release%3D2.7.5%26revision%3D1.0.0%26side%3Dprovider%26timestamp%3D1582872850187%26version%3D1.0.0]
至此,服务消费端就可以从注册中心获取服务提供service进行调用了,下节我们继续来分析,消费端是如何从注册中心拉取service进行处理的。
消费调用
consumer启动流程
消费者在启动之后,会通过ReferenceConfig#get()
来生成远程调用代理类。在get
方法中,会启动一系列调用函数,我们来一个个解析。
配置同样包含2种:
- XML
1 |
|
- Java API
1 | public class ApiConsumerApplication { |
1. new ReferenceConfig
在此阶段,会初始化org.apache.dubbo.config.AbstractConfig
& org.apache.dubbo.config.ReferenceConfig
的静态变量以及静态代码块。
2. ReferenceConfig#get
ReferenceConfig#init
- 通过
DubboBootstrap
启动dubbo。 - 继而初始化服务的元数据信息,
URL.buildKey(interfaceName, group, version)
这段用来生成唯一服务的key,所以我们之前说dubbo的唯一标识是通过全路径
和group以及version来决定的。 - 接下来通过
org.apache.dubbo.config.utils.ConfigValidationUtils#checkMock
来检查我们mock是否设置正确。 - 设置一系列要用的参数(系统运行参数、是否为consumer、是否为泛型调用等等),检查
dubbo
的注册地址,默认为当前主机IP
ReferenceConfig#createProxy
创建调用代理开始
ReferenceConfig#shouldJvmRefer
首先判断是否为Injvm
调用如果不为
injvm
,判断是否为peer to peer
端对端设置,如果为p2p,那么就直连url检查注册中心是否存在(注册中心有可能有多个)
循环检查注册中心是否有效
配置转换
URL
1 | registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=deep-in-dubbo-first-consumer&dubbo=2.0.2&pid=9780&refer=application%3Ddeep-in-dubbo-first-consumer%26dubbo%3D2.0.2%26group%3Ddubbo-sxzhongf-group%26interface%3Dcom.sxzhongf.deep.in.dubbo.api.service.IGreetingService%26methods%3DsayHello%2CtestGeneric%26pid%3D9780%26register.ip%3D192.168.14.99%26release%3D2.7.5%26revision%3D1.0.0%26side%3Dconsumer%26sticky%3Dfalse%26timeout%3D5000%26timestamp%3D1582959441066%26version%3D1.0.0®istry=zookeeper&release=2.7.5×tamp=1582961922459 |
- 如果只有一个注册中心,执行
REF_PROTOCOL.refer(interfaceClass, urls.get(0));
来将URL
转为Invoker
对象,因为private static final Protocol REF_PROTOCOL = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
是扩展是Adaptive
,因此在这里会执行Protocol$Adaptive#refer
方法,又由于protocol
参数值为registry
,因此会只是RegistryProtocol#refer
,又由于被Wrapper
类装配,因此会先执行三个Wrapper类,最后才能执行到RegistryProtocol#refer -> RegistryProtocol#doRefer
,在doRefer
方法中会订阅服务提供者地址,最后返回Invoker
对象。)
那么究竟是如何生成的Invoker
对象呢?我们来看下具体代码:
1 | private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) { |
上述代码中,步骤1根据URL生成了一个RegistryDirectory
(关于Directory接口的作用,可以自行查询一些,直白一些就是将一堆Invoker对象封装成一个List<Invoker>
,只有2种实现RegistryDirectory & StaticDirectory
,从命名可看出一个是动态可变,一个不可变),代码2 封装了订阅所要使用的参数信息,代码3则是封装绑定路由规则链,代码4订阅。代码5调用 Cluster$Adaptive#join
方法生成Invoker
对象。
在代码2种从zk获取服务提供者信息:
一旦zk返回服务提供者列表之后,就会调用
RegistryDirectory#notify
,如下:
在org.apache.dubbo.common.utils.UrlUtils#isMatch
中对provider和consumer的api进行匹配校验。继续跟踪:RegistryDirectory#notify -> RegistryDirectory#refreshOverrideAndInvoker -> RegistryDirectory#refreshInvoker -> RegistryDirectory#toInvokers
在toInvokers
正式将URL转换为Invoker
,通过invoker = new InvokerDelegate<>(protocol.refer(serviceType, url), url, providerUrl);
在这里protocol#refer
同样执行顺序如:
(dubbo 2.7.5) protocol#refer -> protocol$Adaptive#refer -> QosProtocolWrapper#refer -> ProtocolListenerWrapper#refer -> ProtocolFilterWrapper#refer ->AbstractProtocol#refer->DubboProtocol#protocolBindingRefer
,调用代码如下:
1 |
|
关注getClients
,其中执行了DubboProtocol#getSharedClient -> DubboProtocol#initClient
创建netty client进行连接。
因为这里使用的是明确的DubboInvoker
,在回调的时候,Wrapper会对该Invoker进行包装,执行效果如下:
因此,会执行到ProtocolFilterWrapper#buildInvokerChain
,该函数会对服务进行调用链跟踪:
1 | private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) { |
所有的负载均衡、容错策略等都是在这里绑定的。
7. 如果有多个注册中心,将会循环执行步骤6,将URL转换为Invoker
对象,然后添加到一个List,分别进行注册之后,然后判断最后一个注册中心url
是否有效,针对多订阅的场景,URL中添加cluster
参数,默认使用org.apache.dubbo.rpc.cluster.support.registry.ZoneAwareCluster
策略,使用org.apache.dubbo.rpc.cluster.Cluster#join
将多个Invoker
对象封装一个虚拟的Invoker
中,否则如果最后一个注册中心是无效的,直接封装Invoker
对象。
8. 创建服务代理ProxyFactory#getProxy(org.apache.dubbo.rpc.Invoker<T>)
,因为ProxyFactory
是一个适配类。那么同样这里会调用ProxyFactory$Adaptive#getProxy
,这里最终就是返回一个代理服务的Invoker对象。
至此,我们的消费端的绑定远程zk的服务就已经结束了。
那么,我们在调用服务器方法的时候服务器端和客户端都是如何处理的呢?下节我们将继续分析。
consumer发起远程调用过程
在上一节我们有提到,当消费端通过ReferenceConfig#get
获取到一个代理服务类,我们来看JavassistProxyFactory
在生成返回对象之前的代码:
1 | public class JavassistProxyFactory extends AbstractProxyFactory { |
在返回对象之前,传入了一个org.apache.dubbo.rpc.proxy.InvokerInvocationHandler
实例,这个实例是一个拦截器实例,所以当消费者调用提供者接口方法的时候,会被该InvokerInvocationHandler
拦截,具体流程如下:.
组件篇
DubboBootstrap
since 2.7.5
ApplicationModel
ExtensionLoader
ConfigManager
Environment
ServiceRepository
ServiceDescriptor
Invoker
Node
Node 这个接口继承者比较多,像 Registry、Monitor、Invoker、Directory 等均继承了这个接口。这个接口包含了一个获取配置信息的方法 getUrl,实现该接口的类可以向外提供配置信息。
Directory
org.apache.dubbo.rpc.cluster.Directory
是一个SPI扩展接口,继承自org.apache.dubbo.common.Node
,由抽象类org.apache.dubbo.rpc.cluster.directory.AbstractDirectory
实现,目前(2.7.5)有2个具体实现类:
RegistryDirectory
顾名思义,这个是代表会根据注册中心的服务变化而变化。根据他们的声明也能看出。
1 | private volatile List<Invoker<T>> invokers; |
另外,RegistryDirectory
实现了 NotifyListener
接口,当注册中心节点信息发生变化后,RegistryDirectory
可以通过此接口方法得到变更信息,并根据变更信息动态调整内部 Invoker
列表。
StaticDirectory
初始化所有的invoker之后不在改变。
1 | private final List<Invoker<T>> invokers; |
一个Directory
代表一组Invoker
对象,对于消费端来说,一个invoker对象代表一个服务提供者。Directory
内部通过List来维护invoker,并且list的内容是动态变化的,通过Directory
,服务消费者可获取到服务提供者的信息,比如 ip、端口、服务协议等。
RegistryDirectory
图解
创建RegistryDirectory
从上图中可以看出,在调用RegistryProtocol#refer
方法时,由于它是一个SPI,因此这里通过Protocol$Adaptive#refer
来调用,在consumer启动的时候,我们通过增强SPI获取到了Protocol
有3个Wrapper
包装类,因此,这里会一步一步执行QosProtocolWrapper
->ProtocolFilterWrapper
->ProtocolListenerWrapper
->RegistryProtocol
,进到org.apache.dubbo.registry.integration.RegistryProtocol#doRefer
之后,我们可以看到创建了RegistryDirectory
对象。创建 / 更新
List<Invoker>
如前文所述,Directory 核心其实就是封装一个List<Invoker>
,那该属性值的变动就比较重要了,当服务消费者启动时,会调用ReferenceConfig#get
来获取实现代理类,参考上小结请求方法,最终会调用到RegistryProtocol#doRefer
方法,代码如下:
1 | private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) { |
继续跟踪directory.subscribe
方法,一直执行到ZookeeperRegistry#doSubscribe
之中,发现zkClient
添加了一个针对path=/dubbo/com.sxzhongf.deep.in.dubbo.api.service.IGreetingService/providers
节点的监听器org.apache.dubbo.remoting.zookeeper.ChildListener
,如下图:,一旦服务提供段发现变化,zkClient就会获取到最新的节点信息,如下图:
一旦服务端发生变化,zkClient会通知到
继而执行更换替换的动作:...-> RegistryDirectory#notify -> RegistryDirectory#refreshOverrideAndInvoker -> RegistryDirectory#refreshInvoker
,如下图:
然后在toInvoker方法中调用protocol$Adaptive$refer
来生成Invoker
对象,因为使用的是适配类对象,因此具体执行的是RegistryProtocol#refer
,同样是经过QosProtocolWrapper
、ProtocolFilterWrapper
以及ProtocolListenerWrapper
装配过处理。
至此,Url -> Invoker流程就清晰了,如何赋值和修改List<Invoker>
也就知道了。
StaticDirectory
从名称上看,为静态服务资源目录,内部存放的List<Invoker>
是不会变,我们主要看该类中的2个方法:
1 |
|
目前(dubbo 2.7.5)StaticDirectory
仅用于多注册中心合并invoker的时候使用,判断代码如下:
1 | this.multiGroup = group != null && (ANY_VALUE.equals(group) || group.contains(",")); |
Router
服务路由是什么?
服务路由包含一条路由规则,路由规则决定了服务消费者的调用目标,即规定了服务消费者可调用哪些服务提供者。
Directory 在更新List<Invoker>
的时候,会通过 Router 进行服务路由,筛选出符合路由规则的服务提供者,
在org.apache.dubbo.registry.integration.RegistryProtocol#doRefer
方法中我们可以看到有一段代码directory.buildRouterChain(subscribeUrl);
,它是根据URL来生成调用规则链的,代码如下:
1 | //org.apache.dubbo.registry.integration.RegistryDirectory#buildRouterChain |
另外,在得到zkClient#notify之后,会执行org.apache.dubbo.registry.integration.RegistryDirectory#refreshInvoker
,
1 | private void refreshInvoker(List<URL> invokerUrls) { |
在消费方根据集群策略,比如默认的FailoverClusterInboker
获取服务提供者对应的List<invoker>
的时候,会执行FailoverClusterInvoker#doInvoke -> AbstractClusterInvoker#list -> Directory#list -> AbstractDirectory#list -> RegistryDirectory#doList -> RouterChain#route
对服务进行路由。