《Operating Systems: Three Easy Pieces》学习笔记(三十七) 分布式系统、远程过程调用(RPC)
远程过程调用(RPC)
最主要的抽象是基于远程过程调用
(Remote Procedure Call
),或简称 RPC
远程过程调用包都有一个简单的目标:使在远程机器上执行代码的过程像调用本地函数一样简单直接
RPC 系统通常有两部分:存根生成器
(stub generator,有时称为协议编译器,protocol compiler)和运行时库
(run-time library)。
存根生成器
通过自动化,消除将函数参数和结果打包成消息的一些痛苦。
1
2
3
4
interface {
int func1(int arg1);
int func2(int arg1, int arg2);
};
存根生成器类似于写好接口文档,自动生成一个头文件
,可以被其他函数调用。
在内部,客户端存根中的每个函数都执行远程过程调用所需的所有工作。对于客户端
,代码只是作为函数调用
出现(例如,客户端调用 func1(x)
)。
在内部,func1()的客户端存根中的代码执行此操作:
- 创建消息缓冲区。消息缓冲区通常只是某种大小的连续字节数组。
- 将所需信息打包到消息缓冲区中。该信息包括要调用的函数的某种标识符,以及函数所需的所有参数(例如,在上面的示例中,func1 需要一个整数)。将所有这些信息放入单个连续缓冲区的过程,有时被称为参数的封送处理(marshaling)或消息的
序列化
(serialization)。 - 将消息发送到目标 RPC 服务器。与 RPC 服务器的通信,以及使其正常运行所需的所有细节,都由
RPC 运行时库
处理,如下所述。 - 等待回复。由于函数调用通常是
同步
的(synchronous),因此调用将等待其完成。 - 解包返回代码和其他参数。如果函数只返回一个返回码,那么这个过程很简单。但是,较复杂的函数可能会返回更复杂的结果(例如,列表),因此存根可能也需要对它们解包。此步骤也称为解封送处理(unmarshaling)或
反序列化
(deserialization)。 - 返回调用者。最后,只需从客户端存根返回到客户端代码。
对于服务器
,也会生成代码。在服务器上执行的步骤如下:
- 解包消息。此步骤称为解封送处理(unmarshaling)或
反序列化
(deserialization),将信息从传入消息中取出。提取函数标识符和参数。 - 调用实际函数。终于,我们到了实际执行远程函数的地方。RPC 运行时调用 ID 指定的函数,并传入所需的参数。
- 打包结果。返回参数被封送处理,放入一个回复缓冲区。
- 发送回复。回复最终被发送给调用者。
问题1:一个包如何发送复杂的数据结构
?需要合理序列化
问题2:并发性的服务器组织方式?常见的组织方式是线程池
(thread pool)。在这种组织方式中,服务器启动时会创建一组有限的线程。消息到达时,它被分派
给这些工作线程
之一,然后执行 RPC 调用的工作,最终回复。在此期间,主线程
不断接收其他请求,并可能将其发送给其他工作线程。
运行时库
如何找到远程服务?需要命名解析
,如DNS。
TCP作为可靠传输协议有性能损失
许多 RPC 软件包都建立在不可靠
的通信层之上,例如 UDP。这样做可以实现更高效的 RPC 层,但确实增加了为 RPC 系统提供可靠性的责任
。
通过使用某种形式的序列编号,通信层可以保证每个 RPC 只发生一次
(在没有故障的情况下),或者最多只发生一次
(在发生故障的情况下)。
其他问题
当远程调用需要很长时间
才能完成时,一种解决方案是在没有立即生成回复时使用显式确认
(从接收方到发送方)。这让客户端知道
服务器收到了请求
运行时还必须处理具有大参数
的过程调用,发送方分组
(fragmentation,较大的包分成一组较小的包)和接收方重组
(reassembly,较小的部分组成一个较大的逻辑整体)。
字节序
(byte ordering)。有些机器存储值时采用所谓的大端序
(big endian),而其他机器采用小端序
(little endian)。
之前提到服务端可以异步处理请求,客户端也要能异步调用接口
,客户端在某些时候会希望看到异步 RPC 的结果
。因此它再次调用
RPC 层,告诉它等待未完成的 RPC 完成,此时可以访问
返回的结果
。