关于dubbo:Dubbo服务调用过程

8次阅读

共计 8915 个字符,预计需要花费 23 分钟才能阅读完成。

本文已同步至我的公众号 Code4j,欢送各位看官老爷来玩。

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 来进行代理加强。

上面别离是应用 javasisst 和应用 JDK 动静代理时对代理类进行反编译后的后果。

从下面能够看出,InvacationHandler 要做的事无非就是依据本次调用的办法名和办法参数,将其封装成调用信息 Invacation,而后将其传递给持有的 Invoker 对象。从这里开始,才算是真正进入到了 Dubbo 的外围模型中。

2.2 客户端的调用链路

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

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

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

因而,当 Proxy 层的 InvocationHandler 将调用申请委托给持有的 Invoker 时,其实就是向下传递给对应的 ClusterInvoker,并且通过获取可用 Invoker,依据路由规定过滤 Invoker,以及负载平衡选中要调用的 Invoker 等一系列操作后,就会失去一个具体协定的 Invoker

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

对于集群相干的 Invoker,如果有趣味的话能够看一下用于服务降级的 MockClusterInvoker,集群策略形象父类 AbstractClusterInvoker 以及默认的也是最罕用的失败重试集群策略 FailoverClusterInvoker,实际上默认状况下的集群调用链路就是一一通过这三个类的。

顺带提一句,在获取到具体的协定 Invoker 之前会通过一个过滤器链,对于每一个过滤器对于本次申请都会做一些解决,比方用于统计的 MonitorFilter,用于解决以后上下文信息的 ConsumerContextFilter 等等。过滤器这一部分给用户提供了很大的扩大空间,有趣味的话能够自行理解。

拿到具体的 Invoker 之后,此时所处的地位为上图中的 Protocol 层,这时候就能够通过上层的网络层来实现近程过程调用了,先来看一下 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 的类,因而也具备申请的性能。

能够看到,其实 request() 办法只不过是将数据封装成 Request 对象,结构一个申请的语义,最终还是通过 send() 办法将数据单向发送进来。上面是一张对于客户端发送申请的调用链路图。

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

保留映射的目标是因为在异步状况下,申请和响应并不是一一对应的。为了使得前面接管到的响应能够正确被解决,Dubbo 会在响应中带上对应的申请 ID,当接管到响应后,依据其中的申请 ID 就能够找到对应的 DefaultFuture,并将响应后果设置到 DefaultFuture,使得阻塞在 get() 操作的用户线程能够及时返回。

整个过程能够形象为上面的时序图。

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

留神,在调用 send() 办法之前,所有的逻辑都是用户线程在解决的,而编码工作则是由 Netty 的 I/O 线程解决,有趣味的话能够理解一下 Netty 的线程模型。

2.4 协定和编码

上文提到过很屡次协定(Protocol)和编码,那么到底什么是协定,什么又是编码呢?

其实,艰深一点讲,协定就是一套约定好的通信规定。打个比方,张三和李四要进行交换,那么他们之间在交换之前就须要先约定好如何交换,比方单方约定,当听到“Hello World”的时候,就代表对方要开始讲话了。此时,张三和李四之间的这种约定就是他们的通信协议。

而对于编码的话,其实就是依据约定好的协定,将数据组装成协定规定的格局。当张三想和李四说“早上好”的时候,那么张三只须要在“早上好”之前加上约定好的“Hello World”,也就是最终的音讯为“Hello World 早上好”。李四一听到“Hello World”,就晓得随后的内容是张三想说的,通过这种模式,张三和李四之间就能够实现失常的交换了。

具体到理论的 RPC 通信中,所谓的 Dubbo 协定,RMI 协定,HTTP 协定等等,它们只不过是对应的通信规定不一样,但最终的作用都是一样的,就是提供给组装通信数据的一套规定,仅此而已。

这里借用一张官网的图,展现了默认的 Dubbo 协定数据包格局。

Dubbo 数据包分为音讯头和音讯体。音讯头为定长格局,共 16 字节,用于存储一些元信息,例如音讯的起始标识 Magic Number,数据包的类型,应用的序列化形式 ID,音讯体长度等。音讯体则为变长格局,具体长度存储在音讯头中,这部分是用于存储了具体的调用信息或调用后果,也就是 Invocation 序列化后的字节序列或近程调用返回的对象的字节序列,音讯体这部分的数据是由序列化 / 反序列化来解决的。

之前提到过,Dubbo 将用于编解码数据的通道处理器形象为了 Codec 接口,所以在音讯发送进来之前,Dubbo 会调用该接口的 encode() 办法进行编码。其中,对于音讯体,也就是本次调用的调用信息 Invacation,会通过 Serialization 接口来进行序列化。

Dubbo 在启动客户端和服务端的时候,会通过适配器模式,将 Codec 相干的编解码器与 Netty 进行适配,将其增加到 Netty 的 pipeline 中,参见 NettyCodecAdapterNettyClientNettyServer

上面是相干的编码逻辑,对照上图食用更佳。

编码实现之后,数据就会被 NIO 框架所收回,通过网络达到服务端。

2.5 服务端的调用链路

当服务端接管到数据的时候,因为接管到的都是字节序列,所以第一步应该是对其解码,这一步最终会交给 Codec 接口的 decode 办法解决。

解码的时候会先解析失去音讯头,而后再依据音讯头中的元信息,例如音讯头长度,音讯类型,将音讯体反序列化为 DecodeableRpcInvocation 对象(也就是调用信息)。

此时的线程为 Netty 的 I/O 线程,不肯定会在以后线程解码,所以有可能会失去局部解码的 Request 对象,具体分析见下文。

值得注意的是,在 2.6.x 版本中,默认状况下对于申请的解码会在 I/O 线程中执行,而 2.7.x 之后的版本则是交给业务线程执行。

这里的 I/O 线程指的是底层通信框架中接管申请的线程(其实就是 Netty 中的 Worker 线程),业务线程则是 Dubbo 外部用于解决申请 / 响应的线程池中的线程。如果某个事件可能比拟耗时,不能在 I/O 线程上执行,那么就须要通过线程派发器将线程派发到线程池中去执行。

再次借用官网的一张图,当服务端接管到申请时,会依据不同的线程派发策略,将申请派发到线程池中执行。线程派发器 Dispatcher 自身并不具备线程派发的能力,它只是用于创立具备线程派发能力的 ChannelHandler

Dubbo 领有 5 种线程派发策略,默认应用的策略为 all,具体策略差异见下表。

策略 用处
all 所有音讯都派发到线程池,包含申请,响应,连贯事件,断开事件等
direct 所有音讯都不派发到线程池,全副在 IO 线程上间接执行
message 只有申请和响应音讯派发到线程池,其它音讯均在 IO 线程上执行
execution 只有申请音讯派发到线程池,不含响应。其它音讯均在 IO 线程上执行
Connection 在 IO 线程上,将连贯断开事件放入队列,有序一一执行,其它音讯派发到线程池

通过 DubboCodec 解码器解决过的数据会被 Netty 传递给下一个入站处理器,最终依据配置的线程派发策略来到对应的 ChannelHandler,例如默认的 AllChannelHandler

能够看到,对于每种事件,AllChannelHandler 只是创立了一个 ChannelEventRunnable 对象并提交到业务线程池中去执行,这个 Runnable 对象其实只是一个中转站,它是为了防止在 I/O 线程中执行具体的操作,最终真正的操作它会委托给持有的 ChannelHandler 去解决。

服务端对申请进行派发的过程如下图所示。

下面说过,解码操作也有可能在业务线程中执行,因为 ChannelEventRunnable 中间接持有的 ChannelHandler 就是一个用于解码的 DecodeHandler

如果须要解码,那么这个通道处理器会调用在 I/O 线程中创立的 DecodeableRpcInvocation 对象的 decode 办法,从字节序列中反序列化失去本次调用的类名,办法名,参数信息等。

解码实现后,DecodeHandler 会将齐全解码的 Request 对象持续传递到下一个通道处理器即 HeaderExchangeHandler

到这里其实曾经能够领会到 Dubbo 抽取出 ChannelHandler 的益处了,能够防止和特定 NIO 库耦合,同时应用装璜者模式一层层地解决申请,最终对 NIO 库只暴露出一个特定的 Handler,更加灵便。

这里附上一张服务端 ChannelHandler 的结构图。

HeaderExchangeHandler 会依据本次申请的类型决定如何解决。如果是单向调用,那么只需向后调用即可,不须要返回响应。如果是双向调用,那么就须要在失去具体的调用后果后,封装成 Response 对象,并通过持有的 Channel 对象将本次调用的响应发送回客户端。

HeaderExchangeHandler 将调用委托给持有的 ExchangeHandler 处理器,这个处理器是和服务裸露时应用的协定无关的,一般来说都是某个协定的外部类。

因为默认状况下都是应用的 Dubbo 协定,所以接下来对 Dubbo 协定中的处理器进行剖析。

Dubbo 协定外部的 ExchangeHandler 会从曾经裸露的服务列表中找到本次调用的 Invoker,并且向其发动本地调用。不过要留神的是,这里的 Invoker 是一个动静生成的代理对象,类型为 AbstractProxyInvoker,它持有了解决业务的实在对象。

当发动 invoke 调用时,它会通过持有的实在对象实现调用,并将其封装到 RpcResult 对象中并且返回给上层。

对于 RpcResult 有趣味的话能够理解一下 2.7.x 异步化革新后的变动。简略来说就是 RpcResultAppResonse 所代替,用来保留调用后果或调用异样,同时引入了一个新的中间状态类 AsyncRpcResult 用于代表未实现的 RPC 调用。

这个代理对象是在服务端进行服务裸露的时候生成的,javassist 会动静生成一个 Wrapper 类,并且创立一个匿名外部对象,将调用操作委托给 Wrapper

上面是反编译失去的 Wrapper 类,能够看到具体的解决逻辑和客户端的 InvocationHandler 相似,都是依据本次调用的办法名来向实在对象发动调用。

.png)

至此,服务端已实现了调用过程。上层 ChannelHandler 收到调用后果后,就会通过 Channel 将响应发送回客户端,期间又会通过编码序列化等操作,因为和申请的编码序列化过程相似,这里不再赘述,感兴趣的话能够自行查看 ExchangeCodec#encodeResponse() 以及 DubboCodec#encodeResponseData()

这里再附上一张服务端解决申请的时序图。

2.6 客户端解决响应

当客户端收到调用的响应后,毫无疑问仍旧须要对收到的字节序列进行解码及反序列化,这里和服务端解码申请的过程是相似的,查看 ExchangeCodec#decode() 以及 DubboCodec#decodeBody() 自行理解,也可参考下面的服务端解码申请的时序图,这里只附上一张客户端解决已(局部)解码的响应的时序图。

这里次要讲的是客户端对解码后的 Reponse 对象的解决逻辑。客户端的 ChannelHandler 构造和下面的服务端 ChnnelHandler 结构图没有太大区别,通过解码后的响应最终也会传递到 HeaderExchangeHandler 处理器中进行解决。

在客户端发动申请时咱们提到过,每个结构的申请都有一个 ID 标识,当对应的响应返回时,就会把这个 ID 带上。当接管到响应时,Dubbo 会从申请的 Future 映射汇合中,依据返回的申请 ID,找到对应的 DefaultFuture,并将后果设置到 DefaultFuture 中,同时唤醒阻塞的用户线程,这样就实现了 Dubbo 的业务线程到用户线程的转换。

有趣味的话能够再理解一下 DefauFuture 的超时解决 以及 Dubbo 2.7 异步化革新后的线程模型变动。

最初附上一张起源官网的图。

至此,一个残缺的 RPC 调用就完结了。

因为自己程度无限,可能局部细节并没有讲清楚,如果有疑难的话欢送大家指出,一起交流学习。

3. 参考链接

  • Dubbo 官网 – 服务调用过程
  • 《深刻了解 Apache Dubbo 与实战》
正文完
 0