RPC框架的组成

目录

RPC自定义私有协议

RPC的传输层

RPC的动态代理

RPC的协议层

RPC服务注册和发现

RPC负载均衡


​​​​​​​

RPC自定义私有协议

整个协议就会拆分成两部分:协议头和协议体。

协议头是由一堆固定的长度参数组成,而协议体是根据请求接口和参数构造的,长度属于可变的,具体协议如下图所示

为了保证能平滑地升级改造前后的协议,我们有必要设计一种支持可扩展的协议。其关键在于让协议头支持可扩展,扩展后协议头的长度就不能定长了。那要实现读取不定长的协议头里面的内容,在这之前肯定需要一个固定的地方读取长度,所以我们需要一个固定的写入协议头的长度。整体协议就变成了三部分内容:协议头固定部分协议头扩展部分、协议体内容,前两部分我们还是可以统称为“协议头”,具体协议如下:

 

我们协议的结构不仅要支持协议体的扩展,还要做到协议头也能扩展。

按照协议格式来编解码,就能从字节流中正确区分消息。跟粘包半包问题相关。

RPC的序列化

序列化反序列化和编解码通常被整合在一起。

为什么要序列化?

网络传输的数据必须是二进制数据,但调用方Request对象和服务方Response对象都是对象。对象是不能直接在网络中传输的,所以我们需要提前把它转成可传输的二进制,并且要求转换算法是可逆的,这个过程我们一般叫做“序列化”。这时,服务提供方就可以正确地从二进制数据中分割出不同的请求,同时根据请求类型和序列化类型,把二进制的消息体逆向还原成请求对象,这个过程我们称之为“反序列化”。

 

 

 

 

有哪些序列化()技术(把基本类型数据或对象作为可存储的字节数组)?优劣?

1、JDK自带序列化  objectoutputstream/objectinputstream

jdk序列化的缺点:

1、无法跨语言

2、序列化编码后的字节数组太大

3、序列化耗时严重

2fastJSON:文本型序列化框架,String和对象的转换,应用广泛。如果 RPC 框架选用 JSON 序列化,服务提供者与服务调用者之间传输的数据量要相对较小,否则将严重影响性能。

3、Hessian

4、Google 的Protobuf

5、Message pack

6、JBoss Marshalling(序列化包)

 

Kryo:专为 JAVA 定制的序列化协议,序列化后字节数少,利于网络传输。但不支持跨语言(或支持的代价比较大)。dubbox 扩展中支持了 kryo 序列化协议。github 3018 star。

Hessian:支持跨语言,序列化后字节数适中,API 易用。是国内主流 rpc 框架:dubbo,motan 的默认序列化协议。hessian.caucho.com 未托管在 github

Protostuff:提起 Protostuff 不得不说到 Protobuf。Protobuf 可能更出名一些,因为其是 google 的亲儿子,grpc 框架便是使用 protobuf 作为序列化协议,虽然 protobuf 与语言无关平台无关,但需要使用特定的语法编写 .prpto 文件,然后静态编译,这带了一些复杂性。而 protostuff 实际是对 protobuf 的扩展,protostuff-runtime 模块继承了 protobuf 性能,且不需要预编译文件,但与此同时,也失去了跨语言的特性。所以 protostuff 的定位是一个 JAVA 序列化框架,其性能略优于 Hessian。tip :protostuff 反序列化时需用户自己初始化序列化后的对象,其只负责将该对象进行赋值。github 719 star。

Fastjson:作为一个 json 工具,被拉到 RPC 的序列化方案中似乎有点不妥,但 motan 该 RPC 框架除了支持 hessian 之外,还支持了 fastjson 的序列化。可以将其作为一个跨语言序列化的简易实现方案。如果 RPC 框架选用 JSON 序列化,服务提供者与服务调用者之间传输的数据量要相对较小,否则将严重影响性能。

 

 

选择序列化技术的标准?

我们首选的还是 Hessian 与 Protobuf,因为他们在性能、时间开销、空间开销、通用性、兼容性和安全性上,都满足了我们的要求。其中 Hessian 在使用上更加方便,在对象的兼容性上更好;Protobuf 则更加高效,通用性上更有优势。

 

 

 

 

序列化的实现

 

 

RPC的传输层

两种传输方式,socket BIO传输和netty NIO传输

 

传输对象

 

 

 

Socket服务器

 

 

 

 

socket客户端

报文头 + 报文体的传输格式是一种常见的格式,除此之外,使用特殊的字符如空行也可以划分出报文结构。在示例中,我们使用一个 int(4 字节)来传递报问题的长度,之后传递报文体,在复杂的通信协议中,报文头除了存储报文体还会额外存储一些信息,包括协议名称,版本,心跳标识等。

 

使用 Socket 通信可以发现:每次 Server 处理 Client 请求都会从线程池中取出一个线程来处理请求,这样的开销对于一般的 Rpc 调用是不能够接受的,而 Netty 一类的网络框架便派上了用场。

 

 

 

Netty服务器

 

 

 

 

 

Netty客户端

 

 

真正的 RPC 封装操作大多集中在 Handler 的 channelRead 方法(负责读取)以及 channel.writeAndFlush 方法(负责写入)中。

 

编码器

 

解码器

 

使用 Netty 不能保证返回的字节大小,所以需要加上 in.readableBytes()< 4 这样的判断,以及 in.markReaderIndex() 这样的标记,用来区分报文头和报文体。

 

 

在异步调用中,如果发起一次异步调用后,立刻使用 future.get(),则大致和同步调用等同。其真正的优势是在 submit 和 future.get() 之间可以混杂一些非依赖性的耗时操作,而不是同步等待,从而充分利用时间片。

RPC的动态代理

动态代理发生在服务调用方 / 客户端,RPC 框架需要解决的一个问题是:像调用本地接口一样调用远程的接口。于是如何组装数据报文,经过网络传输发送至服务提供方,屏蔽远程接口调用的细节,便是动态代理需要做的工作了。RPC 框架中的代理层往往是单独的一层,以方便替换代理方式(如 motan 代理层位于 com.weibo.api.motan.proxy ,dubbo 代理层位于 com.alibaba.dubbo.common.bytecode )。

 

RPC 框架无论选择何种代理技术,所需要完成的任务其实是固定的,不外乎‘整理报文’,‘确认网络位置’,‘序列化’,’网络传输’,‘反序列化’,’返回结果’…

 

由服务提供者给出业务接口声明,在调用方的程序里面,RPC 框架根据调用的服务接口提前生成动态代理实现类,并通过依赖注入等技术注入到声明了该接口的相关业务逻辑里面。该代理实现类会拦截所有的方法调用,在提供的方法处理逻辑里面完成一整套的远程调用,并把远程调用结果返回给调用方,这样调用方在调用远程方法的时候就获得了像调用本地接口一样的体验。

3

 

 

 

 

 

 

Jdk 的执行速度一定会慢于 Cglib 和 Javassist,但最慢也就 2 倍,并没有达到数量级的差距;Cglib 和 Javassist 不相上下,差距不大(测试中偶尔发现 Cglib 实行速度会比平时慢 10 倍,不清楚是什么原因)

所以出于易用性和性能,私以为使用 Cglib 是一个很好的选择(性能和 Javassist 持平,易用性和 Jdk 持平)。

 

反射调用

RPC 框架中在 Provider 服务端需要根据客户端传递来的 className + method + param 来找到容器中的实际方法执行反射调用。除了反射调用外,还可以使用 Cglib 来加速。

RPC的协议层

RPC 框架是一个分层结构,从我的这个《深入理解 RPC》系列就可以看出,是按照分层来介绍 RPC 的原理的,前面已经介绍过了传输层,序列化层,动态代理层,他们各自负责 RPC 调用生命周期中的一环,而协议层则是凌驾于它们所有层之上的一层。简单描述下各个层之间的关系:

protocol 层主要用于配置 refer(发现服务) 和 exporter(暴露服务) 的实现方式,transport 层定义了传输的方式,codec 层诠释了具体传输过程中报文解析的方式,serialize 层负责将对象转换成字节,以用于传输,proxy 层负责将这些细节屏蔽。

它们的包含关系如下:protocol > transport > codec > serialize

 

 

Dubbo 中的协议

dubbo://

rmi://

hessian://

http://

webserivice://

RPC服务注册和发现

针对同一个接口有着多个服务提供者,但这多个服务提供者对于我们的调用方来说是透明的,所以在 RPC 里面我们还需要给调用方找到所有的服务提供方,并需要在 RPC 里面维护好接口跟服务提供者地址的关系,这样调用方在发起请求的时候才能快速地找到对应的接收地址,这就是我们常说的“服务发现”。

 

注册中心,用于服务端注册远程服务以及客户端发现服务

服务端,对外提供后台服务,将自己的服务信息注册到注册中心

客户端,从注册中心获取远程服务的注册信息,然后进行远程过程调用

 

1服务平台管理端先在 ZooKeeper 中创建一个服务根路径,可以根据接口名命名(例如:/service/com.demo.xxService),在这个路径再创建服务提供方目录服务调用方目录(例如:provider、consumer),分别用来存储服务提供方的URL和服务调用方的URL

2当服务提供方发起注册时,会在服务提供方目录中创建一个临时节点ZNode,节点中存储该服务提供方的注册信息URL

3当服务调用方想要调用方法,因为客户端和服务端共享同一个接口,发起订阅时,就能通过这个接口获取到对应服务端的注册信息URL。然后在服务调用方目录中创建一个临时节点ZNode,节点中存储该服务调用方的信息URL,同时服务调用方 watch 该服务的服务提供方目录(/service/com.demo.xxService/provider)中所有的服务节点数据。

4、通过心跳检测机制当服务提供方目录下有节点数据发生变更时,ZooKeeper 就会通知给发起订阅的服务调用方。

 

 

注册信息URL包括IP地址+端口号serialization 对应序列化方式,protocol 对应协议名称,maxContentLength 对应 RPC 传输中数据报文的最大长度,shareChannel 是传输层用到的参数,netty channel 中的一个属性,group 对应分组名称。各种传输层和协议层的参数。

 

 

 

RPC负载均衡

负载均衡

负载均衡可以分为服务端负载均衡和客户端负载均衡,而服务端负载均衡又按照实现方式的不同可以划分为软件负载均衡和硬件负载均衡

在 RPC 调用中,客户端持有所有的服务端节点引用,自行通过负载均衡算法选择一个节点进行访问,这便是客户端负载均衡。(客户端如何获取到所有的服务端节点引用呢?一般是通过配置的方式,或者是从上一篇文章介绍的服务注册与发现组件中获取)

 

负载均衡算法有几种经典实现,已经是老生常谈了,总结后主要有如下几个:

  1. 轮询(Round Robin)
  2. 加权轮询(Weight Round Robin)
  3. 随机(Random)
  4. 加权随机(Weight Random)
  5. 源地址哈希(Hash)
  6. 一致性哈希(ConsistentHash)
  7. 最小连接数(Least Connections)
  8. 低并发优先(Active Weight)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RPC(Remote Procedure Call)是一种用于分布式系统间通信的机制。它允许一个系统中的应用程序调用另一个系统中的远程服务,就像调用本地服务一样。RPC的核心思想是将不同系统间的网络通信、数据传输和序列化等复杂细节进行封装,使开发者可以像调用本地函数一样方便地调用远程函数。 Dubbo是一种基于RPC机制的分布式服务框架。它由阿里巴巴集团开发,用于解决大规模分布式系统中的服务治理问题。Dubbo提供了服务注册与发现、负载均衡、容错处理、服务路由等功能,使得分布式系统的开发和管理更加简单高效。Dubbo框架支持多种远程通信协议,如HTTP、TCP等,可根据实际场景选择适合的协议。 Dubbo的架构由三层组成:服务提供者、注册中心和服务消费者。服务提供者将自己提供的服务注册到注册中心,服务消费者从注册中心获取服务的地址列表,并通过远程调用的方式调用服务提供者提供的功能。Dubbo框架还支持服务的负载均衡,使得调用请求可以在多个服务提供者之间进行分发,提高系统的并发能力和稳定性。 总之,RPC机制和Dubbo框架都是用于构建分布式系统的重要工具。RPC机制提供了远程调用的能力,使得分布式系统中的不同应用程序可以互相调用;而Dubbo框架RPC机制的基础上实现了更加便捷的服务治理功能,简化了分布式系统的开发和管理过程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值