1. RPC
RPC(Remote Procedure Call),即远程过程调用,是一个分布式系统间通信的必备技术。分布式系统中的网络通信一般都会采用四层的TCP协议或七层的HTTP协议
HTTP协议,以其中的Restful规范为代表,它可读性好,且可以得到防火墙的支持、跨语言的支持。缺点是有用信息占比少,效率低,毕竟HTTP工作在第七层,包含了大量的HTTP头等信息。
RPC 最核心要解决的问题就是在分布式系统之间间,如何执行另外一个地址空间上的函数,就仿佛在本地调用一样。
- 屏蔽远程调用跟本地调用的区别,让我们感觉就是调用项目内的方法
- 隐藏底层网络通信的复杂性,让我们更专注于业务逻辑。
一个典型 RPC 的使用场景中,包含了服务发现、负载、容错、网络传输、序列化等组件,其中“RPC 协议”就指明了程序如何进行网络传输和序列化。
2. RPC核心功能
RPC 的核心功能是指实现一个 RPC 最重要的功能模块,就是上图中的”RPC 协议”部分
要让网络通信细节对使用者透明,我们需要对通信细节进行封装,我们先看下一个RPC调用的流程涉及到哪些通信细节:
- 服务消费者(Client 客户端)通过本地调用的方式调用服务。
- 客户端存根(Client Stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体。
- 客户端存根(Client Stub)找到远程的服务地址,并且将消息通过网络发送给服务端。
- 服务端存根(Server Stub)收到消息后进行解码(反序列化操作)。
- 服务端存根(Server Stub)根据解码结果调用本地的服务进行相关处理
- 服务端(Server)本地服务业务处理。
- 处理结果返回给服务端存根(Server Stub)。
- 服务端存根(Server Stub)序列化结果。
- 服务端存根(Server Stub)将结果通过网络发送至消费方。
- 客户端存根(Client Stub)接收到消息,并进行解码(反序列化)。
- 服务消费方得到最终结果。
RPC的目标就是要2~10这些步骤都封装起来,让用户对这些细节透明。
上图也说明了RPC核心的重要组成:
- 客户端(Client):服务调用方。
- 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
- 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
- 服务端(Server):服务的真正提供者。
- Network Service:底层传输,可以是 TCP 或 HTTP
3. RPC 核心功能的实现
RPC 的核心功能主要由 5 个模块组成
- 透明化远程服务调用:字节码生成,JDK动态代理
- 编码和解码
- 服务寻址:Call ID 映射,可以直接使用函数字符串,也可以使用整数 ID。映射表一般就是一个哈希表。
- 数据流的序列化和反序列化:可以自己写,也可以使用 Protobuf 或者 FlatBuffers 之类的。
- 网络传输:在 RPC 中可选的网络传输方式有多种,可以选择 TCP 协议、UDP 协议、HTTP 协议
3.1. 透明化远程服务调用
Java通过代理封装通信细节让让用户像以本地调用方式调用远程服务。Java代理有两种方式:
- jdk 动态代理
- 字节码生成
尽管字节码生成方式实现的代理更为强大和高效,但代码维护不易,大部分公司实现RPC框架时还是选择动态代理方式。
动态代理的介绍查看这篇文章
3.2. 编码和解码
通过动态代理封装了通信细节,而通信的第一步就是要确定客户端和服务端相互通信的消息结构。
客户端的请求消息结构一般需要包括以下内容:
- 接口名称,服务端会有很多接口
- 方法名,一个接口内可能有很多方法
- 参数类型&参数值,参数类型有很多,比如有bool、int、long、double、string、map、list,甚至如struct(class);以及相应的参数值;
- 超时时间
- requestID,标识唯一请求id
服务端返回的消息结构一般包括以下内容。
- 返回值
- 状态code
- requestID
3.3. 服务寻址
服务寻址可以使用 Call ID 映射。
在本地调用中,函数体是直接通过函数指针来指定的,但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以在 RPC 中,所有的函数都必须有自己的一个 ID。这个 ID 在所有进程中都是唯一确定的。
客户端在做远程过程调用时,必须附上这个 ID。然后我们还需要在客户端和服务端分别维护一个函数和Call ID的对应表。
当客户端需要进行远程调用时,它就查一下这个表,找出相应的 Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
3.4. 数据流的序列化和反序列化
序列化可以简单理解为对象 –> 字节的过程,同理,反序列化则是相反的过程。这一过程的目的可以理解为转义,然后方便传输。
在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用 C++,客户端用 Java 或者 Python)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。
现如今序列化的方案越来越多,每种序列化方案都有优点和缺点,它们在设计之初有自己独特的应用场景。从RPC的角度上看,主要看三点:
- 通用性,比如是否能支持Map等复杂的数据结构;
- 性能,包括时间复杂度和空间复杂度,由于RPC框架将会被公司几乎所有服务使用,如果序列化上能节约一点时间,对整个公司的收益都将非常可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少;
- 可扩展性,对互联网公司而言,业务变化飞快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。
目前互联网公司广泛使用Protobuf、Thrift、Avro等成熟的序列化解决方案来搭建RPC框架,这些都是久经考验的解决方案。
3.5. 网络传输
远程调用往往用在网络上,客户端和服务端是通过网络连接的。消息数据结构被序列化为二进制串后,下一步就要进行网络通信了。
所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把 Call ID 和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。 只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。
在 RPC 中可选的网络传输方式有多种,可以选择 TCP 协议、UDP 协议、HTTP 协议,每一种协议对整体的性能和效率都有不同的影响。
如何选择一个正确的网络传输协议呢?首先要搞明白各种传输协议在 RPC 中的工作方式。
- 基于TCP协议的RPC调用
由服务的调用方与服务的提供方建立 Socket 连接,并由服务的调用方通过 Socket 将需要调用的接口名称、方法名称和参数序列化后传递给服务的提供方,服务的提供方反序列化后再利用反射调用相关的方法,最后将结果返回给服务的调用方
- 基于HTTP协议的RPC调用
服务的调用者向服务的提供者发送请求,这种请求的方式可能是 GET、POST、PUT、DELETE 等中的一种,服务的提供者可能会根据不同的请求方式做出不同的处理,或者某个方法只允许某种请求方式。 而调用的具体方法则是根据 URL 进行方法调用,而方法所需要的参数可能是对服务调用方传输过去的 XML 数据或者 JSON 数据解析后的结果,返回 JOSN 或者 XML 的数据结果。
两种方式对比
- 基于TCP的协议实现的RPC调用,由于 TCP 协议处于协议栈的下层,能够更加灵活地对协议字段进行定制,减少网络开销,提高性能,实现更大的吞吐量和并发数。但是需要更多关注底层复杂的细节,实现的代价更高。同时对不同平台,如安卓,iOS 等,需要重新开发出不同的工具包来进行请求发送和相应解析,工作量大,难以快速响应和满足用户需求。
- 基于HTTP协议实现的RPC则可以使用 JSON 和 XML 格式的请求或响应数据。而 JSON 和 XML 作为通用的格式标准(使用 HTTP 协议也需要序列化和反序列化,不过这不是该协议下关心的内容,成熟的 Web 程序已经做好了序列化内容),开源的解析工具已经相当成熟,在其上进行二次开发会非常便捷和简单。但是由于 HTTP 协议是上层协议,发送包含同等内容的信息,使用 HTTP 协议传输所占用的字节数会比使用 TCP 协议传输所占用的字节数更高。因此在同等网络下,通过 HTTP 协议传输相同内容,效率会比基于 TCP 协议的数据效率要低,信息传输所占用的时间也会更长,当然压缩数据,能够缩小这一差距。
4. RPC 和 Restful API 对比
面对对象不同:
- RPC 更侧重于动作。
- REST 的主体是资源。
RESTful 是面向资源的设计架构,但在系统中有很多对象不能抽象成资源,比如登录,修改密码等而 RPC 可以通过动作去操作资源。所以在操作的全面性上 RPC 大于 RESTful。
传输效率:
- RPC 效率更高。RPC,使用自定义的 TCP 协议,可以让请求报文体积更小,或者使用 HTTP2 协议,也可以很好的减少报文的体积,提高传输效率。
复杂度:
- RPC 实现复杂,流程繁琐。
- REST 调用及测试都很方便。
RPC 实现需要实现编码,序列化,网络传输等。而 RESTful 不要关注这些,RESTful 实现更简单。
灵活性:
- HTTP 相对更规范,更标准,更通用,无论哪种语言都支持 HTTP 协议。
- RPC 可以实现跨语言调用,但整体灵活性不如 RESTful。
5. RPC要解决的问题
- 协议
- 序列化与反序列化
- 网络通信
- 服务发现
- 健康检查
- 路由策略
- 负载均衡
- 异常重试
- 熔断限流
- 优雅关闭
- 优雅启动
- 流量隔离
6. 参考资料
https://zhuanlan.zhihu.com/p/148044947
https://www.cnblogs.com/crazylqy/p/7995395.html
https://developer.51cto.com/art/201906/597963.htm