Dubbo
整体架构
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<Invoker> ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更
Directory
获取invoker
是从methodInvokerMap
中获取的,主要都是读操作,那它的写操作是在什么时候写的呢?就是在回调方法notify
的时候操作的,也就是注册中心有变化,则更新methodInvokerMap
和urlInvokerMap
的值
Router接口
- MockInvokersSelector
- ConditionRouter
- ScriptRouter
ScriptRouter:脚本路由规则 支持 JDK 脚本引擎的所有脚本,比如:javascript, jruby, groovy 等,通过 type=javascript 参数设置脚本类型,缺省为 javascript。
ConditionRouter(条件路由)
根据dubbo-admin配置的路由规则来过滤相关的invoker,当我们对路由规则点击启用,就会触发RegistryDirectory
类的notify
方法。
notify方法调用refreshInvoker方法。
route方法的实现类为ConditionRoute 根据条件进行过滤
1、调用mathThen方法
2、调用matchCondition方法
3、调用isMatch判断
4、调用isMatchGlobPattern方法
Cluster
Cluster 将 Directory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个
集群模块是服务提供者和服务消费者的中间层,为服务消费者屏蔽了服务提供者的情况,这样服务消费者就可以专心处理远程调用相关事宜。比如发请求,接受服务提供者返回的数据等。这就是Dubbo Cluster集群的作用。
<dubbo:service cluster="failsafe" />
通过cluster来指定集群容错方式
其实就是应对出错情况采取的策略
- MergeableCluster 分组聚合 按组合并返回结果,用group区分,比如消费者需要从每种group中调用一次返回结果,合并结果返回。
- AvailableCluster 可用的,遍历所有的Invokers,判断isAvalible,只要一个有为true的直接调用返回,否则就抛出异常。
- ForkingCluster 并行调用多个服务器,只要一个成功即返回
- FailfastCluster 快速失败,只发起一次调用,失败立即报错,通常用于非幂等性的写操作,比如新增
- MockClusterWrapper 本地mock用于服务降级,如果服务提供方全部挂掉后,客户端通过mock返回授权失败
- FailoverCluster 失败自动切换,当出现失败时重试其他服务器,通常用于读操作,可通过retries来设置重试次数。所以如果是读接口,适合用FailoverCluster,如果是写接口,适合FailfastCluster
- FailbackCluster 失败自动恢复,后台记录失败请求,定时重发,通常用于消息通知操作
- FailsafeCluster 失败安全,出现异常时,直接忽略,通常用于写入审计日志等操作。
- BroadcastCluster 广播调用所有提供者,逐个调用,任意一台报错则报错,通常用于通知所有提供者更新缓存或者日志等本地资源信息。
粘滞链接
用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非提供者挂了,再连另一台,自动开启延迟链接,以减少长接数
<dubbo:referenc id="xxxService" interface="com.xxx.xxx" sticky="true"
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:consumer loadbalance="smart"/>
dubbo服务支持参数动态调整,例如动态调整权重,但dubbo实现方式较为特殊,并不是常规思路。
普通状态
默认情况下,任意一台dubbo服务端上线后会注册到配置中心(zk),路径为/<group>/<service>/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的 /<group>/<service>/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
ServiceConfig类拿到对外提供服务的实际类ref,然后通过ProxyFactory类的getInvoker方法使用ref生成一个AbstractProxyInvoker实例,到这一步就完成具体服务到Invoker的转换(javassistProxyFacory、JdkProxyFactory),接着要做Invoker转换到Export的过程
服务发布:本地暴露、远程暴露
为什么会有本地暴露
和远程暴露
呢?不从场景考虑讨论技术的没有意义是.在dubbo中我们一个服务可能既是Provider
,又是Consumer
,因此就存在他自己调用自己服务的情况,如果再通过网络去访问,那自然是舍近求远,因此他是有本地暴露
服务的这个设计.从这里我们就知道这个两者的区别
- 本地暴露是暴露在JVM中,不需要网络通信. url是inJvm开头
- 远程暴露是将ip,端口等信息暴露给远程客户端,调用时需要网络通信.url是registry开头
本地暴露:
远程暴露
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() 方法,这样在服务下线时清理相关资源。
- 委托具体协议(Dubbo)进行服务暴露,创建NettyServer监听端口和保存服务实例。
- 创建注册中心对象,与注册中心创建TCP连接。
- 注册服务元数据到注册中心。
- 订阅configuators节点,监听服务动态属性变更事件。
- 服务销毁收尾工作,比如关闭端口、反注册服务信息等。
Filter 在服务暴露前,做拦截器初始化,在加载所有拦截器时会过滤支队provider生效的数据。
ZK连接
dubbo中zookeeper做注册中心,如果注册中心集群都挂掉,那发布者和订阅者还能通信吗?
可以。zookeeper的信息会缓存到本地作为一个缓存文件,并且转换成properties
对象方便使用。建立线程池,定时检测并连接注册中心,失败了就重连。
创建节点
注册服务到zk其实就是在zk上创建临时节点,当节点下线或者down掉时,即会删除临时节点,从而使服务从可用列表中剔除。
持久节点
所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点,也就是说不会因为创建该节点的客户端会话失效而消失
临时节点
临时节点的生命周期和客户端会话绑定,也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉
1、export的时候进行zk订阅
2、设置监听回调的地址,回调给FailbackRegistry的notify
3、创建持久节点
4、设置对该节点的监听
5、更新新的服务信息,服务启动和节点更新回调,都会调用到这里
6、更新缓存文件
7、对比新旧信息是否有变化,有则重新暴露服务
服务暴露总结:
服务降级
为什么需要服务降级
高并发大业务量情况下,暂时屏蔽边缘业务
怎么做?
MockClusterInvoker
- no mock 直接调用
- force:direct mock 直接不调用,返回一个之前设置的值
- fail-mock 调用失败后,返回一个设置的值
服务引用
Dubbo 服务引用的时机有两个,第一个是在 Spring 容器调用 ReferenceBean 的 afterPropertiesSet 方法时引用服务,第二个是在 ReferenceBean 对应的服务被注入到其他类中时引用。这两个引用服务的时机区别在于,第一个是饿汉式的,第二个是懒汉式的。默认情况下,Dubbo 使用懒汉式引用服务。如果需要使用饿汉式,可通过配置 <dubbo:reference> 的 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)
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 的源码。