MiniRpc 开篇

这篇文章根据16年暑假准备开始写Rpc库之前写的笔记整理的。

Rpc即远程服务调用,使用非常广泛,也有很多的开源实现,比如google的gRpc、微信的phxrpc,阿里的Dubbo。之所以重新造轮子开始纯粹是为了练手。到现在为止一开始设想的基本功能差不多都已经实现了,翻了翻EverNote,发现在编码测试重构的过程中还是遇到了不少问题,有设计上一开始没有考虑清楚的,有编码上的低级bug,还有一些调试上的,所以就想着从EverNote中整理出几篇博客记录一下。

这是第一篇,先介绍下如果要写一个自己的Rpc库所先需要基本概念。

消息模式:

一个Rpc系统中最重要的就是消息模式,消息模式定义了对传输消息的保证,常见的模式有:

  • at least once :指消息至少发送一次,可能超过一次
  • at most once :指消息最多发送一次,可能不发送

在实现中如何区分两者呢?很好理解,就是Rpc系统是否有持久化和重传功能,如果消息在指定时间内没有相应,就会触发重传,这就实现了at least once 。如果没有重传,就是at most once。

在我的实现中并不是at most once但也不是严格意义上的at least once。MiniRpc带有超时重传功能,但是没有对消息进行持久化,如果内存耗尽或者进程崩溃就会造成消息的丢失。这有点类似于ZeroMq。

另外多说一句,现实系统中由于网络并非百分百可靠,所以并不存在just once的模式。

调用模型:

前面的模式针对的消息的传输次数,那这里的调用模型针对的是调用方,分为同步和异步模型。

  • 同步模型 :指Client端阻塞在Rpc调用处,一直等到结果返回或者调用超时
  • 异步模型 :指Client端不用等待结果返回

MiniRpc同时支持上面两种模型。同步模型很好实现,直接阻塞即可,为了实现异步模型,我们需要注册回调函数供返回消息时调用。

调用基本流程:

我第一次接触到Rpc时觉得有点神奇,因为一台机器居然可以调用另外一台机器上的函数,其实稍微一想就会明白这就是一个加了Buff的网络库。

为了使用Rpc,需要在Client端和Server端各添加一个stub。

Client端的stub负责将方法、参数等组装成能够进行网络传输的消息体,同时找到服务地址,发送消息。

Server端的stub接收到消息后,根据提取出的信息,调用对应的函数,将结果打包发送给Client端。

所有这一切都对用户透明,所以在用户看来仅仅是调用一个函数,并不关心函数是在哪台机器上运行,只用等待结果即可,不用纠缠于细节,Rpc可以极大的减轻用户的负担。

网络模型

知道了一次Rpc调用的基本流程,我们就知道所有的Rpc都是要经过网络的,选择恰当的网络模型对于实现Rpc是至关重要的。

MiniRpc使用的是Reactor模型,基于linux提供的高效的epoll系统调用,同时加入了多线程模型,来应对耗时长的调用。

实现中网络层这一块的细节我会放在第二篇来讲解。

消息格式

确定了网络模型后,我们就可以在Client和Server直接收发消息了,那么接下来就是顺理成章的要确定收发的消息格式了。

消息一般分两种,request和reply,分别是Client到Server和Server回到Client。

Request:按照最直观的理解,我们需要在消息头添加上函数名和对应的参数,只有知道了名称server端才知道该调用那个函数。
其他还需要调用的超时时间,如果在这个时间内没有消息返回的话就会放弃本次调用,将超时异常返回给Client端。如果是异步调用还需要添加requestId,才能识别不同的异步调用。

Reply:包含返回值和已经函数调用的状态,同时还要包含RequestId。

序列化

序列化是可选的,一般选择序列化都是为了方便网络传输,如果不在乎消息的大小,直接使用JSON传输也是可以的。

MiniRpc选用了ProtoBuf来实现序列化,能尽量减小消息的格式,缺点是二进制协议不具备可读性。

服务发现

除了原始的通过ip和port提供服务以外,最好我们能将服务发现的过程自动化。

Server端将提供的服务提交给服务提供节点,Client端每次调用服务的时候从服务提供节点获取对应服务的server的ip和端口,再去连接server。

这就要求服务提供节点具有极高的容错性,避免出现单点故障,所以一般使用Zookeeper或者其他实现了类paxos协议的机器群。