Dubbo原理解析(非常透彻)

2022-10-19,,,

一.概述

dubbo是一款经典的rpc框架,用来远程调用服务的。

dubbo的作用:

面向接口的远程方法调用
智能容错和负载均衡
服务自动注册和发现。
自定义序列化协议


Dubbo 架构中的核心角色有哪些?

Container 服务运行容器,负责加载、运行服务提供者。必须。
Provider 暴露服务的服务提供方,会向注册中心注册自己提供的服务。必须。
Consumer 调用远程服务的服务消费方,会向注册中心订阅自己所需的服务。必须。
Registry 服务注册与发现的注册中心。注册中心会返回服务提供者地址列表给消费者。非必须。
Monitor: 统计服务的调用次数和调用时间的监控中心。服务消费者和提供者会定时发送统计数据到监控中心。非必须。


Dubbo 中的 Invoker 概念了解么?

Invoker 是 Dubbo 领域模型中非常重要的一个概念,你如果阅读过 Dubbo 源码的话,你会无数次看到这玩意。就比如下面我要说的负载均衡这块的源码中就有大量 Invoker 的身影。

简单来说,Invoker 就是 Dubbo 对远程调用的抽象。分为2种:

服务提供 Invoker
服务消费 Invoker
我们需要调用一个远程方法,我们需要动态代理来屏蔽远程调用的细节吧!我们屏蔽掉的这些细节就依赖对应的 Invoker 实现, Invoker 实现了真正的远程服务调用


Dobbo的分层架构(工作原理)

Service业务层:就是我们写代码的层,我们使用rpc只需要关注该层就行,主要是定义接口和实现类。
config 配置层:Dubbo 相关的配置。支持代码配置,同时也支持基于 Spring 来做配置,以 ServiceConfigReferenceConfig 为中心
proxy 服务代理层:调用远程方法像调用本地的方法一样简单的一个关键,真实调用过程依赖代理类,以 ServiceProxy 为中心。
registry 注册中心层:封装服务地址的注册与发现。
cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心。
monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心。
protocol 远程调用层:封装 RPC 调用,以 InvocationResult 为中心。
exchange 信息交换层:封装请求响应模式,同步转异步,以 RequestResponse 为中心。
transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心。
serialize 数据序列化层 :对需要在网络传输的数据进行序列化。

 二.SPI

Dubbo 的 SPI 机制了解么?

SPI(Service Provider Interface) 机制被大量用在开源项目中(比如Dubbo,SpringBoot...),它可以帮助我们进行功能扩展。
SPI 的具体原理是这样的:我们将接口的实现类放在配置文件中,我们在程序运行过程中读取配置文件,通过反射加载实现类。这样,我们可以在运行的时候,动态替换接口的实现类。
一些配置类就可以使用SPI进行加载,如果我们需要进行扩展,就自定义类放在指定文件夹下。
Java 本身就提供了 SPI 机制的实现。不过,Dubbo 没有直接用,而是对 Java 原生的 SPI 机制进行了增强,以便更好满足自己的需求。


为什么 Dubbo 不用 JDK 的 SPI,而是要自己实现?

因为 Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。
因此 Dubbo 就自己实现了一个 SPI,给每个实现类配了个名字,通过名字去文件里面找到对应的实现类全限定名然后加载实例化,按需加载。

如果想深入研究Java SPI:请看一位大佬写的文章:SPI源码分析


如何扩展 Dubbo 中的默认实现?

比如说我们想要实现自己的负载均衡策略,我们创建对应的实现类 XxxLoadBalance 实现 LoadBalance 接口或者 AbstractLoadBalance 类。
我们将这个是实现类的路径写入到resources 目录下的 META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance文件中即可。

public class XxxLoadBalance implements LoadBalance {
public <T> Invoker<T> select(List<Invoker<T>> invokers, Invocation invocation) throws RpcException {
// ...
}
}

 三.Dubbo 的负载均衡策略

RandomLoadBalance:随机。随机的选择一个。是Dubbo的默认负载均衡策略。(加权/不加权)
RoundRobinLoadBalance:轮询。轮询选择一个。(加权/不加权)
LeastActiveLoadBalance:最少活跃数。每个服务维护一个活跃数计数器。当A机器开始处理请求,该计数器加1,此时A还未处理完成。若处理完毕则计数器减1。而B机器接受到请求后很快处理完毕。那么A,B的活跃数分别是1,0。当又产生了一个新的请求,则选择B机器去执行(B活跃数最小),这样使慢的机器A收到少的请求。
ConsistentHashLoadBalance:一致性哈希。相同参数的请求总是落在同一台机器上。

四.服务暴露,服务引用,服务调用过程

服务暴露生成代理类将信息注册到ZK

组装URL:Spring IOC 容器刷新完毕之后,会根据配置参数组装成 URL, 然后根据 URL 的参数来进行代理类的生成。
生成代理类:会通过 proxyFactory.getInvoker,利用 javassist 来进行动态代理,封装真的实现类。
根据协议生成暴露对象(exporter):通过 URL 参数选择对应的协议来进行 protocol.export,默认是 Dubbo 协议。
自适应:代理类会根据 Invoker 里面的 URL 参数得知具体的协议,然后通过 Dubbo SPI 机制选择对应的实现类进行 export。
注册到ZK:将 export 得到的 exporter 存入一个 Map 中,供之后的远程调用查找,然后会向注册中心注册提供者的信息。

想要深究源码的小伙伴可以看一位大佬写的文章:服务暴露源码分析。


服务引用:获取远程调用的类,生成代理类。可以看作服务引用的逆过程。

组装URL:会根据配置参数组装成 URL, 然后根据 URL 的参数来进行代理类的生成。(2种时机)

饿汉式:饿汉式是通过实现 Spring 的InitializingBean接口中的 afterPropertiesSet方法,容器通过调用 ReferenceBean的 afterPropertiesSet方法时引入服务。(在Spring启动时,给所有的属性注入实现类,包含远程和本地的实现类)
懒汉式:只有当这个服务被注入到其他类中时启动引入流程,也就是说用到了才会开始服务引入。
在应用的Spring Context初始化完成事件时触发,扫描所有的Bean,将Bean中带有@Reference注解的field获取到,然后创建field类型的代理对象,创建完成后,将代理对象set给此field。后续就通过该代理对象创建服务端连接,并发起调用。(dubbo默认)
从ZK中获取需要的服务提供者的信息:得到Map。
根据协议解析传过来的exporter:
协议的不同,获取的路径也不同:本地,直接,从ZK。

生成代理类:供消费者使用Netty进行远程调用。invoker。

想要深究源码的小伙伴可以看一位大佬写的文章:服务引用源码分析。


服务调用:

服务调用是在生成代理对象后,使用Netty,生成Netty Client进行远程调用。Netty Server通过反射返回调用结果。
在调用之前,就会进行智能容错和负载均衡。
首先客户端调用接口的某个方法,实际调用的是代理类,代理类会通过 cluster 从 directory(invoker的集合) 中获取一堆 invokers(如果有一堆的话),然后进行 router 的过滤(其中看配置也会添加 mockInvoker 用于服务降级),然后再通过 SPI 得到 loadBalance 进行一波负载均衡。

补充:cluster 是什么?

一个中间层,为消费者屏蔽了服务提供者的情况,简化了消费者的使用。
它可以负责选择哪个invoker返回给调用者,比如选择一个 invoker ,调用出错了可以换一个等等。

想要深究源码的小伙伴可以看一位大佬写的文章:服务调用源码分析。


容错机制:先容错,再负载均衡。

首先在服务引入的时候,将多个远程调用都塞入 Directory 中,然后通过 Cluster 来封装这个目录,封装的同时提供各种容错功能,比如 FailOver、FailFast 等等,最终暴露给消费者的就是一个 invoker。
然后消费者调用的时候会目录里面得到 invoker 列表,当然会经过路由的过滤,得到这些 invokers 之后再由 loadBalance 来进行负载均衡选择一个 invoker,最终发起调用。

dubbo常见的容错机制

Failover Cluster(默认)

失败自动切换,当出现失败,重试其它服务器。
通常用于读操作,但重试会带来更长延迟。
Failfast Cluster
快速失败,只发起一次调用,失败立即报错。
通常用于非幂等性的写操作,比如新增记录。
Failsafe Cluster
失败安全,出现异常时,直接忽略。
通常用于写入审计日志等操作。
Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。
通常用于消息通知操作。
Forking Cluster
并行调用多个服务器,只要一个成功即返回。
通常用于实时性要求较高的读操作,但需要浪费更多服务资源。

想要深究源码的小伙伴可以看一位大佬写的文章:dubbo智能容错源码分析

五.其他小问题

Dubbo 为什么默认用 Javassist?

很简单,就是快,且字节码生成方便。
其他常见的动态代理: JDK 的动态代理、ASM、cglib。
ASM 比 Javassist 更快,但是没有快一个数量级,而Javassist 只需用字符串拼接就可以生成字节码,而 ASM 需要手工生成,成本较高,比较麻烦。


Dubbo 支持多种序列化方式?

JDK 自带的序列化:不支持跨语言调用 ;性能差
JSON:性能差
ProtoBuf :支持跨语言
hessian2(默认):支持跨语言
Protostuff:支持跨语言
Kryo:新引入的,只支持JAVA
FST:新引入的,只支持JAVA

寄语:你所看到的惊艳,都曾被平庸历练

Dubbo原理解析(非常透彻)的相关教程结束。

《Dubbo原理解析(非常透彻).doc》

下载本文的Word格式文档,以方便收藏与打印。