Dubbo——服务调用、服务暴露、服务引用过程

Dubbo

整体架构

image-20200905202820262
image-20200905202931254

1、InvokerInvocationHandler jdk动态代理

5、RegistryDirector返回Invokers
Router分为:Script 脚本路由、Condition 条件路由

6、通过MockInvokersSelector的route方法(getNormalInvokers)拿到能正常执行的invokers

8、当回到AbstractClusterInvoker后,执行(默认FailoverClusterInvoker,根据配置的是,Failfast Cluster(快速失败),Failsafe Cluster(失败安全),Failback Cluster(失败自动恢复),Forking Cluster(并行调用多个服务器,只要一个成功即返回),Broadcast Cluster(广播调用所有提供者,逐个调用,任意一台报错则报错))doInvoker方法

9、FailoverClusterInvoker调用AbstractClusterInvoker的select方法

10、执行doSelect方法

11、调用AbstractLoadbalance的select方法

12、根据配置的负载均衡策略调用对应的(如RoundRobinLoadBalance)类的doSelect方法

13、返回invokers.get()方法

14、调用FailoverClusterInvoker的invoke方法

  • Directory中找出本次集群中的全部invokers
  • Router中,将上一步的全部invokers挑选出能正常执行的invokers
  • LoadBalance中,将上一步的能正常的执行invokers中,根据配置的负载均衡策略,挑选出需要执行的invoker

Director接口

  • StaticDirectory Invoker通过构造函数传入,所以不是动态变化的,用的较少。
  • RegistryDirectory 实现了NotifyListener接口,notify方法就是注册中心的回调,可以根据注册中心动态变化

均继承自抽象类AbstractDirectory

Directory 代表多个 Invoker,可以把它看成 List ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更

Directory获取invoker是从methodInvokerMap中获取的,主要都是读操作,那它的写操作是在什么时候写的呢?就是在回调方法notify的时候操作的,也就是注册中心有变化,则更新methodInvokerMapurlInvokerMap的值

Router接口

  • MockInvokersSelector
  • ConditionRouter
  • ScriptRouter

ScriptRouter:脚本路由规则 支持 JDK 脚本引擎的所有脚本,比如:javascript, jruby, groovy 等,通过 type=javascript 参数设置脚本类型,缺省为 javascript。

ConditionRouter(条件路由)

根据dubbo-admin配置的路由规则来过滤相关的invoker,当我们对路由规则点击启用,就会触发RegistryDirectory类的notify方法。

img

notify方法调用refreshInvoker方法。

route方法的实现类为ConditionRoute 根据条件进行过滤

1、调用mathThen方法

2、调用matchCondition方法

3、调用isMatch判断

4、调用isMatchGlobPattern方法

Cluster

Cluster 将 Directory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个

集群模块是服务提供者和服务消费者的中间层,为服务消费者屏蔽了服务提供者的情况,这样服务消费者就可以专心处理远程调用相关事宜。比如发请求,接受服务提供者返回的数据等。这就是Dubbo Cluster集群的作用。


通过cluster来指定集群容错方式

其实就是应对出错情况采取的策略

img
  • MergeableCluster 分组聚合 按组合并返回结果,用group区分,比如消费者需要从每种group中调用一次返回结果,合并结果返回。
  • AvailableCluster 可用的,遍历所有的Invokers,判断isAvalible,只要一个有为true的直接调用返回,否则就抛出异常。
  • ForkingCluster 并行调用多个服务器,只要一个成功即返回
  • FailfastCluster 快速失败,只发起一次调用,失败立即报错,通常用于非幂等性的写操作,比如新增
  • MockClusterWrapper 本地mock用于服务降级,如果服务提供方全部挂掉后,客户端通过mock返回授权失败
  • FailoverCluster 失败自动切换,当出现失败时重试其他服务器,通常用于读操作,可通过retries来设置重试次数。所以如果是读接口,适合用FailoverCluster,如果是写接口,适合FailfastCluster
  • FailbackCluster 失败自动恢复,后台记录失败请求,定时重发,通常用于消息通知操作
  • FailsafeCluster 失败安全,出现异常时,直接忽略,通常用于写入审计日志等操作。
  • BroadcastCluster 广播调用所有提供者,逐个调用,任意一台报错则报错,通常用于通知所有提供者更新缓存或者日志等本地资源信息。

粘滞链接

用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非提供者挂了,再连另一台,自动开启延迟链接,以减少长接数

LoadBalance

  • RandomLoadBalance 随机 按权重设置随机概率
  • RoundRobinLoadBalance 轮询,按权重设置轮询比率
  • LeastActiveLoadBalance 最少活跃数 根据当前连接数来分配,越慢的提供者收到越少的请求。
  • ConsistenHashLoadaBalance 一致性哈希 相同参数的请求总是发到同一提供者,如果一台机器挂了,则平摊分发到虚拟节点,默认只对第一个参数做hash,默认160个虚拟节点

自定义负载均衡算法——原理:

​ 启动时服务提供者将当前进程启动时间注册到ZK;服务消费者发现该节点后计算服务启动时间(相对当前时间),在默认预热时间的前20%时间内,该节点权重始终固定为2,这样客户端的负载均衡器只会分发极少的请求至节点。

​ 在预热时间之后的80%时间内,该节点权重将随着时间的推移而线性增长;待预热时间到期后,权重自动恢复为默认值100;负载均衡器的内核是一个标准的WLC算法模块,即加权最少连接算法;

​ 如果某个节点Hang住或宕机,其权重会迅速自动调节降低,避免持续性影响;当节点下线时,服务端提前触发权重调节,重载默认权重至1并发布到注册中心,服务消费者将迅速感知到该事件;
服务提供者优雅下线步骤(注意这套逻辑仅在服务端执行)在ok.htm?down=true对应的controller中加入下列逻辑,注意要判断down是否为true,因为正常来说false表示启动验证而不是关机

  • 调用smartServerHelper.setOverrideWeight(1);将当前服务端的权重设置为1;等待5秒;
  • 调用smartServerHelper.prepareShutdown();将当前服务端置为临时禁用(当进程结束后该状态会被自动清除)
  • 等待1秒;
  • 正常关闭进程;
  • 在关机脚本(一般为shutdown)的stop方法中,调用/ok.htm?down=true

服务者消费者配置


dubbo服务支持参数动态调整,例如动态调整权重,但dubbo实现方式较为特殊,并不是常规思路。

普通状态
默认情况下,任意一台dubbo服务端上线后会注册到配置中心(zk),路径为///providers

节点名为完整的服务URI,类似下面

dubbo%3A%2F%2F10.57.17.156%3A20880%2Fcn.tongdun.arch.dbench.IDbenchService%3Fanyhost%3Dtrue%26application%3Ddbench%26default.remote.timestamp%3D1553248880792%26dubbo%3D2.8.4%26generic%3Dfalse%26interface%3Dcn.tongdun.arch.dbench.IDbenchService%26methods%3DgetTotalPayloadSize%2CtestWithSleep%26pid%3D798%26revision%3D1.0%26side%3Dprovider%26timestamp%3D1553248881596%26version%3D1.0.1%26warmup%3D600000
这个节点本身是具有瞬时性和不变性,机器下线时将自动删除,那dubbo怎么保存动态修改的值呢?

覆盖状态
对于动态配置,dubbo将在zk的 ///configurators 路径下单独发布一条URI,例如调整某台机器权重后,会产生下面的节点override%3A%2F%2F10.57.17.156%3A20880%2Fcn.tongdun.arch.dbench.IDbenchService%3Fcategory%3Dconfigurators%26dynamic%3Dtrue%26version%3D1.0.1%26weight%3D1
这个节点不具备瞬时性(跟普通状态的节点不一样),也就是说这条动态配置一经发布,不会自动删除。

另外,可以任意发布多条动态配置,最终体现在zk里的顺序是随机的。

客户端
客户端订阅服务后,会去zk拉取普通状态节点和覆盖状态下的所有节点,将所有的状态的节点进行叠加后,得到最终的订阅配置数据。

因此如果发布了动态配置而不删除,则可能会导致客户端得到的配置错乱,造成不可预料之后果

服务暴露原理

  • 暴露本地服务
  • 暴露远程服务
  • 启动netty
  • 连接zookeeper
  • 到zookeeper注册
  • 监听zookeeper
img
img

​ ServiceConfig类拿到对外提供服务的实际类ref,然后通过ProxyFactory类的getInvoker方法使用ref生成一个AbstractProxyInvoker实例,到这一步就完成具体服务到Invoker的转换(javassistProxyFacory、JdkProxyFactory),接着要做Invoker转换到Export的过程

​ 服务发布:本地暴露、远程暴露

​ 为什么会有本地暴露远程暴露呢?不从场景考虑讨论技术的没有意义是.在dubbo中我们一个服务可能既是Provider,又是Consumer,因此就存在他自己调用自己服务的情况,如果再通过网络去访问,那自然是舍近求远,因此他是有本地暴露服务的这个设计.从这里我们就知道这个两者的区别

  • 本地暴露是暴露在JVM中,不需要网络通信. url是inJvm开头
  • 远程暴露是将ip,端口等信息暴露给远程客户端,调用时需要网络通信.url是registry开头

本地暴露:

img

远程暴露

img
img

ServiceConfig ------> ref

ProxyFactory --------> Javassist、JDK动态代理

Invoker ----------> AbstractProxyInvoker

Protocol --------> Dubbo、injvm等

Exporter

1、spring启动,解析配置文件

2、创建dubbo标签解析器

3、解析dubbo标签

4、ServiceBean解析

5、容器创建完成,触发ContextRefrestEvent

6、export暴露服务

7、duExportUrls

8、doExportUrlsFor1Protocol

  • JavassistProxyFactory模式原理:创建Wrapper子类,在子类中实现invokeMethod方法,方法体内会为每个ref方法都做方法名和方法参数匹配校验,如果匹配则直接调用即可,相比JDKProxyFactory省去了反射调用的开销。
  • JDKProxyFactory:通过反射获取真实对象的方法,然后调用即可。

9、getInvoker

10、protocol.export

11、开启服务器 openServer()如nettyServer

12、注册服务到注册中心 registerProvider

getRegistry() 方法根据注册中心类型(默认 Zookeeper)获取注册中心客户端,由注册中心客户端实例来进行真正的服务注册。注册中心客户端将节点注册到注册中心,同时订阅对应的 override 数据,实时监听服务的属性变动实现动态配置功能。最终返回的 Exporter 实现了 unexport() 方法,这样在服务下线时清理相关资源。

  1. 委托具体协议(Dubbo)进行服务暴露,创建NettyServer监听端口和保存服务实例。
  2. 创建注册中心对象,与注册中心创建TCP连接。
  3. 注册服务元数据到注册中心。
  4. 订阅configuators节点,监听服务动态属性变更事件。
  5. 服务销毁收尾工作,比如关闭端口、反注册服务信息等。

Filter 在服务暴露前,做拦截器初始化,在加载所有拦截器时会过滤支队provider生效的数据。

ZK连接

dubbo中zookeeper做注册中心,如果注册中心集群都挂掉,那发布者和订阅者还能通信吗?

可以。zookeeper的信息会缓存到本地作为一个缓存文件,并且转换成properties对象方便使用。建立线程池,定时检测并连接注册中心,失败了就重连。

创建节点

注册服务到zk其实就是在zk上创建临时节点,当节点下线或者down掉时,即会删除临时节点,从而使服务从可用列表中剔除。

持久节点

所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点,也就是说不会因为创建该节点的客户端会话失效而消失

临时节点

临时节点的生命周期和客户端会话绑定,也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉

img

1、export的时候进行zk订阅

2、设置监听回调的地址,回调给FailbackRegistry的notify

3、创建持久节点

4、设置对该节点的监听

5、更新新的服务信息,服务启动和节点更新回调,都会调用到这里

6、更新缓存文件

7、对比新旧信息是否有变化,有则重新暴露服务

服务暴露总结:

img
img

服务降级

为什么需要服务降级

高并发大业务量情况下,暂时屏蔽边缘业务

怎么做?

MockClusterInvoker

  • no mock 直接调用
  • force:direct mock 直接不调用,返回一个之前设置的值
  • fail-mock 调用失败后,返回一个设置的值

服务引用

img

Dubbo 服务引用的时机有两个,第一个是在 Spring 容器调用 ReferenceBean 的 afterPropertiesSet 方法时引用服务,第二个是在 ReferenceBean 对应的服务被注入到其他类中时引用。这两个引用服务的时机区别在于,第一个是饿汉式的,第二个是懒汉式的。默认情况下,Dubbo 使用懒汉式引用服务。如果需要使用饿汉式,可通过配置 的 init 属性开启。下面我们按照 Dubbo 默认配置进行分析,整个分析过程从 ReferenceBean 的 getObject 方法开始。当我们的服务被注入到其他类中时,Spring 会第一时间调用 getObject 方法,并由该方法执行服务引用逻辑。按照惯例,在进行具体工作之前,需先进行配置检查与收集工作。接着根据收集到的信息决定服务用的方式,有三种,第一种是引用本地 (JVM) 服务,第二是通过直连方式引用远程服务,第三是通过注册中心引用远程服务。不管是哪种引用方式,最后都会得到一个 Invoker 实例。如果有多个注册中心,多个服务提供者,这个时候会得到一组 Invoker 实例,此时需要通过集群管理类 Cluster 将多个 Invoker 合并成一个实例。合并后的 Invoker 实例已经具备调用本地或远程服务的能力了,但并不能将此实例暴露给用户使用,这会对用户业务代码造成侵入。此时框架还需要通过代理工厂类 (ProxyFactory) 为服务接口生成代理类,并让代理类去调用 Invoker 逻辑。避免了 Dubbo 框架代码对业务代码的侵入,同时也让框架更容易使用。

  • 将spring的schemas标签信息转换ReferenceBean,然后通过这个bean的信息,连接、订阅zookeeper节点信息创建一个invoker
  • invoker的信息创建一个动态代理对象(createProxy)
  • 1、加载配置中心拼装成urls
  • 2、遍历urls调用refProtocol创建远程的动态代理Invoker
  • 3、调用proxyFacotry创建服务代理(javassistProxyFacotry)
img
img

SPI

​ SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,我们先来了解一下 Java SPI 与 Dubbo SPI 的用法,然后再来分析 Dubbo SPI 的源码。

image-20200905202628624
image-20200905202647690

你可能感兴趣的:(Dubbo——服务调用、服务暴露、服务引用过程)