谭浩的博客

Simple is beauty.

分布式系统--了解RPC

远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。

为什么需要RPC

基于socket接口我们也可以编写分布式应用,但是该方法存在着一些缺点。许多的细节需要程序员去处理,比如如何在同一个连接中分离不同的请求?不同机器上不同的方式实现的进程如何从网络读写数据?

正如计算机中其他问题的解决思路一样,我们可以在socket上提供另一层,隐藏一些和实际任务关系不大的细节,这一层也就是RPC。RPC提供易于网络编程的通信,使客户端服务端通信透明化,程序员无需关注网络通信的细节。最终达到让远程过程调用表现为本地调用的目标。

RPC的生命周期

RPC的实现需要以下步骤:

  • 创建存根程序(stub functions),让用户感觉是在执行本地调用
  • 客户端存根程序(proxy)拥有调用接口,打包参数并调用服务器端程序
  • 服务器存根程序(skeleton)接受请求并调用本地程序

服务器存根程序由两部分组成,Dispatcher接受客户请求,并分发给特定程序调用。Skeletion将网络消息解析为本地类型,调用本地程序,封装返回结果并返回给Dispatcher。

一个RPC请求调用流程如下:

  1. 客户端调用存根程序(将参数压入栈中)

  1. 存根程序将参数转化为网络消息(marshal)

  1. 客户端操作系统给服务器发送一个网络消息

  1. 服务器操作系统接收到消息,并将其向上发送给存根程序

  2. 服务器存根程序解析参数,并调用服务器相应程序(unmarlshal)

  3. 服务器程序运行,并返回值

  1. 服务器存根将返回值转化为网络消息,并发送给服务器操作系统(marshal)

  2. 服务器操作系统将返回值通过网络返回给客户端

  3. 客户端操作系统接收到返回值,并传递给存根程序

  4. 客户端存根解析返回值,并返回给客户端调用程序

可能出现的故障

  1. 客户端可能崩溃和重启
  2. 丢包
  3. 服务端可能崩溃和重启
  4. 网络和服务器可能非常慢

以上的所有的问题从客户端角度来看都是同样的表现,故障的真正的原因对客户端来说是被隐藏的。

At-Least-Once方案

客户端等待服务端确认消息,超时则重新发送请求,重复多次如果仍然不能接收到消息,则返回错误。

没有确认消息返回不代表服务器端没有执行程序,多次请求可能产生副作用。所以At-Least-Once方案只能适用于没有副作用的读操作以及有处理冗余和重排序功能的应用。

At-Most-Once方案

服务端RPC代码检测冗余的请求,返回先前已经执行过的结果而不是重新处理请求。

客户端给每一个RPC请求包含一个唯一的id,重复的请求使用相同的id。则服务端可以使用以下的逻辑检测冗余的请求。

1
2
3
4
5
6
7
if seen[xid]:
retval = old[xid]
else:
retval = handler()
old[xid] = retval
seen[xid] = true
return retval

存在的问题:

服务端seen和old数组可能会无限长大,客户端获取返回值的请求将不会重发,可以在后续请求捎带已经获取返回值的xid。

并发请求,对于正在执行任务不希望执行两次,可以给执行中的RPC请求添加pending标识。

服务器端可能会崩溃和重启,需要将seen和old表写到磁盘中去。

Exactly-Once方案

  • 需要At-Least-Once方案的重传

  • At-Most-Once的冗余请求检测

    为了防止客户端奔溃,需要记录pending的rpc的请求,所以可以重传相同xid的请求

  • 保障服务端的稳定性

    服务端需要将完成的rpc请求写入磁盘中

  • 不能有额外的操作