【51CTO.com原创稿件】分布式应用场景有高并发,内容高可扩展和高性能的眼面要求。还涉及到,知道序列化/反序列化,内容网络,眼面多线程以及设计模式的问题。幸好 Dubbo 框架将上述知识进行了封装,让程序员能够把注意力放到业务上。
[[284227]]
图片来自 Pexels
为了更好地了解和使用 Dubbo,今天来介绍一下 Dubbo 的主要组件和实现原理。
Dubbo 分层
Dubbo 是一款高性能 Java RPC 架构。它实现了面向接口代理的 RPC 调用,服务注册和发现,负载均衡,容错,扩展性等等功能。
Dubbo 大致上分为三层,分别是:
Dubbo 的三层结构
从上图中可以看到,三层结构中包含了 Dubbo 的核心组件。他们的基本功能如下,对于比较常用的组件,会在后面的篇幅中详细讲解。
组件功能列表
这里将这些组件罗列出来,能有一个感性的认识。具体开发的时候,知道运用哪些组件。
Dubbo 调用工作流
Dubbo 框架是用来处理分布式系统中,服务发现与注册以及调用问题的,并且管理调用过程。
上面介绍了 Dubbo 的框架分层,下图的工作流就展示了他们是如何工作的。
Dubbo 服务调用流程图
工作流涉及到服务提供者(Provider),注册中心(Registration),网络(Network)和服务消费者(Consumer):
上面调用步骤经历了这么多过程,其中出现了 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
getProxy 代码片段 2
从上面代码片段可以看出,消费者服务在调用服务提供者时,做了以下动作:
注册中心
说完服务暴露,再回头来看看注册中心。Dubbo 通过注册中心实现了分布式环境中服务的注册和发现。
配置中心
其主要作用如下:
配置中心工作流
注册调用流程图
先看看注册中心调用的流程图:
注册中心工作原理
Dubbo 有四种注册中心的实现,分别是 ZooKeeper,Redis,Simple 和 Multicast。
这里着重介绍一下 ZooKeeper 的实现。ZooKeeper 是负责协调服务式应用的。
它通过树形文件存储的 ZNode 在 /dubbo/Service 目录下面建立了四个目录,分别是:
客户端第一次连接注册中心的时候,会获取全量的服务元数据,包括服务提供者和服务消费者以及路由和配置的信息。
根据 ZooKeeper 客户端的特性,会在对应 ZNode 的目录上注册一个 Watcher,同时让客户端和注册中心保持 TCP 长连接。
如果服务的元数据信息发生变化,客户端会接受到变更通知,然后去注册中心更新元数据信息。变更时根据 ZNode 节点中版本变化进行。
Dubbo 集群容错
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 信息。
Notify 监听三类信息
通过前面三步生成的 Invoker 需要调用最终的服务,但是服务有可能分布在不同的节点上面。所以,需要经过 LoadBalance。
Dubbo 的负载均衡策略有四种:
最后进行 RPC 调用。如果调用出现异常,针对不同的异常提供不同的容错策略。Cluster 接口定义了 9 种容错策略,这些策略对用户是完全透明的。
用户可以在,,, 标签上通过 Cluster 属性设置:
Dubbo 远程调用
服务消费者经过容错,Invoker 列表,路由和负载均衡以后,会对 Invoker 进行过滤,之后通过 Client 编码,序列化发给服务提供者。
过滤,发送请求,编码,序列化发送给服务提供者
从上图可以看出在服务消费者调用服务提供者的前后,都会调用 Filter(过滤器)。
可以针对消费者和提供者配置对应的过滤器,由于过滤器在 RPC 执行过程中都会被调用,所以为了提高性能需要根据具体情况配置。
Dubbo 系统有自带的系统过滤器,服务提供者有 11 个,服务消费者有 5 个。过滤器的使用可以通过 @Activate 的注释,或者配置文件实现。
配置文件实现过滤器
过滤器的使用遵循以下几个规则:
过滤器叠加,如果服务提供者和服务消费者都配置了过滤器,那么两个过滤器会被叠加生效。
由于,每个服务都支持多个过滤器,而且过滤器之间有先后顺序。因此在设计上 Dubbo 采用了装饰器模式,将 Invoker 进行层层包装,每包装一层就加入一层过滤条件。在执行过滤器的时候就好像拆开一个一个包装一样。
调用请求经过过滤以后,会以 Invoker 的形式对 Client 进行调用。Client 会交由底层 I/O 线程池处理,其包括处理消息读写,序列化,反序列化等逻辑。
同时会对 Dubbo 协议进行编码和解码操作。Dubbo 协议基于 TCP/IP 协议,包括协议头和协议体。
协议体包含了传输的主要内容,其意义不言而喻,它是由 16 字节长的报文组成,每个字节包括 8 个二进制位。
内容如下:
服务消费者在调用之前会将上述服务消息体,根据 Dubbo 协议打包好。框架内部会调用 DefaultFuture 对象的 get 方法进行等待。
在准备发送请求的时候,才创建 Request 对象,这个对象会保存在一个静态的 HashMap 中,当服务提供者处理完 Request 之后,将返回的 Response 放回到 Futures 的 HashMap 中。
在 HashMap 中会找到对应的 Request 对象,并且返回给服务消费者。
服务消费者请求和响应图
协议打包好以后就需要给协议编码和序列化。这里需要用到 Dubbo 的编码器,其过程是将信息传化成字节流。
Dubbo 协议编码请求对象分为使用 ExchangeCodec 中的两个方法,encodeRequest 负责编码协议头和 encodeRequestData 编码协议体。
同样通过 encodeResponse 编码协议头,encodeResponseData 编码协议体。
服务消费者和提供者都通过 decode 和 decodeBody 两个方法进行解码,不同的是解码有可能在 IO 线程或者 Dubbo 线程池中完成。
虽然,编码和解码的细节在这里不做展开,但是以下几点需要注意:
当服务提供者收到请求协议包以后,先将其放到 ThreadPool 中,然后依次处理。
由于服务提供者在注册中心是通过 Exporter 的方式暴露服务的,服务消费者也是通过 Exporter 作为接口进行调用的。
Exporter 是将 Invoker 进行了包装,将拆开的 Invoker 进行 Filter 过滤链条进行过滤以后,再去调用服务实体。最后,将信息返回给服务消费者。
总结
我们首先了解 Dubbo 的分层和几个核心模块,分别介绍他们的职责。然后通过一个简单的例子,服务消费者调用服务提供者,用 Dubbo 的工作流程将各个模块串起来。
在这 22 步的流程中,以服务提供者和服务消费者的初始化为起点,用到了 Config 和 Proxy 以及 Protocol,Invoker。
注册中心作为两者的连接桥梁,起到了服务发现和注册的作用,并且着重讲了如何通过 ZooKeeper 实现注册中心的原理。
在服务消费者调用提供者之前,需要通过 Cluster 容错机制,Directory 获取 Invoker 列表,Router 找到路由信息,再使用 LoadBalance 知道具体服务。
在调用服务提供者之间还不忘通过 Filter 进行过滤,通过装饰者模式实现的 Filter 可以形成过滤链条,依次对条件进行过滤。
对于远程调用,需要调用打包协议,针对 Dubbo 协议进行了描述,并且针对该协议进行了编码/解码和序列化/反序列化的操作。
服务提供者收到请求以后,会将请求放到 ThreadPool 中逐一处理。通过 Exporter,Invoker,Filter 的逐级转换,最后响应请求。
由于篇幅有限很多功能例如 SPI,Merger 等没有介绍到,有时间再和大家细聊。
作者:崔皓
简介:十六年开发和架构经验,曾担任过惠普武汉交付中心技术专家,需求分析师,项目经理,后在创业公司担任技术/产品经理。善于学习,乐于分享。目前专注于技术架构与研发管理。
【51CTO原创稿件,合作站点转载请注明原文作者和出处为51CTO.com】
责任编辑:武晓燕 来源: 51CTO专栏 Dubbo高并发分布式
(责任编辑:综合)
小米12/Pro/Ultra外形齐曝光 高管回应:米粉放心了
568万元!四川省攀枝花市获省建筑领域绿色低碳循环发展专项资金支持