DUBBO源码应用总结十三---服务调用过程

本文深入探讨了远程过程调用(RPC)的概念,以Dubbo为例,详细阐述了从动态代理开始的远程调用过程。文章介绍了Dubbo的调用链路,包括客户端的调用、动态代理、集群层、协议层以及网络层的工作原理,揭示了Dubbo如何将远程调用包装成本地调用的无缝体验。
摘要由CSDN通过智能技术生成

"2021SC@SDUSC"

1. 什么是远程过程调用

在讲述 Dubbo 的服务调用过程之前,让我们先来了解一下什么是远程过程调用。

远程过程调用即 Remote Producedure Call,简单来说就是跨进程调用,通过网络传输,使得 A 机器上的应用可以像调用本地的服务一样去调用 B 机器上的服务。

举个最简单的栗子,假设现在有一个电商系统,其中有着用户服务,优惠券服务,订单服务等服务模块,这些不同的服务并不是运行在同一个 JVM 中,而是分开运行在不同的 JVM 中。因此,当订单服务想要调用优惠券服务时,就不能像以前的单体应用一样,直接向对应服务发起本地调用,只能够通过网络来发起调用。

那么,一个最简单的远程过程调用是怎么样的呢?来看下面这张图。

最简单的调用过程

也就是说,一次最简单的 RPC 调用,无非就是调用方通过网络,将调用的参数传送到服务方,服务方收到调用请求后,根据参数完成本地调用,并且将结果通过网络传送回调用方。

在这个过程中,像参数的封装,网络传输等细节会由 RPC 框架来完成,把上面的图片完善一下,一个完整的 RPC 调用的流程是这样的:

  • 客户端(Client)以本地调用的方式调用远程服务。
  • 客户端代理对象(Client Stub)将本次请求的相关信息(要调用的类名、方法名、方法参数等)封装成 Request,并且对其序列化,为网络通信做准备。
  • 客户端代理对象(Client Stub)找到服务端(Server)的地址,通过网络(Socket 通信)将 Request 发送到服务端。
  • 服务端代理对象(Server Stub)接收到客户端(Client)的请求后,将二进制数据反序列化为 Request
  • 服务端代理对象(Server Stub)根据调用信息向本地的方法发起调用。
  • 服务端代理对象(Server Stub)将调用后的结果封装到 Response 中,并且对其序列化,通过网络发送给客户端。
  • 客户端代理对象(Client Stub)收到响应后,将其反序列化为 Response,远程调用结束。

调用过程核心流程

2. Dubbo 的远程调用过程

本节内容基于 Dubbo 2.6.x 版本,并且使用官网提供的 Demo 对同步调用进行分析。

在上一节内容中,我们已经对服务调用的过程有了一定的了解。实际上,Dubbo 在实现远程调用的时候,核心流程和上面的图片是完全一样的,只不过 Dubbo 在此基础上增加了一些额外的流程,例如集群容错、负载均衡、过滤器链等。

本篇文章只分析核心的调用流程,其它的额外流程可以自行了解。

在讲解 Dubbo 的调用过程之前,我们先来了解一下 Dubbo 的一些概念。

  • Invoker:在 Dubbo 中作为实体域,也就是代表了要操作的对象模型,这有点像 Spring 中的 Bean,所有的操作都是围绕着这个实体域来进行。

    • 代表了一个可执行体,可以向它发起 invoke 调用。它有可能是一个本地实现,也有可能是一个远程实现,也有可能是一个集群实现。
  • Invocation:在 Dubbo 中作为会话域,表示每次操作的瞬时状态,操作前创建,操作后销毁。

    • 其实就是调用信息,存放了调用的类名、方法名、参数等信息。
  • Protocol:在 Dubbo 作为服务域,负责实体域和会话域的生命周期管理。

    • 可以理解为 Spring 中的 BeanFactory,是产品的入口。

2.1 远程调用的开端 —— 动态代理

在了解以上基本概念后,我们开始来跟踪 Dubbo 的远程调用流程。在 RPC 框架中,想要实现远程调用,代理对象是不可或缺的,因为它可以帮我们屏蔽很多底层细节,使得我们对远程调用无感知。

如果用过 JDK 的动态代理或者是 CGLIB 的动态代理,那么应该都知道每个代理对象都会有对应的一个处理器,用于处理动态代理时的增强,例如 JDK 使用的 InvacationHandler 或者 CGLIB 的 MethodInterceptor。在 Dubbo 中,默认是使用 javasisst 来实现动态代理的,它与 JDK 动态一样使用 InvocationHandler 来进行代理增强。

InvokerInvocationHandler

下面分别是使用 javasisst 和使用 JDK 动态代理时对代理类进行反编译后的结果。

代理类反编译结果

从上面可以看出,InvacationHandler 要做的事无非就是根据本次调用的方法名和方法参数,将其封装成调用信息 Invacation,然后将其传递给持有的 Invoker 对象。从这里开始,才算是真正进入到了 Dubbo 的核心模型中。

2.2 客户端的调用链路

在了解客户端的调用链路之前,我们需要先看一下 Dubbo 的整体设计,下图是来自于 Dubbo 官网的一张框架设计图,很好地展示了整个框架的结构。

Dubbo 整体框架设计

为了容易理解,我把上图中的 Proxy 代理层、Cluster 集群层以及 Protocol 协议层进行了一个抽象。

如下图所示, Dubbo 的 Proxy 代理层先与下层的 Cluster 集群层进行交互。Cluster 这一层的作用就是将多个 Invoker 伪装成一个 ClusterInvoker 后暴露给上层使用,由该 ClusterInvoker 来负责容错的相关逻辑,例如快速失败,失败重试等等。对于上层的 Proxy 来说,这一层的容错逻辑是透明的。

Proxy 到 Invoker 的大概流程

因此,当 Proxy 层的 InvocationHandler 将调用请求委托给持有的 Invoker 时,其实就是向下传递给对应的 ClusterInvoker,并且经过获取可用 Invoker,根据路由规则过滤 Invoker,以及负载均衡选中要调用的 Invoker 等一系列操作后,就会得到一个具体协议的 Invoker

这个具体的 Invoker 可能是一个远程实现,例如默认的 Dubbo 协议对应的 DubboInvoker,也有可能是一个本地实现,例如 Injvm 协议对应的 InjvmInvoker 等。

关于集群相关的 Invoker,如果有兴趣的话可以看一下用于服务降级的 MockClusterInvoker,集群策略抽象父类 AbstractClusterInvoker 以及默认的也是最常用的失败重试集群策略 FailoverClusterInvoker,实际上默认情况下的集群调用链路就是逐个经过这三个类的。

顺带提一句,在获取到具体的协议 Invoker 之前会经过一个过滤器链,对于每一个过滤器对于本次请求都会做一些处理,比如用于统计的 MonitorFilter,用于处理当前上下文信息的 ConsumerContextFilter 等等。过滤器这一部分给用户提供了很大的扩展空间,有兴趣的话可以自行了解。

拿到具体的 Invoker 之后,此时所处的位置为上图中的 Protocol 层,这时候就可以通过下层的网络层来完成远程过程调用了,先来看一下 DubboInvoker 的源码。

DubboInvoker

可以看到,Dubbo 对于调用方式做了一些区分,分别为同步调用,异步调用以及单次调用。

首先有一点要明确的是,同步调用也好,异步调用也好,这都是站在用户的角度来看的,但是在网络这一层面的话,所有的交互都是异步的,网络框架只负责将数据发送出去,或者将收到的数据向上传递,网络框架并不知道本次发送出去的二进制数据和收到的二进制的数据是否是一一对应的。

因此,当用户选择同步调用的时候,为了将底层的异步通信转化为同步操作,这里 Dubbo 需要调用某个阻塞操作,使用户线程阻塞在这里,直到本次调用的结果返回。

2.3 远程调用的基石 —— 网络层

在上一小节的 DubboInvoker 当中,我们可以看到远程调用的请求是通过一个 ExchangeClient 的类发送出去的,这个 ExchangeClient 类处于 Dubbo 框架的远程通信模块中的 Exchange 信息交换层。

从前面出现过的架构图中可以看到,远程通信模块共分为三层,从上到下分别是 Exchange 信息交换层,Transport 网络传输层以及 Serialize 序列化层,每一层都有其特定的作用。

从最底层的 Serialize 层说起,这一层的作用就是负责序列化/反序列化,它对多种序列化方式进行了抽象,如 JDK 序列化,Hessian 序列化,JSON 序列化等。

往上则是 Transport 层,这一层负责的单向的消息传输,强调的是一种 Message 的语义,不体现交互的概念。同时这一层也对各种 NIO 框架进行了抽象,例如 Netty,Mina 等等。

再往上就是 Exhange 层,和 Transport 层不同,这一层负责的是请求/响应的交互,强调的一种 RequestReponse 的语义,也正是由于请求响应的存在,才会有 ClientServer 的区分。

远程通信模块分层结构

了解完远程通信模块的分层结构后,我们再来看一下该模块中的核心概念。

Dubbo 在这个模块中抽取出了一个端点 Endpoint 的概念,通过一个 IP 和 一个 Port,就可以唯一确定一个端点。在这两个端点之间,我们可以建立 TCP 连接,而这个连接被 Dubbo 抽象成了通道 Channel,通道处理器 ChannelHandler 则负责对通道进行处理,例如处理通道的连接建立事件、连接断开事件,处理读取到的数据、发送的数据以及捕获到的异常等。

同时,为了在语义上对端点进行区分,Dubbo 将发起请求的端点抽象为客户端 Client,而发送响应的端点则抽象成服务端 Server。由于不同的 NIO 框架对外接口和使用方式不一样,所以为了避免上层接口直接依赖具体的 NIO 库,Dubbo 在 ClientServer 之上又抽象出了一个 Transporter 接口,该接口用于获取 ClientServer,后续如果需要更换使用的 NIO 库,那么只需要替换相关实现类即可。

Dubbo 将负责数据编解码功能的处理器抽象成了 Codec 接口,有兴趣的话可以自行了解。

网络通信抽象图

Endpoint 主要的作用就是发送数据,因此 Dubbo 为其定义了 send() 方法;同时,让 Channel 继承 Endpoint,使其在发送数据的基础上拥有添加 K/V 属性的功能。

对于客户端来说,一个 Cleint 只会关联着一个 Channel,因此直接继承 Channel 使其也具备发送数据的功能即可,而 Server 可以接受多个 Cleint 建立的 Channel 连接,所以 Dubbo 没有让其继承 Channel,而是选择让其直接继承 Endpoint,并且提供了 getChannels() 方法用于获取关联的连接。

为了体现了请求/响应的交互模式,在 ChannelServer 以及 Client 的基础上进一步抽象出 ExchangeChannelExchangeServer 以及 ExchangeClient 接口,并为 ExchangeChannel 接口添加 request() 方法,具体类图如下。

远程通信模块类图

了解完网络层的相关概念后,让我们看回 DubboInvoker,当同步调用时,DubboInvoker 会通过持有的 ExchangeClient 来发起请求。实际上,这个调用最后会被 HeaderExchangeChannel 类所接收,这是一个实现了 ExchangeChannel 的类,因此也具备请求的功能。

HeaderExchangeChannel

可以看到,其实 request() 方法只不过是将数据封装成 Request 对象,构造一个请求的语义,最终还是通过 send() 方法将数据单向发送出去。下面是一张关于客户端发送请求的调用链路图。

客户端发送请求

这里值得注意的是 DefaultFuture 对象的创建。DefaultFuture 类是 Dubbo 参照 Java 中的 Future 类所设计的,这意味着它可以用于异步操作。每个 Request 对象都有一个 ID,当创建 DefaultFuture 时,会将请求 ID 和创建的 DefaultFutrue 映射给保存起来,同时设置超时时间。

保存映射的目的是因为在异步情况下,请求和响应并不是一一对应的。为了使得后面接收到的响应可以正确被处理,Dubbo 会在响应中带上对应的请求 ID,当接收到响应后,根据其中的请求 ID 就可以找到对应的 DefaultFuture,并将响应结果设置到 DefaultFuture,使得阻塞在 get() 操作的用户线程可以及时返回。

DefaultFuture 构造函数

整个过程可以抽象为下面的时序图。

DefaultFuture 的作用

ExchangeChannel 调用 send() 后,数据就会通过底层的 NIO 框架发送出去,不过在将数据通过网络传输之前,还有最后一步需要做的,那就是序列化和编码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值