扫描下方海报
试读
“
分布式应用场景有高并发,高可扩展和高性能的要求。还涉及到,序列化/反序列化,网络,多线程以及设计模式的问题。幸好 Dubbo 框架将上述知识进行了封装,让程序员能够把注意力放到业务上。
为了更好地了解和使用 Dubbo,今天来介绍一下 Dubbo 的主要组件和实现原理。
Dubbo 是一款高性能 Java RPC 架构。它实现了面向接口代理的 RPC 调用,服务注册和发现,负载均衡,容错,扩展性等等功能。
Dubbo 大致上分为三层,分别是:
Dubbo 的三层结构
从上图中可以看到,三层结构中包含了 Dubbo 的核心组件。他们的基本功能如下,对于比较常用的组件,会在后面的篇幅中详细讲解。
组件功能列表
这里将这些组件罗列出来,能有一个感性的认识。具体开发的时候,知道运用哪些组件。
Dubbo 框架是用来处理分布式系统中,服务发现与注册以及调用问题的,并且管理调用过程。
上面介绍了 Dubbo 的框架分层,下图的工作流就展示了他们是如何工作的。
Dubbo 服务调用流程图
工作流涉及到服务提供者(Provider),注册中心(Registration),网络(Network)和服务消费者(Consumer):
-
服务提供者在启动的时候,会通过读取一些配置将服务实例化。
-
Proxy 封装服务调用接口,方便调用者调用。客户端获取 Proxy 时,可以像调用本地服务一样,调用远程服务。
-
Proxy 在封装时,需要调用 Protocol 定义协议格式,例如:Dubbo Protocol。
-
将 Proxy 封装成 Invoker,它是真实服务调用的实例。
-
将 Invoker 转化成 Exporter,Exporter 只是把 Invoker 包装了一层,是为了在注册中心中暴露自己,方便消费者使用。
-
将包装好的 Exporter 注册到注册中心。
-
服务消费者建立好实例,会到服务注册中心订阅服务提供者的元数据。元数据包括服务 IP 和端口以及调用方式(Proxy)。
-
消费者会通过获取的 Proxy 进行调用。通过服务提供方包装过程可以知道,Proxy 实际包装了 Invoker 实体,因此需要使用 Invoker 进行调用。
-
在 Invoker 调用之前,通过 Directory 获取服务提供者的 Invoker 列表。在分布式的服务中有可能出现同一个服务,分布在不同的节点上。
-
通过路由规则了解,服务需要从哪些节点获取。
-
Invoker 调用过程中,通过 Cluster 进行容错,如果遇到失败策略进行重试。
-
调用中,由于多个服务可能会分布到不同的节点,就要通过 LoadBalance 来实现负载均衡。
-
Invoker 调用之前还需要经过 Filter,它是一个过滤链,用来处理上下文,限流和计数的工作。
-
生成过滤以后的 Invoker。
-
用 Client 进行数据传输。
-
Codec 会根据 Protocol 定义的协议,进行协议的构造。
-
构造完成的数据,通过序列化 Serialization 传输给服务提供者。
-
Request 已经到达了服务提供者,它会被分配到线程池(ThreadPool)中进行处理。
-
Server 拿到请求以后查找对应的 Exporter(包含有 Invoker)。
-
由于 Export 也会被 Filter 层层包裹
-
通过 Filter 以后获得 Invoker
-
最后,对服务提供者实体进行调用。
上面调用步骤经历了这么多过程,其中出现了 Proxy,Invoker,Exporter,Filter。
实际上都是调用实体在不同阶段的不同表现形式,
本质是一样的,在不同的使用场景使用不同的实体。
例如 Proxy 是用来方便调用者调用的。Invoker 是在调用具体实体时使用的。Exporter 用来注册到注册中心的等等。
后面我们会对具体流程进行解析。如果时间不够无法阅读完全文,可以把上面的图保存。
上面讲到的服务调用流程中,开始服务提供者会进行初始化,将暴露给其他服务调用。服务消费者也需要初始化,并且在注册中心注册自己。
首先来看看服务提供者暴露服务的整体机制:
开篇的大图中列举了 Config 核心组件,在服务提供者初始化的时候,会通过 Config 组件中的 ServiceConfig 读取服务的配置信息。
这个配置信息有三种形式,分别是 XML 文件,注解(Annoation)和属性文件(Properties 和 yaml)。
在读取配置文件生成服务实体以后,会通过 ProxyFactory 将 Proxy 转换成 Invoker。
此时,Invoker 会被定义 Protocol,之后会被包装成 Exporter。最后,Exporter 会发送到注册中心,作为服务的注册信息。
上述流程主要通过 ServiceConfig 中的 doExport 完成。
下面是针对多协议多注册中心进行源代码分析:
doExportUrls 方法
doExportUrlsFor1Protocol 方法-1
doExportUrlsFor1Protocol 方法-2
上面截取了服务提供者暴露服务的代码片段,从注释上看整个暴露过程分为七个步骤:
一旦服务注册到注册中心以后,注册中心会通过 RegistryProtocol 中的 Export 方法将服务暴露出去,并依次做以下操作:
说完了服务提供者的暴露再来看看服务消费者。
服务消费者首先持有远程服务实例生成的 Invoker,然后把 Invoker 转换成用户接口的动态代理引用。
框架进行服务引用的入口点在 ReferenceBean 中的 getObject 方法,会将实体转换成 ReferenceBean,它是集成与 ReferenceConfig 类的。
这里一起来看看 createProxy 的源代码:
getProxy 代码片段 1
从上面代码片段可以看出,消费者服务在调用服务提供者时,做了以下动作:
-
检查是否是同一个 JVM 内部引用。
-
如果是同一个 JVM 的引用,直接使用 injvm 协议从内存中获取实例。
-
注册中心地址后,添加 refer 存储服务消费元数据信息。
-
单注册中心消费。
-
依次获取注册中心的服务,并且添加到 Invokers 列表中。
-
通过 Cluster 将多个 Invoker 转换成一个 Invoker。
-
把 Invoker 转换成接口代理。
说完服务暴露,再回头来看看注册中心。Dubbo 通过注册中心实现了分布式环境中服务的注册和发现。
-
动态载入服务。服务提供者通过注册中心,把自己暴露给消费者,无须消费者逐个更新配置文件。
-
动态发现服务。消费者动态感知新的配置,路由规则和新的服务提供者。
-
参数动态调整。支持参数的动态调整,新参数自动更新到所有服务节点。
-
服务统一配置。统一连接到注册中心的服务配置。
-
提供者(Provider)启动时,会向注册中心写入自己的元数据信息(调用方式)。
-
消费者(Consumer)启动时,也会在注册中心写入自己的元数据信息,并且订阅服务提供者,路由和配置元数据的信息。
-
服务治理中心(duubo-admin)启动时,会同时订阅所有消费者,提供者,路由和配置元数据的信息。
-
当提供者离开或者新提供者加入时,注册中心发现变化会通知消费者和服务治理中心。
Dubbo 有四种注册中心的实现,分别是 ZooKeeper,Redis,Simple 和 Multicast。
这里着重介绍一下 ZooKeeper 的实现。
ZooKeeper 是负责协调服务式应用的。
它通过树形文件存储的 ZNode 在 /dubbo/Service 目录下面建立了四个目录,分别是:
-
Providers 目录下面,
存放服务提供者 URL 和元数据。
-
Consumers 目录下面,
存放消费者的 URL 和元数据。
-
Routers 目录下面,
存放消费者的路由策略。
-
Configurators 目录下面,
存放多个用于服务提供者动态配置 URL 元数据信息。
客户端第一次连接注册中心的时候,会获取全量的服务元数据,包括服务提供者和服务消费者以及路由和配置的信息。
根据 ZooKeeper 客户端的特性,会在对应 ZNode 的目录上注册一个 Watcher,同时让客户端和注册中心保持 TCP 长连接。
如果服务的元数据信息发生变化,客户端会接受到变更通知,然后去注册中心更新元数据信息。变更时根据 ZNode 节点中版本变化进行。
Cluster,Directory,Router,LoadBalance 核心接口
分布式服务多以集群形式出现,Dubbo 也不例外。在消费服务发起调用的时候,会涉及到 Cluster,Directory,Router,LoadBalance 几个核心组件。
Cluster,Directory,Router,LoadBalance 调用流程
①生成
Invoker
对象。
根据
Cluster
实现的不同,生成不同类型的
ClusterInvoker
对象。
通过
ClusertInvoker
中的
Invoker
方法启动调用流程。
②获取可调用的服务列表,
可以通过
Directory
的
List
方法获取。
这里有两类服务列表的获取方式。
分别是
RegistryDirectory
和
StaticDirectory
:
在
Directory
获取所有
Invoker
列表之后,会调用路由接口(
Router
)。
其会根据用户配置的不同策略对
Invoker
列表进行过滤,只返回符合规则的
Invoker
。
假设用户配置接口
A
的调用,都使用了
IP
为
192.168.1.1
的节点,则
Router
会自动过滤掉其他的
Invoker
,只返回
192.168.1.1
的
Invoker
。
这里介绍一下
RegistryDirectory
的实现,它通过
Subscribe
和
Notify
方法,订阅和监听注册中心的元数据。
Subscribe
,订阅某个
URL
的更新信息。
Notify
,根据订阅的信息进行监听。
包括三类信息,配置
Configurators
,路由
Router
,以及
Invoker
列表。
管理员会通过
dubbo-admin
修改
Configurators
的内容,
Notify
监听到该信息,就更新本地服务的
Configurators
信息。
同理,路由信息更新了,也会更新服务本地路由信息。如果
Invoker
的调用信息变更了(服务提供者调用信息),会根据具体情况更新本地的
Invoker
信息。
通过前面三步生成的 Invoker 需要调用最终的服务,但是服务有可能分布在不同的节点上面。所以,需要经过 LoadBalance。
-
Random LoadBalance,
随机,按照权重设置随机概率做负载均衡。
-
RoundRobinLoadBalance,
轮询,按照公约后的权重设置轮询比例。
-
LeastActiveLoadBalance,
按照活跃数调用,活跃度差的被调用的次数多。活跃度相同的 Invoker 进行随机调用。
-
ConsistentHashLoadBalance,
一致性 Hash,相同参数的请求总是发到同一个提供者。
最后进行 RPC 调用。如果调用出现异常,针对不同的异常提供不同的容错策略。Cluster 接口定义了 9 种容错策略,这些策略对用户是完全透明的。
用户可以在
,
,
,
标签上通过 Cluster 属性设置:
-
Failover,
出现失败,立即重试其他服务器。可以设置重试次数。
-
Failfast,
请求失败以后,返回异常结果,不进行重试。
-
Failsafe,
出现异常,直接忽略。
-
Failback,
请求失败后,将失败记录放到失败队列中,通过定时线程扫描该队列,并定时重试。
-
Forking,
尝试调用多个相同的服务,其中任意一个服务返回,就立即返回结果。
-
Broadcast,
广播调用所有可以连接的服务,任意一个服务返回错误,就任务调用失败。
-
Mock,
响应失败时返回伪造的响应结果。
-
Available,
通过遍历的方式查找所有服务列表,找到第一个可以返回结果的节点,并且返回结果。
-
Mergable,
将多个节点请求合并进行返回。
服务消费者经过容错,Invoker 列表,路由和负载均衡以后,会对 Invoker 进行过滤,之后通过 Client 编码,序列化发给服务提供者。
从上图可以看出在服务消费者调用服务提供者的前后,都会调用 Filter(过滤器)。
可以针对消费者和提供者配置对应的过滤器,由于过滤器在 RPC 执行过程中都会被调用,所以为了提高性能需要根据具体情况配置。
Dubbo 系统有自带的系统过滤器,服务提供者有 11 个,服务消费者有 5 个。过滤器的使用可以通过 @Activate 的注释,或者配置文件实现。