RPC架构

举报
Mr.Z事顺意 发表于 2023/02/23 18:12:17 2023/02/23
【摘要】 RPC就是把拦截到的方法参数,转成可以在网络中传输的二进制,并保证在服务提供方能正确地还原出语义,最终实现像调用本地一样地调用远程的目的。1 RPC架构RPC本质是远程调用,就要通过网络来传输数据。考虑到可靠性,一般默认采用TCP协议。为屏蔽网络传输复杂性,要封装一个单独的数据传输模块收发二进制数据,即传输模块。用户请求是基于方法调用,方法出入参数都是对象数据,要提前转成二进制,即序列化过程...

RPC就是把拦截到的方法参数,转成可以在网络中传输的二进制,并保证在服务提供方能正确地还原出语义,最终实现像调用本地一样地调用远程的目的。
1 RPC架构

RPC本质是远程调用,就要通过网络来传输数据。考虑到可靠性,一般默认采用TCP协议。为屏蔽网络传输复杂性,要封装一个单独的数据传输模块收发二进制数据,即传输模块。

用户请求是基于方法调用,方法出入参数都是对象数据,要提前转成二进制,即序列化过程。但只是把方法调用参数的二进制数据传输到服务提供方不够,要在方法调用参数的二进制数据后增加“断句”符,分隔出不同的请求,在两个“断句”符号中间放的内容就是请求的二进制数据,即协议封装。

这两个不同过程目的一样,保证数据在网络中正确传输:

    数据能够传输
    传输后能正确还原出传输前的语义

可把这两个处理过程放在架构中的同一个模块,统称为协议模块。

还可在协议模块加压缩功能,压缩过程也是对传输的二进制数据进行操作。在实际网络传输过程中,请求数据包在数据链路层可能因太大而被拆分成多个数据包进行传输,为减少被拆分次数,导致整个传输过程时间太长,可在RPC调用时:在方法调用参数或者返回值的二进制数据大于某个阈值的情况下,我们可以通过压缩框架进行无损压缩,然后在另外一端也用同样的压缩算法进行解压,保证数据可还原。

传输和协议两模块是RPC最基础功能,它们使对象可正确传输到服务提供方。但距离RPC目标——实现像调用本地一样调用远程,还缺点。要让这两个模块同时工作,要手写一些黏合代码,但这些代码对使用RPC的研发无意义,且属于一个重复工作,导致使用体验不友好。

要在RPC里把这些细节对研发屏蔽,让他们感觉不到本地调用和远程调用区别。假设有用到Spring,希望RPC能让我们把一个RPC接口定义成一个Spring Bean,并且这个Bean也会统一被Spring Bean Factory管理,可在项目中通过Spring依赖注入到方式引用。这是RPC调用的入口,一般叫Bootstrap模块。

点对点(Point to Point)版本的RPC框架就完成了,一般这种模式的RPC框架为单机版,没有集群能力。

集群能力:针对同一接口有多个服务提供者,但这多个服务提供者对调用方透明,所以在RPC里还要给调用方找到所有的服务提供方,并在RPC里维护好接口跟服务提供者地址的关系,调用方在发起请求时,才能快速找到对应接收地址,即“服务发现”。

但服务发现只解决接口和服务提供方地址映射关系查找,是一种“静态数据”,对RPC来说,每次发送请求时都要用TCP连接的,相对服务提供方IP地址,TCP连接状态瞬息万变,所以RPC框架要有连接管理器去维护TCP连接状态。

有了集群,提供方可能就需要管理好这些服务,RPC就要内置一些服务治理功能,如服务提供方权重的设置、调用授权等一些常规治理手段。而服务调用方需要额外做哪些事?每次调用前,都要根据服务提供方设置的规则,从集群中选择可用的连接,以发送请求。

按分层设计原则,将这些功能模块分为:


2 可扩展架构

RPC框架怎么支持插件化架构?可将每个功能点抽象成一个接口,将这个接口作为插件契约,然后把这个功能的接口与功能实现分离,并提供接口默认实现。

JDK自带SPI可动态为某接口寻找服务实现,要在Classpath下的META-INF/services目录创建一个以服务接口命名的文件,文件内容就是接口具体实现类。
JDK自带SPI的缺陷

不能按需加载,ServiceLoader加载某接口实现类时,会遍历全部获取,即接口的实现类得全部载入并实例化,造成不必要浪费。

扩展如果依赖其它的扩展,就做不到自动注入和装配,很难和其他框架集成,如扩展里面依赖了一个Spring Bean,原生Java SPI就不支持。

加上插件功能,RPC框架就包含了两大核心体系——核心功能体系与插件体系:

整个架构就成了一个微内核架构,我们将每个功能点抽象成一个接口,将这个接口作为插件的契约,然后把这个功能的接口与功能的实现分离并提供接口的默认实现。

这样的架构可扩展性好,实现开闭原则,用户方便通过插件扩展实现功能,而且不需要修改核心功能本身

保持了核心包的精简,依赖外部包少,有效减少开发人员引入RPC导致的包版本冲突问题。

单体应用拆分成多个服务后的优点:

1.降低项目的臃肿程度,按业务维度划分服务后,各个团队专注于所负责的业务线,职责单一化

2.服务间解耦,各个服务可以安排合适的研发人员,独立开发,采用合适的技术栈和独立部署

项目服务化后也会引申出一些问题:

一个业务功能请求需要经过多个服务间调用才能最终完成,各个服务独立部署,无法像单体应用一样本地调用,我们要如何实现服务间的调用呢?

方式一:调用方和被调用方约定一个协议格式,通过Socket通信来传输参数,被调用方监听Socket,根据接收的参数调用本地方法

缺点:调用方和被调用方每次都要关注底层的调用细节

    入参序列化成字节流,字节流反序列化成参数
    Socket传输协议,socket发送和接收

为什么要进行序列化?

在业务程序中,我们通常通过“对象”来操纵数据,但当需要对数据进行存储或者传输时,“对象”就不这么好用了,往往需要把数据转化成连续空间的“二进制字节流”,一些典型的场景是:

    数据库索引的磁盘存储:数据库的索引在内存里是b+树,但这个格式是不能够直接存储到磁盘上的,所以需要把b+树转化为连续空间的二进制字节流,才能存储到磁盘上
    缓存的KV存储:redis/memcache是KV类型的缓存,缓存存储的value必须是连续空间的二进制字节流,而不能够是User对象
    数据的网络传输:socket发送的数据必须是连续空间的二进制字节流,也不能是对象

所谓序列化(Serialization),就是将“对象”形态的数据转化为“连续空间二进制字节流”形态数据的过程。这个过程的逆过程叫做反序列化。

怎么进行序列化?

    通过具有自描述特性的标记语言(例如json和xml)来描述对象,规定好转换规则,发送方很容易把一个对象序列化为xml,服务方收到xml二进制流之后,也很容易将其反序列化为对象。
    通过自定义一个二进制协议来进行序列化,我们重点来介绍下google的protocol buffer(pb)

我们都知道软件开发的过程很复杂,不仅是因为业务需求经常变化,更难的是在开发过程中要保证团队成员的目标统一。我们需要用一种可沟通的话语、可“触摸”的愿景达成目标,我认为这就是软件架构设计的意义。

但仅从功能角度设计出的软件架构并不够健壮,系统不仅要能正确地运行,还要以最低的成本进行可持续的维护,因此我们十分有必要关注系统的可扩展性。只有这样,才能满足业务变化的需求,让系统的生命力不断延伸。
4 FAQ

我是个小测试。使用jmeter进行压力测试。jmeter官网中支持进行定制sampler取样器,写好的jar包放在lib\ext下,再启动jmter时就能看到了。插件化是一个概念,有很多种实现方式,这种也算。

spring的spring.factories这一套也是利用的面向接口编程,感觉比jdk自带的spi也好很多,既然有些问题,那为啥jdk的spi不优化一下?jdk我理解更多是标准。

jdk自带spi一般会有一个接口加载很多实现类的情况吗,因为只能用迭代器遍历,导致只能用类型判断才能找到自己想要的类,这样感觉不够优雅吧,所以我感觉应该都是一个接口配置一个实现类这样就是使用者想要的情况了。那样插件的意义就不存在了!

业务为工业设备联网数据采集,设备种类和型号繁多,产品中通过抽象出一套“驱动”的概念,把每类设备当作一个插件开发,整体产品架构不变,感觉有点这个概念。只是产品还不够大,其他插件体系还不够明确。



【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。