看大牛是如何一次性把RPC远程过程调用,Dubbo架构进阶给讲清的

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Dubbo架构进阶Dubbo架构主要包含四个角色:消费者、提供者、注册中心和监控系统。

Dubbo架构进阶


Dubbo架构主要包含四个角色:消费者、提供者、注册中心和监控系统,如下图所示。


image.png


具体的交互流程是:消费者(Consumer)通过注册中心获取提供者(Provider)节点后,通过Dubbo的客户端SDK与Provider建立连接,并发起调用。Provider通过Dubbo的服务端SDK接收Consumer的请求,处理后再把结果返回给Consumer。


对于采用Dubbo进行RPC调用的解决方案,消费者和提供者都需要引入Dubbo的SDK来完成远程调用。因为Dubbo本身是采用Java实现的,所以要求服务消费者和服务提供者也都必须采用Java实现。不过开源社区已经开始使用对核心扩展点进行TCK(Technology CompatibilityKit)提升框架的兼容性。它为用户增加一种扩展实现,只需通过TCK,即可确保与框架的其他部分兼容运行,可以有效提高健壮性,也方便第三方接入。下面是Dubbo的官方详细架构。


image.png


  • 左边部分是服务消费者使用的接口,右边部分是服务提供者使用的接口,位于中轴线上的为双方都用到的接口。
  • 从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中Service 和 Config 层 为 API , 其 他 各 层 均 为 SPI ( ServiceProvider Interface)。


  • 浅色小块为扩展接口,深色小块为实现类,图中只显示用于关联各层的实现类。
  • 深色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承(读者可到官网查看彩色图片),可以把子类看作父类的同一个节点,线上的文字为调用的方法。Dubbo服务调用过程Dubbo服务调用过程比较复杂,包含众多步骤,比如发送请求、编解码、服务降级、过滤器链处理、序列化、线程派发及响应请求等。


下面我们重点分析请求的发送与接收、编解码、线程派发及响应的发送与接收等过程。


Dubbo的服务调用过程


如下图所示。

image.png

首先服务消费者通过代理对象Proxy发起远程调用,接着通过网络客户端Client将编码后的请求发送给服务提供者的网络层,也就是Server。Server在收到请求后,首先要做的事情是对数据包进行解码。然后将解码后的请求发送至分发器Dispatcher,再由分发器将请求派发到指定的线程池上,最后由线程池调用具体的服务。这就是一个远程调用请求的发送与接收过程。


服务消费者发送请求


Dubbo支持同步和异步两种调用方式,其中异步调用还可细分为“有返回值”的异步调用和“无返回值”的异步调用。所谓“无返回值”的异步调用是指服务消费者只管调用,但不关心调用结果,此时Dubbo会直接返回一个空的RpcResult。若要使用异步特性,需要服务消费者手动进行配置。默认情况下,Dubbo使用同步调用方式。服务调用的线程栈快照如下图所示。


image.png


服务提供者接收请求


默认情况下,Dubbo使用Netty作为底层的通信框架。Netty首先会通过解码器对数据进行解码,并将解码后的数据传递给下一个处理器的指定方法。

解 码 器 将 数 据 包 解 析 成 Request 对 象 后 , NettyHandler 的messageReceived方法紧接着会收到这个对象,并将这个对象继续向下传 递 。 其 间 该 对 象 会 被 依 次 传 递 给 NettyServer 、MultiMessageHandler、HeartbeatHandler以及AllChannelHandler处理。最后由AllChannelHandler将该对象封装到Runnable实现类对象中,并将Runnable放入线程池中执行后续的调用逻辑,调用栈如下图所示。


image.png


Dispatcher就是线程派发器。需要说明的是,Dispatcher真实的职 责 是 创 建 具 有 线 程 派 发 能 力 的 ChannelHandler , 比 如AllChannelHandler、MessageOnlyChannelHandler和ExecutionChannelHandler等,其本身并不具备线程派发能力。Dubbo的5种不同的线程派发策略如下表所示。


image.png


默认配置下,Dubbo使用all派发策略,即将所有的消息都派发到线 程 池 。 请 求 对 象 会 被 封 装 在 ChannelEventRunnable 中 ,ChannelEventRunnable将会是服务调用过程的新起点。所以接下来我们看一下以ChannelEventRunnable为起点的服务提供者的线程调用栈,如下图所示。


image.png


向用户线程传递调用结果


响应数据解码完成后,Dubbo会将响应对象派发到线程池。要注意的是,线程池中的线程并非用户的调用线程,所以要想办法将响应对象从线程池传递到用户线程上。

用户线程在发送完请求后,调用DefaultFuture的get方法等待响应对象的到来。当响应对象到来后,用户线程会被唤醒,并通过调用编号获取属于自己的响应对象。


Dubbo设计原理


Dubbo在架构上通过SPI机制(SPI的全称为Service ProviderInterface,SPI机制是一种服务发现机制)的设计,使得整体架构具备了极高的可扩展性。


下面是Dubbo的核心设计原理:

  • 采 用 Microkernel+Plugin 模 式 , Microkernel 负 责 组 装Plugin,Dubbo自身的功能也是通过扩展点实现的,也就是Dubbo的所有功能点都可被用户自定义扩展所替换。
  • 采用URL作为配置信息的统一格式,所有扩展点都通过传递URL携带配置信息。


SPI机制的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类,这样它可以在运行时动态为接口替换实现类。正因为此特性,我们可以通过SPI机制为程序提供拓展功能,这样可以在运行时动态为接口替换实现类。Dubbo就是通过SPI机制加载所有组件的,不过Dubbo并未使用Java原生的SPI机制,而是对其进行了增强,使其能够更好地满足需求。


Dubbo SPI示例


首先,我们定义一个接口,名称为Hello:

image.png

image.png


Dubbo SPI的相关逻辑被封装在了ExtensionLoader类中,通过ExtensionLoader类我们可以加载指定的实现类。Dubbo SPI所需的配置文件需放置在META-INF/dubbo路径下,配置内容如下:

image.png


与Java SPI实现类配置不同,Dubbo SPI通过键值对的方式进行配置,我们可以按需加载指定的实现类。另外,在测试Dubbo SPI时,需要在Robot接口上标注@SPI注解。

image.png


上述代码的输出结果如下:

image.png


SPI机制


下面我们结合源码来理解Dubbo的SPI机制和整体架构特性,需要明确几个核心概念,如下图所示。


image.png


  • ExtensionLoader

ExtensionLoader作为整个SPI机制的核心起着无可替代的作用,扩展点并不会强制所有用户都使用Dubbo提供的某些架构。例如Dubbo提供了ZooKeeper注册中心,但是如果我们更倾向于其他的注册中心,我们可以替换掉Dubbo提供的注册中心。我们称这种可被替换的技术实现点为扩展点,类似的扩展点有很多,例如Protocol、Filter、Loadbalance等。


鉴 于 ExtensionLoader 的 用 法 比 较 多 , 下 面 我 们 以ExtensionLoader 类 作 为 入 口 进 行 讲 解 。 首 先 , 我 们 通 过ExtensionLoader的getExtensionLoader方法获取一个单例实例,然后通过ExtensionLoader的getExtension方法获取拓展类对象。其中,getExtensionLoader 方 法 用 于 从 缓 存 中 获 取 与 拓 展 类 对 应 的ExtensionLoader实例,若缓存未命中,则创建一个新的实例。下面我们以ExtensionLoader的getExtension方法作为入口,代码如下:


image.png

image.png


上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建实例化对象的代码实现:

image.png


createExtension模块中包含了如下步骤:

(1)通过getExtensionClasses获取所有的拓展类。

(2)通过反射创建拓展对象。

(3)向拓展对象中注入依赖。

(4)将拓展对象包裹在相应的Wrapper对象中。

我们在通过名称获取拓展类之前,需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称,拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码如下:

image.png


这里也是先检查缓存,若缓存未命中则通过synchronized加锁,加锁后再次检查缓存,并判空。此时如果classes仍空,则通过loadExtensionClasses加载拓展类。

下面分析loadExtensionClasses方法的逻辑:

image.png

image.png


loadExtensionClasses方法总共做了两件事情,一是对SPI注解进行解析,二是调用loadDirectory方法加载指定目录中的配置文件。

SPI注解解析过程比较简单,loadDirectory方法先通过类加载器获 取 所 有 资 源 链 接 , 然 后 通 过 loadResource 方 法 加 载 资 源 。

loadResource方法用于读取和解析配置文件,并通过反射加载类,最后调用loadClass方法进行其他操作。Dubbo会从以下三个路径读取并加载扩展点配置文件:

image.png


  • Wrapper

在实例化扩展点的代码中可以看到,在加载某个接口的扩展类时,如果某个实现中有一个拷贝类构造函数,那么该接口实现就是该接口的包装类,此时Dubbo会在真正的实现类上层包装上Wrapper。即这个时候从ExtensionLoader中返回的实际扩展类是被Wrapper包装的接口实现类。在上文代码的createExtension(String name)实例化扩展点中(代码1#)可以看到相关代码实现:将反射创建的instance实例作为参数传给Wrapper的构造方法,并通过反射创建Wrapper实例 , 而 后 在 Wapper 实 例 中 注 入 依 赖 , 最 后 将 Wapper 实 例 赋 值 给instance实例。


  • Setter

Dubbo IoC通过setter方法注入依赖。Dubbo首先会通过反射获取实例的所有方法,然后遍历方法列表,检测方法名是否具有setter方法特征。若有这个特征则通过ObjectFactory获取依赖对象,最后通过反射调用setter方法将依赖设置到目标对象中。整个过程对应的注入扩展点代码如下:

image.png

image.png


扩展点实现类的成员如果为其他扩展点类型,ExtensionLoader会自动注入依赖的扩展点。ExtensionLoader通过扫描扩展点实现类的所有set方法来判定其成员。


  • @SPI

在SPI代码实例中,Dubbo只有接口类使用了@SPI注解才会去加载扩展点实现,Dubbo本身重新实现了一套SPI机制,支持AOP与依赖注入,并且可以利用缓存提升加载实现类的性能,也支持实现类的灵活获取。下面是@SPI的定义:

image.png


在上文的loadExtensionClasses中(代码2#)中,我们可以看到getExtensionLoader会对传入的接口进行校验,其中就会检验接口是否被@SPI注解,通过获取并缓存接口的@SPI注解上的默认实现类cacheDefaultExtensionName,再调用loadDirectory方法记载指定目录中的配置文件。源码实现如下:

image.png

image.png


  • @Adaptive

在 Dubbo 中 , 很 多 扩 展 都 是 通 过 SPI 机 制 进 行 加 载 的 , 比 如Protocol、Cluster、LoadBalance等。然而有些扩展并不想在框架启动阶段被加载,而是希望在扩展方法被调用时根据运行时参数进行加载。

在对自适应扩展生成过程进行深入分析之前,我们来看一下与自适应扩展息息相关的一个注解,即@Adaptive注解,该注解的定义如下:

image.png


@Adaptive可注解在类或方法上。当@Adaptive注解在类上时,Dubbo不会为该类生成代理类。当@Adaptive注解在方法(接口方法)上时,Dubbo则会为该方法生成代理逻辑。@Adaptive注解在类上的情况 很 少 , 在 Dubbo 中 仅 有 两 个 类 被 @Adaptive 注 解 了 , 分 别 是AdaptiveCompiler和AdaptiveExtensionFactory。

getAdaptiveExtension方法是获取自适应扩展的入口方法,相关代码如下:

image.png

image.png


getAdaptiveExtension方法首先会检查缓存,如果缓存未命中,则 调 用 方 法 创 建 自 适 应 扩 展 。 下 面 我 们 看 一 下createAdaptiveExtension方法的代码:

image.png


createAdaptiveExtension方法的代码包含了三个逻辑,分别如下:

○ 调 用 getAdaptiveExtensionClass 方 法 获 取 自 适 应 扩 展Class对象。

○ 通过反射进行实例化。

○ 调用injectExtension方法向扩展实例中注入依赖。

  • @Activate

@Activate注解表示一个扩展是否被激活,可以放在类定义和方法上,Dubbo将它用在SPI扩展类定义上,表示这个扩展实现的激活条件和时机。下面是代码示例:

image.png

image.png

上 述 示 例 表 示 只 有 当 group 参 数 作 为 提 供 者 时 才 会 使RpcServerInterceptor拦截逻辑生效,这个注解的作用和Spring Boot中的@Condition注解类似。

相关文章
|
6月前
|
Dubbo Java 应用服务中间件
微服务学习 | Springboot整合Dubbo+Nacos实现RPC调用
微服务学习 | Springboot整合Dubbo+Nacos实现RPC调用
|
6月前
|
Dubbo Cloud Native 网络协议
【Dubbo3技术专题】「服务架构体系」第一章之Dubbo3新特性要点之RPC协议分析介绍
【Dubbo3技术专题】「服务架构体系」第一章之Dubbo3新特性要点之RPC协议分析介绍
94 1
|
6月前
|
设计模式 负载均衡 网络协议
【分布式技术专题】「分布式技术架构」实践见真知,手把手教你如何实现一个属于自己的RPC框架(架构技术引导篇)
【分布式技术专题】「分布式技术架构」实践见真知,手把手教你如何实现一个属于自己的RPC框架(架构技术引导篇)
263 0
|
6月前
|
消息中间件 监控 持续交付
构建高效微服务架构:后端开发的进阶之路
【4月更文挑战第20天】 随着现代软件开发的复杂性日益增加,传统的单体应用已难以满足快速迭代和灵活部署的需求。微服务架构作为一种新兴的分布式系统设计方式,以其独立部署、易于扩展和维护的特点,成为解决这一问题的关键。本文将深入探讨微服务的核心概念、设计原则以及在后端开发实践中如何构建一个高效的微服务架构。我们将从服务划分、通信机制、数据一致性、服务发现与注册等方面入手,提供一系列实用的策略和建议,帮助开发者优化后端系统的性能和可维护性。
|
2月前
|
安全 应用服务中间件 API
微服务分布式系统架构之zookeeper与dubbo-2
微服务分布式系统架构之zookeeper与dubbo-2
|
5月前
|
存储 关系型数据库 MySQL
MySQL数据库进阶第六篇(InnoDB引擎架构,事务原理,MVCC)
MySQL数据库进阶第六篇(InnoDB引擎架构,事务原理,MVCC)
|
6月前
|
消息中间件 分布式计算 中间件
秀出天际!阿里甩出的988页分布式微服务架构进阶神仙手册我粉了
秀出天际!阿里甩出的988页分布式微服务架构进阶神仙手册我粉了
|
2月前
|
XML 负载均衡 监控
分布式-dubbo-简易版的RPC框架
分布式-dubbo-简易版的RPC框架
|
3月前
|
XML API 网络架构
API架构风格对比:SOAP vs REST vs GraphQL vs RPC
API架构风格对比:SOAP vs REST vs GraphQL vs RPC
69 2
|
6月前
|
消息中间件 Java 微服务
构建高效可靠的微服务架构:后端开发的进阶之路
【5月更文挑战第29天】 在当今软件开发领域,微服务架构已成为一种流行的设计模式,它通过将大型应用程序分解为一组小型、独立的服务来提高可扩展性和灵活性。本文将探讨微服务架构的关键概念、优势以及如何在后端开发中实现这种架构。我们将讨论如何选择合适的技术栈、设计服务间通信机制以及确保系统的可靠性和安全性。通过实际案例和最佳实践,我们将展示如何构建一个高性能、可维护的微服务系统。