Dubbo——远程(RPC)调用原理dubbo 支持的9种协议

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6


摘要

服务暴露和服务引入两个流程了,而这两个流程就是为了服务的调用,本博文将详细的介绍Dubbo的服务调用流程。

PRC架构组件

一个基本的RPC架构里面应该至少包含以下4个组件:

  • 客户端(Client):服务调用方(服务消费者)
  • 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端
  • 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理
  • 服务端(Server):服务的真正提供者

Dubbo——远程(RPC)调用原理dubbo 支持的9种协议_服务器

具体调用过程:

  • 显示通过zk来找到的相应的远程的服务的一个调用(这个两个服务的通信的基础)
  • 服务消费者(client客户端)通过调用本地服务的方式调用需要消费的服务;
  • 客户端存根(client stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体;
  • 客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端;
  • 服务端存根(server stub)收到消息后进行解码(反序列化操作);
  • 服务端存根(server stub)根据解码结果调用本地的服务进行相关处理;
  • 本地服务执行具体业务逻辑并将处理结果返回给服务端存根(server stub);
  • 服务端存根(server stub)将返回结果重新打包成消息(序列化)并通过网络发送至消费方;
  • 客户端存根(client stub)接收到消息,并进行解码(反序列化);
  • 服务消费方得到最终结果;

而RPC框架的实现目标则是将上面的第2-10步完好地封装起来,也就是把调用、编码/解码的过程给封装起来,让用户感觉上像调用本地服务一样的调用远程服务。

RPC的实现原理架构图

Dubbo——远程(RPC)调用原理dubbo 支持的9种协议_服务器_02

也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。

比如说,A服务器想调用B服务器上的一个方法:

建立通信

首先要解决通讯的问题:即A机器想要调用B机器,首先得建立起通信连接。

主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。通常这个连接可以是按需连接(需要调用的时候就先建立连接,调用结束后就立马断掉),也可以是长连接(客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效),多个远程过程调用共享同一个连接。

服务寻址

要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称名称是什么。通常情况下我们需要提供B机器(主机名或IP地址)以及特定的端口,然后指定调用的方法或者函数的名称以及入参出参等信息,这样才能完成服务的一个调用。可靠的寻址方式(主要是提供服务的发现)是RPC的实现基石,比如可以采用Redis或者Zookeeper来注册服务等等。

Dubbo——远程(RPC)调用原理dubbo 支持的9种协议_序列化_03

从服务提供者:

  • 当服务提供者启动的时候,需要将自己提供的服务注册到指定的注册中心,以便服务消费者能够通过服务注册中心进行查找;
  • 当服务提供者由于各种原因致使提供的服务停止时,需要向注册中心注销停止的服务;
  • 服务的提供者需要定期向服务注册中心发送心跳检测,服务注册中心如果一段时间未收到来自服务提供者的心跳后,认为该服务提供者已经停止服务,则将该服务从注册中心上去掉。

从调用者:

  • 服务的调用者启动的时候根据自己订阅的服务向服务注册中心查找服务提供者的地址等信息;
  • 当服务调用者消费的服务上线或者下线的时候,注册中心会告知该服务的调用者;
  • 服务调用者下线的时候,则取消订阅。

网络传输

序列化

  • 当A机器上的应用发起一个RPC调用时,调用方法和其入参等信息需要通过底层的网络协议如TCP传输到B机器,由于网络协议是基于二进制的,所有我们传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输。然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给B机器。

反序列化

  • 当B机器接收到A机器的应用发来的请求之后,又需要对接收到的参数等信息进行反序列化操作(序列化的逆操作),即将二进制信息恢复为内存中的表达方式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理Proxy去调用, 通常会有JDK动态代理、CGLIB动态代理、Javassist生成字节码技术等),之后得到调用的返回值。

服务调用

  • B机器进行本地调用(通过代理Proxy和反射调用)之后得到了返回值,此时还需要再把返回值发送回A机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发送回A机器,而当A机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,最后再交给A机器上的应用进行相关处理(一般是业务逻辑处理操作)。

通常,经过以上四个步骤之后,一次完整的RPC调用算是完成了,另外可能因为网络抖动等原因需要重试等。

服务调用总结

注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小

监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示

注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外

注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者

注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表

注册中心和监控中心都是可选的,服务消费者可以直连服务提供者

服务提供者无状态,任意一台宕掉后,不影响使用

服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复

PRC通信协议

常见的三种协议形式

应用层一般有三种类型的协议形式,分别是:固定长度形式、特殊字符隔断形式、header+body 形式。

固定长度形式:指的是协议的长度是固定的,比如100个字节为一个协议单元,那么读取100个字节之后就开始解析。

  • 优点就是效率较高,无脑读一定长度就解析。
  • 缺点就是死板,每次长度只能固定,不能超过限制的长度,并且短了还得填充,在 RPC 场景中不太合适,谁晓得参数啥的要多长,定长了浪费,定短了不够。

特殊字符隔断形式:其实就是定义一个特殊结束符,根据特殊的结束符来判断一个协议单元的结束,比如用换行符等等。

  • 这个协议的优点是长度自由,反正根据特殊字符来截断,缺点就是需要一直读,直到读到一个完整的协议单元之后才能开始解析,然后假如传输的数据里面混入了这个特殊字符就出错了。

header+body 形式:也就是头部是固定长度的,然后头部里面会填写 body 的长度, body 是不固定长度的,

  • 这样伸缩性就比较好了,可以先解析头部,然后根据头部得到 body 的 len 然后解析 body。

dubbo 协议就是属于 header+body 形式,而且也有特殊的字符 0xdabb ,这是用来解决 TCP 网络粘包问题的。

dubbo 支持的9种协议

但是Dubbo官网是推荐我们使用Dubbo协议的

  • dubbo 协议 (默认)
  • rmi 协议
  • hessian 协议
  • http 协议
  • webservice 协议
  • thrift 协议
  • memcached 协议
  • redis 协议
  • rest ( 就是 RestFull)

dubbo协议 (默认)

dubbo 缺省协议 采用单一长连接和NIO异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况,不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。

缺省协议:使用基于mina1.1.7+hessian3.2.1的tbremoting交互。

dubbo协议的特性

  • 连接个数:单连接
  • 连接方式:长连接
  • 传输协议:TCP
  • 传输方式:NIO异步传输
  • 序列化:Hessian 二进制序列化
  • 适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用dubbo协议传输大文件或超大字符串。

适用场景:常规远程服务方法调用

配置

<!--配置协议: -->
<dubbo:protocol name="dubbo" port="20880" />

<!--设置默认协议: -->
<dubbo:provider protocol="dubbo" />

<!-- 设置服务协议: -->
<dubbo:service protocol="dubbo" />

<!-- 多端口 -->
<dubbo:protocol id="dubbo1" name="dubbo" port="20880" />
<dubbo:protocol id="dubbo2" name="dubbo" port="20881" />

<!-- 配置协议选项: -->
<dubbo:protocol name=“dubbo” port=“9090” server=“netty” client=“netty” codec=“dubbo” 
				serialization=“hessian2” charset=“UTF-8” threadpool=“fixed” threads=“100”
		 		queues=“0” iothreads=“9” buffer=“8192” accepts=“1000” payload=“8388608” />

Dubbo协议缺省每服务每提供者每消费者使用单一长连接,如果数据量较大,可以使用多个连接。

<dubbo:protocol name="dubbo" connections="2" />
<dubbo:service connections=”0”>或<dubbo:reference connections=”0”>表示该服务使用JVM共享长连接。(缺省)
<dubbo:service connections=”1”>或<dubbo:reference connections=”1”>表示该服务使用独立长连接。
<dubbo:service connections=”2”>或<dubbo:reference connections=”2”>表示该服务使用独立两条长连接。

为防止被大量连接撑挂,可在服务提供方限制大接收连接数,以实现服务提供方自我保护

<dubbo:protocol name="dubbo" accepts="1000" />

rmi 协议

RMI协议采用JDK标准的java.rmi.*实现,采用阻塞式短连接JDK标准序列化方式 。如果正在使用RMI提供服务给外部访问(公司内网环境应该不会有攻击风险),同时应用里依赖了老的common-collections包(dubbo不会依赖这个包,请排查自己的应用有没有使用)的情况下,存在反序列化安全风险。

特性

  • 连接个数:多连接
  • 连接方式:短连接
  • 传输协议:TCP
  • 传输方式:同步传输
  • 序列化:Java标准二进制序列化
  • 适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。
  • 适用场景:常规远程服务方法调用,与原生RMI服务互操作

接口

如果服务接口继承了java.rmi.Remote接口,可以和原生RMI互操作,即:
提供者用Dubbo的RMI协议暴露服务,消费者直接用标准RMI接口调用,或者提供方用标准RMI暴露服务,消费方用Dubbo的RMI协议调用。

如果服务接口没有继承java.rmi.Remote接口,缺省Dubbo将自动生成一个com.xxx.XxxService$Remote的接口,并继承java.rmi.Remote接口,并以此接口暴露服务,

但如果设置了<dubbo:protocol name="rmi" codec="spring" />,将不生成$Remote接口,而使用Spring的RmiInvocationHandler接口暴露服务,和Spring兼容。

配置

<!-- 定义 RMI 协议: -->
<dubbo:protocol name="rmi" port="1099" />

<!--设置默认协议: -->
<dubbo:provider protocol="rmi" />

<!-- 设置服务协议: -->
<dubbo:service protocol="rmi" />

<!--多端口: -->
<dubbo:protocol id="rmi1" name="rmi" port="1099" />
<dubbo:protocol id="rmi2" name="rmi" port="2099" />
<dubbo:service protocol="rmi1" />

<!--Spring 兼容性:-->
<dubbo:protocol name="rmi" codec="spring" />

hessian 协议

Hessian 协议用于集成 Hessian 的服务,Hessian 底层采用 Http 通讯,采用 Servlet 暴露服务,Dubbo 缺省内嵌 Jetty 作为服务器实现。Dubbo 的 Hessian 协议可以和原生 Hessian 服务互操作,即:

  •  提供者用 Dubbo 的 Hessian 协议暴露服务,消费者直接用标准 Hessian 接口调用
  •  或者提供方用标准 Hessian 暴露服务,消费方用 Dubbo 的 Hessian 协议调用。

Hessian 是 Caucho 开源的一个 RPC 框架,其通讯效率高于 WebService 和 Java 自带的序列化。

特性

  • 连接个数:多连接
  • 连接方式:短连接
  • 传输协议:HTTP
  • 传输方式:同步传输
  • 序列化:Hessian二进制序列化
  • 适用范围:传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。
  • 适用场景:页面传输,文件传输,或与原生hessian服务互操作

依赖:

<dependency>
    <groupId>com.caucho</groupId>
    <artifactId>hessian</artifactId>
    <version>4.0.7</version>
</dependency>

约束
1、参数及返回值需实现Serializable接口
2、参数及返回值不能自定义实现List, Map, Number, Date, Calendar等接口,只能用JDK自带的实现,因为hessian会做特殊处理,自定义实现类中的属性值都会丢失。

配置

<!-- 定义 hessian 协议: -->
<dubbo:protocol name="hessian" port="8080" server="jetty" />

<!--设置默认协议: -->
<dubbo:provider protocol="hessian" />

<!-- 设置 service 协议: -->
<dubbo:service protocol="hessian" />

<!-- 多端口:-->
<dubbo:protocol id="hessian1" name="hessian" port="8080" />
<dubbo:protocol id="hessian2" name="hessian" port="8081" />

<!--直连:-->
<dubbo:reference id="helloService" interface="HelloWorld" url="hessian://10.20.153.10:8080/helloWorld" />

web.xml 配置:

<servlet>
     <servlet-name>dubbo</servlet-name>
     <servlet-class>com.alibaba.dubbo.remoting.http.servlet.DispatcherServlet</servlet-class>
     <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
     <servlet-name>dubbo</servlet-name>
     <url-pattern>/*</url-pattern>
</servlet-mapping>

注意:如果使用servlet派发请求
协议的端口<dubbo:protocol port="8080" />必须与servlet容器的端口相同,
协议的上下文路径<dubbo:protocol contextpath="foo" />必须与servlet应用的上下文路径相同。

http 协议

基于http表单的远程调用协议

特性

  • 连接个数:多连接
  • 连接方式:短连接
  • 传输协议:HTTP
  • 传输方式:同步传输
  • 序列化:表单序列化 ,即 json
  • 适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件。
  • 适用场景:需同时给应用程序和浏览器JS使用的服务。

配置

<!-- 配置协议:-->
<dubbo:protocol   name="http"  port="8080" />

<!-- 配置 Jetty Server (默认):-->
<dubbo:protocol ...  server="jetty" />

<!-- 配置 Servlet Bridge Server (推荐使用): -->
<dubbo:protocol ... server="servlet" />

配置 DispatcherServlet:

<servlet>
         <servlet-name>dubbo</servlet-name>
         <servlet-class>org.apache.dubbo.remoting.http.servlet.DispatcherServlet</servlet-class>
         <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
         <servlet-name>dubbo</servlet-name>
         <url-pattern>/*</url-pattern>
</servlet-mapping>

注意:如果使用 servlet 派发请求 :

  • 协议的端口<dubbo:protocol port="8080" />必须与servlet容器的端口相同,
  • 协议的上下文路径<dubbo:protocol contextpath="foo" />必须与servlet应用的上下文路径相同。

webservice 协议

可以和原生WebService服务互操作,即:提供者用Dubbo的WebService协议暴露服务,消费者直接用标准WebService接口调用,或者提供方用标准WebService暴露服务,消费方用Dubbo的WebService协议调用。基于 WebService 的远程调用协议,基于 Apache CXF的 frontend-simple 和 transports-http 实现。可以和原生 WebService 服务互操作,即:

  • 提供者用 Dubbo 的 WebService 协议暴露服务,消费者直接用标准 WebService 接口调用,
  • 或者提供方用标准 WebService 暴露服务,消费方用 Dubbo 的 WebService 协议调用。

依赖

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-frontend-simple</artifactId>
    <version>2.6.1</version>
</dependency>
<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-transports-http</artifactId>
    <version>2.6.1</version>
</dependency>

特性

  • 连接个数:多连接
  • 连接方式:短连接
  • 传输协议:HTTP
  • 传输方式:同步传输
  • 序列化:SOAP文本序列化
  • 适用场景:系统集成,跨语言调用

配置

<!-- 配置协议: -->
<dubbo:protocol name="webservice" port="8080" server="jetty" />

<!-- 配置默认协议:-->
<dubbo:provider protocol="webservice" />

<!-- 配置服务协议:-->
<dubbo:service protocol="webservice" />

<!--多端口: -->
<dubbo:protocol id="webservice1" name="webservice" port="8080" />
<dubbo:protocol id="webservice2" name="webservice" port="8081" />

<!-- 直连: -->
<dubbo:reference id="helloService" interface="HelloWorld" url="webservice://10.20.153.10:8080/com.foo.HelloWorld" />

<!-- WSDL -->
http://10.20.153.10:8080/com.foo.HelloWorld?wsdl

<!-- Jetty Server: (默认) -->
<dubbo:protocol ... server="jetty" />

<!-- Servlet Bridge Server: (推荐) -->
<dubbo:protocol ... server="servlet" />

配置 DispatcherServlet:

<servlet>
     <servlet-name>dubbo</servlet-name>
     <servlet-class>com.alibaba.dubbo.remoting.http.servlet.DispatcherServlet</servlet-class>
     <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
     <servlet-name>dubbo</servlet-name>
     <url-pattern>/*</url-pattern>
</servlet-mapping>

注意:如果使用servlet派发请求:
协议的端口<dubbo:protocol port=“8080” />必须与servlet容器的端口相同,
协议的上下文路径<dubbo:protocol contextpath=“foo” />必须与servlet应用的上下文路径相同。

Dubbo的PRC调用原理

Dubbo——远程(RPC)调用原理dubbo 支持的9种协议_服务提供者_04

Dubbo——远程(RPC)调用原理dubbo 支持的9种协议_序列化_05

首先在客户端启动时会从注册中心拉取和订阅对应的服务列表,Cluster会把拉取的服务列表聚合成一个Invoker,每次RPC调用前会通过Directory#list获取providers地址(已经生成好的Invoker列表),获取这些服务列表给后续路由和负载均衡使用。对应图在①中主要是将多个服务提供者做聚合。在框架内部另外一个实现Directory接口是RegistryDirectory类,它和接口名是一对一的关系(每一个接口都有一个RegistryDirectory实例),主要负责拉取和订阅服务提供者、动态配置和路由项。

在Dubbo发起服务调用时,所有路由和负载均衡都是在客户端实现的。客户端服务调用首先会触发路由操作,然后将路由结果得到的服务列表作为负载均衡参数,经过负载均衡后会选出一台机器进行RPC调用,这3个步骤依次对应于②、③和④。客户端经过路由和负载均衡后,会将请求交给底层I/O线程池(比如Netty)处理,I/O线程池主要处理读写、序列化和反序列化等逻辑,因此这里一定不能阻塞操作,Dubbo也提供参数控制(decode.in.io)参数,在处理反序列化对象时会在业务线程池中处理。在⑤中包含两种类似的线程池,一种是I/O线程池(Netty),另一种是Dubbo业务线程池(承载业务方法调用)。

目前Dubbo将服务调用和Telnet调用做了端口复用,在编解码层面也做了适配。在Telnet调用时,会新建立一个TCP连接,传递接口、方法和JSON格式的参数进行服务调用,在编解码层面简单读取流中的字符串(因为不是Dubbo标准头报文),最终交给Telnet对应的Handler去解析方法调用。如果是非Telnet调用,则服务提供方会根据传递过来的接口、分组和版本信息查找Invoker对应的实例进行反射调用。在⑦中进行了端口复用,如果是Telnet调用,则先找到对应的Invoker进行方法调用。Telnet和正常RPC调用不一样的地方是序列化和反序列化使用的不是Hessian方式,而是直接使用fastjson进行处理。

Dubbo的PRC调用流程

Dubbo——远程(RPC)调用原理dubbo 支持的9种协议_dubbo_06

  • 首先客户端调用接口的某个方法,实际调用的是代理类,代理类会通过 cluster 从 directory 中获取一堆 invokers(如果有一堆的话),然后进行 router 的过滤(其中看配置也会添加 mockInvoker 用于服务降级),然后再通过 SPI 得到 loadBalance 进行一波负载均衡。
  • 这里要强调一下默认的 cluster 是 FailoverCluster ,会进行容错重试处理
  • 现在我们已经得到要调用的远程服务对应的 invoker 了,此时根据具体的协议构造请求头,然后将参数根据具体的序列化协议序列化之后构造塞入请求体中,再通过 NettyClient 发起远程调用。
  • 服务端 NettyServer 收到请求之后,根据协议得到信息并且反序列化成对象,再按照派发策略派发消息,默认是 All,扔给业务线程池。
  • 业务线程会根据消息类型判断然后得到 serviceKey 从之前服务暴露生成的 exporterMap 中得到对应的 Invoker ,然后调用真实的实现类。
  • 最终将结果返回,因为请求和响应都有一个统一的 ID, 客户端根据响应的 ID 找到存储起来的 Future, 然后塞入响应再唤醒等待 future 的线程,完成一次远程调用全过程。

博文参考

《Dubbo系列》-Dubbo的服务调用过程

dubbo 支持的9种协议_那年那些事儿

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

“Dubbo——远程(RPC)调用原理dubbo 支持的9种协议” 的相关文章