从本文开始,将记录作者学习 MIT 6.824 分布式系统的学习笔记,如果有志同道合者,欢迎一起交流。
RPC 的定义和结构
RPC 全称为 Remote Procedure Call,他表示一种远程过程的调用,他可以让用户在不知道底层网络协议的情况下,在本地的计算机就可以调用远端服务器的一些处理方法,然后服务器会把处理的结果返回到用户本地,就像这个方法写在用户本地空间中一样。
RPC 的层级结构如下所示:
- Client 端(由上到下):
- Application:应用层,表示和用户直接交互的部分,用户在该层确定自己想要调用的函数,并且输入参数
- stub:原意是烟蒂,很形象的表示这只是个函数空壳,表示用户想要调用的函数,这里可以是函数名
- RPC lib:RPC 库,包括一系列的编码解码工作,对于用户端来说,是编码用户的函数调用请求进入网络层,或者是解码服务端的结果成为上层结构可以理解的语言
- Network Layer:网络层传输协议,这里不再赘述,它可以使 TCP
- Server 端(由下到上):
- Network Layer:网络层传输协议,同上
- RPC lib:RPC 库,对服务端来说,用于解码用户的调用函数请求,或者是函数结果的封装。
- Dispatcher:用户的请求到达该层后,服务端需要通过 Dispatcher,根据请求的函数,找到服务器中对应的函数,并把任务交到这个函数上
- Handler:具体的函数逻辑
如上图所示,RPC 的操作流程为:
- 用户端写好 function call,向下传递到网络层并发送到服务端
- 服务端上行用户请求,通过 Dispatcher 找到具体的 Handler,由 Handler 处理具体的逻辑
- 服务端处理完毕后,通过 RPC lib 编码发还到客户端
- 客户端经过解码后上行结果,用户获得 Application 层获得结果返回。
RPC 的关键技术点
RPC 具体实现的关键技术点如下
- Marshall / Unmarshall:即为我们平常说的序列化/反序列化,用于将应用层的信息转化为网络层可以理解语言,抑或是将网络层的信息解码成为应用层可以理解的语言。这部分在 RPC lib 中可以完成
- Binding:在一个大的网络环境中,如何为用户分配一个符合用户所需逻辑要求的,可用的服务器也是一大难点
- Threads:handler 可能执行得很慢,而通过多线程则可以增强 Server 的吞吐量
RPC 的潜在失败情况和处理方案
RPC 的潜在失败情况有如下几种:
- Lose Packet:网络环境差造成的丢包
- Broken Net:网络直接断了
- Server Crash:服务器崩了
- Server Slow:服务器处理速度太慢
对此,为了一定要让客户端拿到处理结果,有以下几种解决方案:
方案一:At Least once:
该方法的中心思想是:client 发送等待 server 回答,如果没收到,则继续发送。该方式可能造成的问题有:
- 问题一:可能客户端 -> 服务端的网络一直相同,服务器处理正常,但是在回复过程出现问题,从而造成客户端持续发送请求,使得客户端一直重复处理,浪费资源。
- 问题二:如果要处理的请求具有事务性,如操作数据库,那么使用这种方案就无法保证前后的一致性。考虑如下的情况,假设我们要操作的对象是服务端的数据库,可能会出现如下的问题:
如上图所示,PUT 表示我们要去修改一个 key 对应的值,而 GET 则让我们获取这个 key 对应的值,因此当我们延迟的请求到达服务器时,服务器已经处理其他的请求,造成我们最后一次 GET 方法获取的结果可能与我们的预期不同。
所以 At least once 会造成资源浪费,服务端出现不一致的情况,不是一个很好的方法
方案二:At Most Once
其实,方案一的问题在于服务端无法判断客户端因为各种原因收不到而重复发出的请求,因此我们需要让服务器可以发现之前的重复请求,并且在不执行 handler 的情况下,返回之前缓存的结果。因此我们的请求需要带上唯一的识别码,这个识别码可以通过 client ip + 时间戳 生成,也可以使用 client ip + seq(序列号)。这样,服务端就可以知道这是不是一个已经处理过的请求,并通过缓存直接返回结果。
然而,这对服务端也提出了新的问题:
- 问题一: 什么时候可以删掉这些缓存?
毕竟服务端不可能永远保存这些缓存,这会造成容量不够的问题。答案其实也很简单,那就是客户端证明自己收到了回复的时候。对此,我们 RPC 用户端协议可以带一些附加信息,比如:
- 对于客户端 RPC 请求,可以带上其近期已经收到回复的序列号,这样,服务端的在解码过程中可以通过解析该字段,从而删除已经确定收到回复的缓存
- 可以通过使用 sequence number 的方法,即客户端的该次请求是上一次请求的序列号 + 1。这样,客户端就可以在请求中带上一个序列号 n,表示 seq < n 的请求我都已经收到了。服务端即可删除所有序列号小于 n 的缓存。如下的示意图展示了这一种方法:
- 问题二:如果一个请求仍在处理,此时"心急的"客户端又发了一个相同的请求,该如何处理?
这个时候,服务端可以维护一个关于"处理中"的缓存,将正在处理的 request 记录在该缓存,如果有相同请求到来,则先去查询"处理中"的缓存,再去查找"已完成"的缓存,如果存在,则舍弃这条请求,返回已有结果或者等待原有的相同请求处理完成并返回。
总结
本章是对 MIT 6.824 第二课 RPC 内容的总结,主要讲了 RPC 的结构,以及一些潜在错误的处理方法,为 lab1 中 mapreduce RPC 的实现打下基础。