Dubbo
Dubbo
1.何为 RPC?
**RPC(Remote Procedure Call)** 即远程过程调用。
**为什么要 RPC ?** 因为,两个不同的服务器上的服务提供的方法不在一个内存空间,所以,需要通过网络编程才能传递方法调用所需要的参数。并且,方法调用的结果也需要通过网络编程来接收。但是,如果我们自己手动网络编程来实现这个调用过程的话工作量是非常大的,因为,我们需要考虑底层传输方式(TCP还是UDP)、序列化方式等等方面。
**RPC 能帮助我们做什么呢?** 简单来说,通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。并且我们不需要了解底层网络编程的具体细节。
一言蔽之:**RPC 的出现就是为了让你调用远程方法像调用本地方法一样简单。**
2.RPC 的原理是什么?
- 客户端(服务消费端) :调用远程方法的一端。
- 客户端 Stub(桩) : 这其实就是一代理类。代理类主要做的事情很简单,就是把你调用方法、类、方法参数等信息传递到服务端。
- 网络传输 : 网络传输就是你要把你调用的方法的信息比如说参数啊这些东西传输到服务端,然后服务端执行完之后再把返回结果通过网络传输给你传输回来。网络传输的实现方式有很多种比如最近基本的 Socket或者性能以及封装更加优秀的 Netty(推荐)。
- 服务端 Stub(桩) :这个桩就不是代理类了。我觉得理解为桩实际不太好,大家注意一下就好。这里的服务端 Stub 实际指的就是接收到客户端执行方法的请求后,去指定对应的方法然后返回结果给客户端的类。
- 服务端(服务提供端) :提供远程方法的一端。
- 服务消费端(client)以本地调用的方式调用远程服务;
- 客户端 Stub(client stub) 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体(序列化):
RpcRequest
; - 客户端 Stub(client stub) 找到远程服务的地址,并将消息发送到服务提供端;
- 服务端 Stub(桩)收到消息将消息反序列化为Java对象:
RpcRequest
; - 服务端 Stub(桩)根据
RpcRequest
中的类、方法、方法参数等信息调用本地的方法; - 服务端 Stub(桩)得到方法执行结果并将组装成能够进行网络传输的消息体:
RpcResponse
(序列化)发送至消费方; - 客户端 Stub(client stub)接收到消息并将消息反序列化为Java对象:
RpcResponse
,这样也就得到了最终结果。
3.有哪些常见的 RPC 框架?
我们这里说的 RPC 框架指的是可以让客户端直接调用服务端方法,就像调用本地方法一样简单的框架,比如 Dubbo、Motan、gRPC这些。 如果需要和 HTTP 协议打交道,解析和封装 HTTP 请求和响应。这类框架并不能算是“RPC 框架”,比如Feign。
4.什么是 Dubbo?
Apache Dubbo是一款高性能、轻量级的开源 Java RPC 框架。
根据 [Dubbo 官方文档](https://dubbo.apache.org/zh/)的介绍,Dubbo 提供了六大核心能力
- 面向接口代理的高性能RPC调用。
- 智能容错和负载均衡。
- 服务自动注册和发现。
- 高度可扩展能力。
- 运行期流量调度。
- 可视化的服务治理与运维。
简单来说就是: Dubbo 不光可以帮助我们调用远程服务,还提供了一些其他开箱即用的功能比如智能负载均衡。
5.为什么要用 Dubbo?
- 负载均衡 : 同一个服务部署在不同的机器时该调用哪一台机器上的服务。
- 服务调用链路生成 : 随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如何调用的。
- 服务访问压力以及时长统计、资源调度和治理 :基于访问压力实时管理集群容量,提高集群利用率。
6.什么是分布式?
分布式或者说 SOA 分布式重要的就是面向服务,就是我们把整个系统拆分成不同的服务然后将这些服务放在不同的服务器上减轻单体服务的压力提高并发量和性能。
7.Dubbo 架构中的核心角色有哪些?
上述节点简单介绍以及他们之间的关系:
- Container: 服务运行容器,负责加载、运行服务提供者。必须。
- Provider: 暴露服务的服务提供方,会向注册中心注册自己提供的服务。必须。
- Consumer: 调用远程服务的服务消费方,会向注册中心订阅自己所需的服务。必须。
- Registry: 服务注册与发现的注册中心。注册中心会返回服务提供者地址列表给消费者。非必须。
- Monitor: 统计服务的调用次数和调用时间的监控中心。服务消费者和提供者会定时发送统计数据到监控中心。 非必须。
8.Dubbo 中的 Invoker 概念了解么?
`Invoker` 是 Dubbo 领域模型中非常重要的一个概念。简单来说,`Invoker` 就是 Dubbo 对远程调用的抽象。
按照 Dubbo 官方的话来说,Invoker
分为
- 服务提供
Invoker
- 服务消费
Invoker
假如我们需要调用一个远程方法,我们需要动态代理来屏蔽远程调用的细节吧!我们屏蔽掉的这些细节就依赖对应的 Invoker
实现, Invoker
实现了真正的远程服务调用。
9.Dubbo 的工作原理了解么?
下图是 Dubbo 的整体设计,从下至上分为十层,各层均为单向依赖。
左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。
- config 配置层:Dubbo相关的配置。支持代码配置,同时也支持基于 Spring 来做配置,以
ServiceConfig
,ReferenceConfig
为中心。 - proxy 服务代理层:调用远程方法像调用本地的方法一样简单的一个关键,真实调用过程依赖代理类,以
ServiceProxy
为中心。 - registry 注册中心层:封装服务地址的注册与发现。
- cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以
Invoker
为中心。 - monitor 监控层:RPC 调用次数和调用时间监控,以
Statistics
为中心。 - protocol 远程调用层:封装 RPC 调用,以
Invocation
,Result
为中心。 - exchange 信息交换层:封装请求响应模式,同步转异步,以
Request
,Response
为中心。 - transport 网络传输层:抽象 mina 和 netty 为统一接口,以
Message
为中心。 - serialize 数据序列化层 :对需要在网络传输的数据进行序列化。
10.Dubbo 的 SPI 机制了解么? 如何扩展 Dubbo 中的默认实现?
SPI(Service Provider Interface) 机制被大量用在开源项目中,它可以帮助我们动态寻找服务/功能(比如负载均衡策略)的实现。
SPI 的具体原理是这样的:我们将接口的实现类放在配置文件中,我们在程序运行过程中读取配置文件,通过反射加载实现类。这样,我们可以在运行的时候,动态替换接口的实现类。
Java 本身就提供了 SPI 机制的实现。不过,Dubbo 没有直接用,而是对 Java原生的 SPI机制进行了增强,以便更好满足自己的需求。
那我们如何扩展 Dubbo 中的默认实现呢?
比如说我们想要实现自己的负载均衡策略,我们创建对应的实现类 XxxLoadBalance
实现 LoadBalance
接口或者 AbstractLoadBalance
类。
package com.xxx;
import org.apache.dubbo.rpc.cluster.LoadBalance;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.RpcException;
public class XxxLoadBalance implements LoadBalance {
public <T> Invoker<T> select(List<Invoker<T>> invokers, Invocation invocation) throws RpcException {
// ...
}
}
我们将这个实现类的路径写入到resources
目录下的 META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance
文件中即可。
src
|-main
|-java
|-com
|-xxx
|-XxxLoadBalance.java (实现LoadBalance接口)
|-resources
|-META-INF
|-dubbo
|-org.apache.dubbo.rpc.cluster.LoadBalance (纯文本文件,内容为:xxx=com.xxx.XxxLoadBalance)
org.apache.dubbo.rpc.cluster.LoadBalance
xxx=com.xxx.XxxLoadBalance
其他还有很多可供扩展的选择,你可以在官方文档@SPI扩展实现这里找到
11.Dubbo 的微内核架构了解吗?
Dubbo 采用 微内核(Microkernel) + 插件(Plugin) 模式,简单来说就是微内核架构。微内核只负责组装插件。
微内核架构包含两类组件:核心系统(core system) 和 插件模块(plug-in modules)。
核心系统提供系统所需核心能力,插件模块可以扩展系统的功能。因此, 基于微内核架构的系统,非常易于扩展功能。
正是因为Dubbo基于微内核架构,才使得我们可以随心所欲替换Dubbo的功能点。比如你觉得Dubbo 的序列化模块实现的不满足自己要求,没关系啊!你自己实现一个序列化模块就好了啊!
通常情况下,微核心都会采用 Factory、IoC、OSGi 等方式管理插件生命周期。Dubbo 不想依赖 Spring 等 IoC 容器,也不想自己造一个小的 IoC 容器(过度设计),因此采用了一种最简单的 Factory 方式管理插件 :JDK 标准的 SPI 扩展机制 (java.util.ServiceLoader
)。
12.Dubbo 提供的负载均衡策略有哪些?
在集群负载均衡时,Dubbo 提供了多种均衡策略,默认为 random
随机调用。我们还可以自行扩展负载均衡策略(参考Dubbo SPI机制)。
在 Dubbo 中,所有负载均衡实现类均继承自 AbstractLoadBalance
,该类实现了 LoadBalance
接口,并封装了一些公共的逻辑。
1.RandomLoadBalance
根据权重随机选择(对加权随机算法的实现)。这是Dubbo默认采用的一种负载均衡策略。
RandomLoadBalance
具体的实现原理非常简单,假如有两个提供相同服务的服务器 S1,S2,S1的权重为7,S2的权重为3。
我们把这些权重值分布在坐标区间会得到:S1->[0, 7) ,S2->[7, 10)。我们生成[0, 10) 之间的随机数,随机数落到对应的区间,我们就选择对应的服务器来处理请求。
2.LeastActiveLoadBalance
LeastActiveLoadBalance
直译过来就是最小活跃数负载均衡。
初始状态下所有服务提供者的活跃数均为 0,每收到一个请求后,对应的服务提供者的活跃数 +1,当这个请求处理完之后,活跃数 -1。
因此,Dubbo 就认为谁的活跃数越少,谁的处理速度就越快,性能也越好,这样的话,我就优先把请求给活跃数少的服务提供者处理。
如果有多个服务提供者的活跃数相等怎么办?
很简单,那就再走一遍 RandomLoadBalance
。
3.ConsistentHashLoadBalance
ConsistentHashLoadBalance
即一致性Hash负载均衡策略。 ConsistentHashLoadBalance
中没有权重的概念,具体是哪个服务提供者处理请求是由你的请求的参数决定的,也就是说相同参数的请求总是发到同一个服务提供者。
另外,Dubbo 为了避免数据倾斜问题(节点不够分散,大量请求落到同一节点),还引入了虚拟节点的概念。通过虚拟节点可以让节点更加分散,有效均衡各个节点的请求量。
4.RoundRobinLoadBalance
加权轮询负载均衡。
轮询就是把请求依次分配给每个服务提供者。加权轮询就是在轮询的基础上,让更多的请求落到权重更大的服务提供者上。比如假如有两个提供相同服务的服务器 S1,S2,S1的权重为7,S2的权重为3。如果我们有 10 次请求,那么 7 次会被 S1处理,3次被 S2处理。
13.说一下Dubbo服务注册流程?
服务容器负责启动,加载,运行服务提供者。
服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失 败,再选另一台调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中 心。
14.怎么实现动态感知服务下线的呢?
服务订阅通常有 pull 和 push 两种方式:
pull 模式需要客户端定时向注册中心拉取配置;
push 模式采用注册中心主动推送数据给客户端。
Dubbo ZooKeeper 注册中心采用是事件通知与客户端拉取方式。服务第一次订阅的时候将会拉取 对应目录下全量数据,然后在订阅的节点注册一个 watcher。一旦目录节点下发生任何数据变化, ZooKeeper 将会通过 watcher 通知客户端。客户端接到通知,将会重新拉取该目录下全量数据, 并重新注册 watcher。利用这个模式,Dubbo 服务就可以做到服务的动态发现。
注意:ZooKeeper 提供了“心跳检测”功能,它会定时向各个服务提供者发送一个请求(实际上建立 的是一个 socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,并将其剔除。
15.Dubbo 容错策略?
failover cluster 模式
provider 宕机重试以后,请求会分到其他的 provider 上,默认两次,可以手动设置重试次数,建议把写操作重试次数设置成 0。
failback 模式
失败自动恢复会在调用失败后,返回一个空结果给服务消费者。并通过定时任务对失败的调用进行 重试,适合执行消息通知等操作。
failfast cluster 模式
快速失败只会进行一次调用,失败后立即抛出异常。适用于幂等操作、写操作,类似于 failover cluster 模式中重试次数设置为 0 的情况。
failsafe cluster 模式
失败安全是指,当调用过程中出现异常时,仅会打印异常,而不会抛出异常。适用于写入审计日志 等操作。
forking cluster 模式
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多 服务资源。可通过 forks="2" 来设置最大并行数。
broadcacst cluster 模式
广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志 等本地资源信息。
16.Dubbo 框架源码最重要的设计原则是什么?
Dubbo 在设计时具有两大很大的设计原则:
“微内核+插件”的设计模式。内核只负责组装插件(扩展点),Dubbo 的功能都是由插件实现的,也就是 Dubbo 的所有功能点都可被用户自定义扩展类所替换。Dubbo 的高扩展性、开放性在这里被充分体现。
采用 URL 作为配置信息的统一格式,所有扩展点都通过传递 URL 携带配置信息。简单来说就是,在 Dubbo 中,所有重要资源都是以 URL 的形式来描述的。
17.Dubbo 官方给出了四大组件的概念,请谈一下你对它们的认识。
Dubbo 的四大组件为:Consuer、Provider、Registry 与 Monitor。
它们间的关系可以描 述为如下几个过程:
start:Dubbo 服务启动,Spring 容器首先会创建服务提供者。
register:服务提供者创建好后,马上会注册到服务注册中心 Registry,这个注册过程称为服务暴露。服务暴露的本质是将服务名称(接口)与服务提供者主机写入到注册中心 Registry 的服务映射表中。
subscribe:服务消费者启动后,首先会向服务注册中心订阅相关服务。
notify:消费者可能订阅的服务在注册中心还没有相应的提供者。当相应的提供者在注册中心注册后,注册中心会马上通知订阅该服务的消费者。
invoke:消费者以同步或异步的方式调用提供者提供的请求。消费者通过远程注册中心获取到提供者列表,然后消费者会基于负载均衡算法选一台提供者处理消费者的请求。
count:每个消费者对各个服务的累计调用次数、调用时间;每个提供者被消费者调用的累计次数和时间,消费者与调用者都会定时发送到监控中心,由监控中心记录。这些统计数据可以在 Dubbo 的可视化界面看到。
18.什么是 SPI?请简单描述一下 SPI 要解决的问题。
SPI,Service Provider Interface,服务提供者接口,是一种服务发现机制。其主要是解决面向抽象编程中上层对下层接口实现类的依赖问题,可以实现这两层间的解耦合。
19.JDK 的 SPI 机制存在什么问题?
JDK 的 SPI 机制将所有配置文件中的实现类全部实例化,无论是否能够用到,浪费了 宝贵的系统资源。
20.Dubbo 框架的 Adaptive 类都有哪些?Adaptive 类与 Adaptive 方法的区别是什么? 或者说,各自的应用场景有什么不同?
Dubbo 中的 Adaptive 类共有两个:AdaptiveExtensionFactory 与 AdaptiveCompiler。
Adaptive 类主要是用于限定其 SPI 扩展类的获取方式:必须按照该类中指定的方式获取。 Adaptive 类允许程序员在其中自行定义扩展实例的获取逻辑。 在获取 SPI 扩展实例时若采用自适应方式获取,系统会首先查找其 Adaptive 类,若没有找到, 则会查看该 SPI 接口中的 Adaptive 方法,然后根据 Adaptive 方法自动为该 SPI 接口动态生成 一个 Adaptive 扩展类,并自动将其编译。由于 Adaptive 方法生成的 Adaptive 类的逻辑是固定的,所以无法实现程序员自己想要的获取逻辑,但非常方便。若没有特殊需求,Adaptive 方法使用更方便。
21.简述 Dubbo 的 Wrapper 机制?
Wrapper 机制,即扩展类的包装机制。就是对扩展类中的 SPI 接口方法进行增强,进行包装,是AOP 思想的体现,是 Wrapper 设计模式的应用。一个 SPI 可以包含多个 Wrapper, 即可以通过多个 Wrapper 对同一个扩展类进行增强,增强出不现的功能。Wrapper 机制不是 通过注解实现的,而是通过一套 Wrapper 规范实现的。
22.Dubbo 的 Wrapper 类是否属于扩展类?
wrapper 类仅仅是对现有的扩展类功能上的增强,并不是一个独立的扩展类,所以其不属于扩展类范畴。
23.简述 Dubbo 的 Active 机制?
Activate 机制,即扩展类的激活机制。通过指定的条件来实现一次激活多个扩展类的目的。激活机制没有增强扩展类,也没有增加扩展类,其仅仅是为原有的扩展类添加了更多 的识别标签,而不像之前的,每个扩展类仅有一个“功能性扩展名”识别标签。其是通过 @Active 注解实现的。
24.Dubbo 的 Activate 类是否属于扩展类?
Activate 机制仅用于为扩展类添加激活标识的,其是通过在扩展类上添加 @Activate 注解来实现的,所以 Activate 类本身就是扩展类。
25.请对 Dubbo 的普通扩展类、Adaptive 类、Wrapper 类,及 Activate 类的实现方式、 个数,及是否属于扩展类等进行一个总结。
在 Dubbo 的扩展类配置文件中可能会存在四种类:普通扩展类,Adaptive 类,Wrapper 类,及 Activate 类。
它们的共同点是,都实现了 SPI 接口。 Adaptive 类与 Activate 类都是通过注解定义的。 一个 SPI 接口的 Adaptive 类(无论是否是自动生成的)只会有一个,而 Wrapper 类与Activate 类可以有多个。 只有普通扩展类与 Activate 属于扩展类,Adaptive 类与 Wrapper 类均不属于扩展类范畴。因为它们都是依附于扩展类的,无法独立使用。
26.简述 Dubbo 的 ExtensionLoader 实例的组成。
ExtensionLoader 实例用于加载并创建指定类型的扩展类实例。所以这个 loader 实例 由两个成员变量组成。一个是 Class 类型的 type,用于标识这个 loader 可以加载的 SPI 类型; 一个是 ExtensionFactory,用于创建这个指定 SPI 类型的扩展类实例。
27.Dubbo 在查找指定扩展类时,其会查找哪些目录中的扩展类配置文件?对于这些目录中的配置文件,其是查找了所有这些目录,在一个目录中找到了就不再找其它目录了? 其是仅加载了这一个扩展类还是加载了全部该 SPI 的所有扩展类?
Dubbo 在查找指定扩展类时,其会依次查找三个目录:META-INF/dubbo/internal 目录; META-INF/dubbo 目录;META-INF/services 目录。 其会将这三个目录中所有的该类型的 SPI 扩展类全部加载到内存,但仅会创建并初始化 指定扩展名的实例。
28.Dubbo 源码中是如何判断一个类是否是 Wrapper 类的?
Dubbo 源码中对于 Wrapper 类的判断仅是判断其是否包含一个这样的构造器:只包 含一个参数,且这个参数是 SPI 接口类型。即 Wrapper 实例中用于增强的 SPI 扩展类实例, 是通过带参构造器传入的。
29.从 Dubbo 源码中可以看出,一个 SPI 接口的实现类有什么要求?
从 Dubbo 源码中可以看出,一个 SPI 接口的实现类除了其要实现 SPI 接口外,还必须 具有无参构造器。
30.ExtensionLoader 实例中包含一个 ExtensionFactory 实例 objectFactory,该实例用于 创建指定扩展名的扩展类实例,简述 objectFactory 创建扩展类实例的过程。
ExtensionFactory 创建实例的方式有两种:SPI 与 Spring 容器。
objectFactory 通过调 用 getExtension(type, name)方法来获取指定类型与名称的扩展类实例。getExtension()方法首 先会尝试通过 SPI 方式来获取;若没有找到,则再从 Spring 容器中去尝试获取指定名称的实 例;若没有找到,则再从 Spring 容器中去尝试获取指定类型的实例。若还没有,则抛出异常。
31.请简述一个指定功能性扩展名的扩展类实例的创建、setter 及 wrapper 的顺序与过 程?
一个扩展类实例的创建与初始化过程是:在获取该 SPI 接口的 loader 时会首先将当 前 SPI 接口的所有扩展类(四类)全部加载并缓存。然后通过 getExtension()方法获取该实例 时,其会从缓存中获取到该扩展名对应的扩展类,然后调用其无参构造器创建实例。然后调用该实例的 setter 进行注入初始化。若该 SPI 还存在 Wrapper,则会按照这些 Wrapper 的注册顺序逐层将这个实例进行包装。当然,在调用执行时,其一定是从最外层的 Wrapper 开 始逐层向内执行,直至执行到该扩展类实例的方法。