文章

《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 完成,此时可以访问返回的结果

参考

本文由作者按照 CC BY 4.0 进行授权

© Kai. 保留部分权利。

浙ICP备20006745号-2,本站由 Jekyll 生成,采用 Chirpy 主题。