RPC,全称Remote procedure Call,是分布式系统中常用的通信方式。
它使程序能够调用另一个地址空间中的过程或函数,通常是共享网络中的另一台机器上的过程或函数,而不需要程序员显式编码此远程调用的细节。
除了 RPC 之外,系统间数据交换的常见方法还包括分布式消息队列、HTTP 请求调用、数据库和分布式缓存等。
RPC 和 HTTP 调用都是直接的端到端系统数据交互,无需中介。
简介
- RPC本质上是通过传递参数并从另一台机器(服务器)接收结果来调用一台机器(客户端)上的函数或方法(统称为服务)。
- RPC 抽象了底层通信细节,无需直接处理套接字通信或 HTTP 通信。
- 在RPC模型中,客户端发起请求,服务器提供响应,类似于HTTP的操作。RPC 的运行方式类似于调用远程函数或方法,就好像它们是本地函数或方法一样。
我们为什么使用RPC?
RPC 的主要目标是简化分布式应用程序的开发,提供健壮的远程调用能力,同时保持本地调用的简洁语义。
为了实现这一目标,RPC框架需要提供透明的调用机制,允许用户进行远程调用,而无需明确区分本地调用和远程调用。
RPC需要解决的三个关键问题
RPC的目标是让远程调用像本地调用一样方便,向调用者隐藏远程调用的复杂性,确保调用者感觉不到本地调用和远程调用之间的区别。
1. 调用ID 映射
在远程过程调用 (RPC) 中,为了通知远程计算机要调用的函数,会为每个函数分配一个唯一的标识符。本地调用时,通过函数指针直接指定函数,编译器自动根据函数指针来处理调用。
然而,在远程调用的上下文中,调用函数指针是不可能的,因为两个进程的地址空间完全不同。因此,在RPC中,每个函数必须有一个唯一的ID,该ID在所有进程中都是一致的。
当客户端发起远程过程调用时,必须包含此唯一 ID。此外,客户端和服务器都维护一个映射表,将功能与其相应的调用 ID 相关联。客户端和服务器端的表可以不相同,但同一功能的调用 ID 必须匹配。
当客户端需要执行远程调用时,它会在其表中查找 Call ID,并将其传递给服务器,服务器利用自己的表来确定客户端要调用哪个函数。然后服务器执行与该函数相关的代码。
2.序列化和反序列化
客户端如何将参数值传递给远程函数?
在本地调用中,我们只需将参数压入堆栈,然后让函数从堆栈中读取它们。
然而,在远程过程调用中,客户端和服务器是独立的进程,无法通过内存传输参数。有时,客户端和服务器甚至可能不使用相同的编程语言(例如,服务器可能使用 C++,而客户端使用 Java 或 Python)。
在这种情况下,客户端需要将参数转换为字节流,发?送到服务器,然后将字节流转换回客户端可以读取的格式。这个过程称为序列化和反序列化。
同样,从服务器返回的值也需要序列化和反序列化过程。
3.网络传输
远程过程调用通常依赖于网络通信,其中客户端和服务器通过网络连接。所有数据都需要通过网络传输,因此需要网络传输层。
网络传输层负责将Call ID和序列化的参数字节流传输到服务器,随后将序列化的调用结果发送回客户端。
任何完成这些任务的协议都可以用作传输层,因此协议的选择不受限制。
虽然许多 RPC 框架使用TCP作为其底层协议,但UDP也可以使用。
例如,gRPC利用HTTP2。Java 的 Netty 是属于网络传输层类别的另一个组件。
如何实现一个高可用的RPC框架
- 由于系统运行在分布式架构下,一个服务通常有多个实例。为了解决实例发现的挑战,需要一个服务注册表。例如,对于 Dubbo,Zookeeper 可以充当服务注册中心。调用时,客户端可以从 Zookeeper 中检索服务实例列表,然后选择一个进行调用。
- 如何选择实例取决于负载均衡等因素。例如,Dubbo提供了四种负载均衡策略。
- 如果每次都去注册中心查询实例列表效率低下,可以通过缓存来提高效率。
- 客户端每次调用后都无法等待服务器返回数据,因此支持异步调用就变得很有必要。
- 当服务器的界面发生变化而旧界面仍在使用时,版本控制就变得至关重要。
- 服务器无法立即启动新线程来处理每个传入请求。因此需要一个线程池。
一个完整的RPC流程
RPC 涉及网络通信,因为它用于分布式系统不同部分之间的远程调用。
为了保证这些系统之间数据传输的可靠性,RPC通常默认使用TCP进行网络数据传输。
在网络传输过程中,RPC不会将请求参数的所有二进制数据作为一个单元发送到服务提供者的机器上。相反,它将数据拆分为多个数据包(或将多个数据包封装为一个数据包)。
因此,服务提供商可能会同时收到多个数据包或部分数据包,这就是网络传输中的“数据包碎片”和“半数据包”问题。
为了解决这个问题,有必要预先定义传输数据的格式,即RPC protocol.
大多数协议分为数据头和消息体:
- 数据头通常用于标识,包括协议标识符、数据大小、请求类型、序列化类型等信息。
- 消息正文主要包含请求的业务参数以及任何附加属性或扩展。
一旦RPC protocol定义完毕,一次完整的 RPC 调用通常会经历以下步骤:
- 调用者不断地将请求参数对象序列化为二进制数据,然后通过 TCP 传输给服务提供者。
- 服务提供者从 TCP 通道接收二进制数据。
- 遵循 RPC 协议,服务提供者将二进制数据分割成不同的请求数据,然后反转序列化过程以重建请求对象。之后,它会识别相应的实现类并继续执行实际的方法调用。
- 随后,服务提供者将执行结果序列化并写回相应的TCP通道。
- 调用者收到响应数据包后,将其反序列化为响应对象,完成调用者端的RPC调用。
如何提高网络通信性能
RPC框架如何选择高性能网络编程I/O模型?
对于RPC网络通信问题,首先应该掌握网络编程中的五种I/O模型。
- 同步阻塞 I/O (BIO)。
- 同步非阻塞 I/O。
- I/O 复用 (NIO)。
- 信号驱动 I/O。
- 异步 I/O (AIO)。
但在实际开发中,最常用的是BIO和NIO。
NIO相比之下,提高了服务器的线程利用率,BIO并引入了调度程序,将套接字连接的处理与套接字数据的读写分开。
目前主流的RPC框架中,广泛采用I/O复用模型。Linux操作系统中的select、poll、epoll等系统调用都是I/O复用的机制。
如果喜欢这篇文章,点赞支持一下!关注我第一时间查看更多内容!