服务消费的实现原理
在介绍了服务暴露原理之后,我们重点探讨服务是如何消费的。
本节主要讲解如何通过注册中心进行服务发现(包括多注册中心服务消费)和绕过注册中心进行远程服务调用等细节。服务端会执行调用拦截,客户端拦截也会在本节探讨。
单注册中心消费原理
在详细探讨服务消费细节之前,我们先看整体RPC的消费原理,如图5-6所示。
在整体上看,Dubbo框架做服务消费也分为两大部分,第一步通过持有远程服务实例生成Invoker,这个Invoker在客户端是核心的远程代理对象。第二步会把Invoker通过动态代理转换成实现用户接口的动态代理引用。这里的Invoker承载了网络连接、服务调用和重试等功能,在客户端,它可能是一个远程的实现,也可能是一个集群实现。
接下来我们深入探讨内部框架处理的细节,框架真正进行服务引用的入口点在ReferenceBean#getObject,不管是XML还是注解,都会转换成ReferenceBean,它继承自ReferenceConfig,在服务消费前也会按照5.2.1节的覆盖策略生效,主要处理思路就是遍历服务的所有方法,如果没有值则会尝试从 D选项中读取,如果还没有则自动从配置文件dubbo.properties 中读取。
Dubbo支持多注册中心同时消费,如果配置了服务同时注册多个注册中心,则会在ReferenceConfig#createProxy 中合并成一个 Invoker,如代码清单 5-18 所示。
在createProxy实现中完成了远程代理对象的创建及代理对象的转换等工作,为了聚焦重点和方便讲解,对现有代码做了部分删减(不影响理解核心流程)。在①中会优先判断是否在同一个JVM中包含要消费的服务,默认场景下,Dubbo会通过②找出内存中injvm协议的服务,其实injvm协议是比较好理解的,前面提到服务实例都放到内存map中,消费也是直接获取实例调用而已。③:主要在注册中心中追加消费者元数据信息,应用启动时订阅注册中心、服务提供者参数等合并时会用到这部分信息。④:处理只有一个注册中心的场景,这种场景在客户端中是最常见的,客户端启动拉取服务元数据,订阅provider、路由和配置变更。⑤和⑥:分别处理多注册中心的场景,详细内容会在后面讲解。
当经过注册中心消费时,主要通过RegistryProtocol#refer触发数据拉取、订阅和服务Invoker转换等操作,其中最核心的数据结构是RegistryDirectory,如代码清单5-19所示。
这段逻辑主要完成了注册中心实例的创建,元数据注册到注册中心及订阅的功能。在①中会根据用户指定的注册中心进行协议替换,具体注册中心协议会在启动时用registry存储对应值。在②中会创建注册中心实例,这里的URL其实是注册中心地址,真实消费方的元数据信息是放在refer属性中存储的。在③中主要提取消费方refer中保存的元数据信息,如果包含多个分组值则会把调用结果值做合并处理。在④中触发真正的服务订阅和Invoker转换。在⑤中RegistryDirectory实现了 NotifyListener接口,服务变更会触发这个类回调notify方法,用于重新引用服务。在⑥中负责把消费方元数据信息注册到注册中心,比如消费方应用名、IP和端口号等。在⑦中处理provider、路由和动态配置订阅。在⑧中除了通过Cluster将多个服务合并,同时默认也会启用FailoverCluster策略进行服务调用重试。
具体远程Invoker是在哪里创建的呢?客户端调用拦截器又是在哪里构造的呢?当在⑦中第一次发起订阅时会进行一次数据拉取操作,同时触发RegistryDirectory#notify方法,这里的通知数据是某一个类别的全量数据,比如providers和routers类别数据。当通知providers数据时,在RegistryDirectory#toInvokers方法内完成Invoker转换。下面是具体细节,如代码清单5-20所示。
代码清单5-20 Dubbo服务消费服务通知
private MapcString, Invoker> toInvokers(List urls) {
String queryProtocols = this.queryMap.get(Constants.PROTOCOL_KEY);
for (URL providerllrl : urls) (
if (queryProtocols != null && queryProtocols.length() > 0) {
boolean accept = false;
String[] acceptProtocols = queryProtocols.split(",');
for (String acceptProtocol : acceptProtocols) { ①根据消费方protocol配置过滤不匹配协议
if (providerUrl.getProtocol().equals(acceptProtocol)) (
accept = true;
break;
}
}
if (laccept) (
continue;
)
)
URL url = mergeUrl(providerUrl);② 合并provider端配置数据,比如服务端IP和port等
String key = url.toFullString();
if (keys.contains(key)) { // Repeated url③忽略重复推送的服务列表
continue;
}
keys.add(key);
MapInvoker> localllrllnvokerMap = this.urllnvokerMap;Invoker invoker = localUrllnvokerMap == null ? null :localUrllnvokerMap.get(key);if (invoker == null) { // Not in the cache, refer againif (enabled) {invoker = new InvokerDelegate(protocol. refer(serviceType^ url)? url,providerUrl); ④使用具体协议创建远程连接)if (invoker != null) {newlIrllnvokerMap. put (key, invoker);}}...}keys.clear();return newUrllnvokerMap;)
Dubbo框架允许在消费方配置只消费指定协议的服务,具体协议过滤在①中进行处理,支持消费多个协议,允许消费多个协议时,在配置Protocol值时用逗号分隔即可。在②中消费信息是客户端处理的,需要合并服务端相关信息,比如远程IP和端口等信息,通过注册中心获取这些信息,解耦了消费方强绑定配置。在③中消除重复推送的服务列表,防止重复引用。在④中使用具体的协议发起远程连接等操作。在真实远程连接建立后也会发起拦截器构建操作,可参考5.2.2节的图5-4,处理机制类似,只不过处理逻辑在ProtocolFilterWrapper#refer中触发链式构造。
具体Invoker创建是在DubboProtocol#refer中实现的,Dubbo协议在返回Dubbolnvoker对象之前会先初始化客户端连接对象。Dubbo支持客户端是否立即和远程服务建立TCP连接是由参数是否配置了 lazy属性决定的,默认会全部连接。DubboProtocol#refer内部会调用DubboProtocol#initClient负责建立客户端连接和初始化Handler,如代码清单5-21所示。
①:支持lazy延迟连接,在真实发生RPC调用时创建。②:立即发起远程TCP连接,具体使用底层传输也是根据配置transporter决定的,默认是Netty传输。在②中会触发HeaderExchanger#connect调用,用于支持心跳和在业务线程中编解码Handler,最终会调用Transporters#connect生成Netty客户端处理。详细的Handler逻辑会在后面讲解。
回到5.3.1节,在⑦中把Invoker转换成接口代理。最终代理接口都会创建InvokerlnvocationHandler,这个类实现了 JDk 的 InvocationHandler 接口,所以服务暴露的Dubbo接口都会委托给代理去发起远程调用(injvm协议除外)。
多注册中心消费原理
在实际使用过程中,我们更多遇到的是单注册中心场景,但是当跨机房消费时,Dubbo框架允许同时消费多个机房服务。默认Dubbo消费机房的服务顺序是按照配置注册中心的顺序决定的,配置靠前优先消费。
多注册中心消费原理比较简单,每个单独注册中心抽象成一个单独的Invoker,多个注册中心实例最终通过StaticDirectory保存所有的Invoker,最终通过Cluster合并成一个Invoker。
我们可以回到5.3.1节代码清单5-18代码逻辑中,⑤是逐个获取注册中心的服务,并添加到invokers列表,这里多个注册中心逐一获取服务。⑥主要将集群服务合并成一个Invoker,这里也不难理解,第一层包含的服务Invoker是注册中心实例,对应注册中心实例的Invoker对象内部持有真实的服务提供者对象列表。这里还有一个特殊点,在多注册中心场景下,默认使用的集群策略是available,如代码清单5.22所示。
在①中实现dolnvoke实际持有的invokers列表是注册中心实例,比如配置了 ZooKeeper和etcd3注册中心,实际调用的invokers列表只有2个元素。在②中会判断具体注册中心中是否有服务可用,这里发起的invoke实际上会通过注册中心RegistryDirectory获取真实provider机器列表进行路由和负载均衡调用。到这里,读者应该能够理解Dubbo所有的概念都在向Invoker靠拢。
使用多注册中心进行服务消费时,给框架开发者和扩展特性的开发人员带来了一些挑战,特别是在编写同机房路由时,在服务路由层获取的也是注册中心实例Invoker,需要进入Invoker内部判断服务列表是否符合匹配规则,如果匹配到符合匹配规则的机器,则这个时候只能把外层注册中心Invoker返回,否则会破坏框架服务调用的生命周期(导致跳过MockClusterlnvoker服务调用)。
直连服务消费原理
Dubbo可以绕过注册中心直接向指定服务(直接指定目标IP和端口)发起RPC调用,使用直连模式可以方便在某些场景下使用,比如压测指定机器等。Dubbo框架也支持同时指定直连多台机器进行服务调用,如代码清单5-23所示。
在①中允许用分号指定多个直连机器地址,多个直连机器调用会使用负载均衡,更多场景是单个直连,但是不建议在生产环境中使用直连模式,因为上游服务发布会影响服务调用方。
在②中允许配置注册中心地址,这样可以通过注册中心发现服务消费。在③中指定服务调用协议、IP和端口,注意这里的URL没有添加refer和注册中心协议,默认是Dubbo会直接触发DubboProtocol进行远程消费,不会经过Registryprotocol去做服务发现。
优雅停机原理解析
优雅停机特性是所有RPC框架中非常重要的特性之一,因为核心业务在服务器中正在执行时突然中断可能会出现严重后果。接下来我们详细探讨Dubbo框架内部实现优雅停机的原理,如图5-7所示。
Dubbo中实现的优雅停机机制主要包含6个步骤:
(1)收到kill 9进程退出信号,Spring容器会触发容器销毁事件。
(2) provider端会取消注册服务元数据信息。
(3) consumer端会收到最新地址列表(不包含准备停机的地址)。
(4) Dubbo协议会发送readonly事件报文通知consumer服务不可用。
(5 )服务端等待已经执行的任务结束并拒绝新任务执行。
可能读者会有疑问,既然注册中心已经通知了最新服务列表,为什么还要再发送readonly报文呢?这里主要考虑到注册中心推送服务有网络延迟,以及客户端计算服务列表可能占用一些时间。Dubbo协议发送readonly时间报文时,consumer端会设置响应的provider为不可用状态,下次负载均衡就不会调用下线的机器。
Dubbo 2.6.3以后修复了优雅停机的一些bug,在以前的版本中没有做到完全优雅停机的原因是Spring注册了 JVM停止的钩子,Dubbo也注册了 JVM停止的钩子,这种场景下两个并发执行的线程可能引用已经销毁的资源,导致优雅停机失去了意义。比如,Dubbo正在执行的任务需要引用Spring中的Bean,但这时Spring钩子函数已经关闭了 Spring的上下文状态,导致访问任何Spring资源都会报错。
小结
本章我们首先对Dubbo中XML schema约束文件进行了讲解,也包括如何映射到对应Java对象中。现在越来越多地使用注解的方式,我们也对注解的解析核心流程进行了探讨。然后对Dubbo框架的几种服务暴露原理进行了详解,紧接着对服务消费进行了讲解,这些服务暴露和消费对所有的协议都具有参考价值。最后我们对优雅停机的原理进行了探讨,也对以前的实现缺陷的原因进行了概述。
本篇对Dubbo框架的主流程原理进行了梳理,觉得文章不错的话,可以转发关注一下小编!!!
下一篇将深入探讨Dubbo框架内部特性和细节,欢迎大家来围观!!!
深入ApacheDubbo与实战:Dubbo配置解析+Dubbo服务暴露原,快上车
大数据架构师分享ExtensionLoader工作原理+扩展点动态编译的实现
大数据架构师教你学,Dubbo扩展点加载机制:加载机制+扩展点注解