阅读 1364

机器之间如何高效交流?90后技术专家带你读懂RPC框架

OB君:在单机环境下,进程之间的通讯主要通过IPC来实现。但是在不同的机器上,RPC就成为了进行通讯的经典方式。本文由OceanBase的90后技术专家符风向大家娓娓道来RPC框架背后的What、Why和How,并对目前主流的C++ RPC框架进行对比分析。

本文作者:符风
现任蚂蚁金服OceanBase团队技术专家,2012年毕业后加入Oceanbase团队,主要负责OceanBase基础库的建设工作。

早期的计算机程序都是单机程序,一个程序只会在一台机器上跑。如果一台机器上的A进程和B进程,它们之间需要通讯交流,就只能通过IPC(Inter-Process Communication)的方式。典型的就有管道、信号量、共享内存等方式。但是如果这两个进程分别运行在不同的机器上,那么使用IPC就不够了,还需要把网络通讯这个功能加进来。其中,RPC就是在不同机器上进行通讯的经典方式。

什么是RPC?

说白了,RPC就是一种网络上程序之间的沟通方式。稍微正式一点的定义我们可以参考维基百科上查询到的结果:

RPC(Remote Procedure Call)或者叫做远程过程调用是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的程序,而程序员无需额外地为这个交互作用编程。
—— 维基百科

从维基百科的介绍中,我们可以总结出以下几个关键字:通信协议,跨计算机调用,易编程

  • RPC必须要有自己的通讯协议,协议包括序列化协议和网络协议,典型的有JSON、XML等等
  • 因为要跨计算机调用,所以RPC必须要有自己的网络传输层,典型的有TCP/IP等等
  • RPC都是封装成函数调用的方式,使用RPC时就像调用一个函数那样就可以了

为什么用RPC?

网络上进程进行通讯的方式有很多,比如像我们浏览网页使用的HTTP协议,为什么不是直接发个HTTP请求进行通讯呢?使用RPC进行封装的通讯方式有什么优势呢?最大的好处就是易用性

如果没有RPC的封装,一台计算机上的进程要访问另外一台机器上进程的内容需要怎么做呢?

1. 以一种对方能够理解的协议构建请求包
2. 将这个请求包设法发送到对方机器的进程上
3. 对方机器根据协议理解请求包的内容
4. 对方机器处理请求,并以客户机能够理解的协议构造回复包
5. 想办法把这个回复包发送到请求包来源的远程机器上的具体进程,并设法把请求包和回复包关联起来

我们可以看到,这是一个比较复杂并且烦人的工作,单单是如果把消息从一台机器发送到另外一台机器就涉及很多问题:比如怎么进行传输、如何通知进程等等。何况还要处理网络上的各种异常情况:超时、断链接、网络抖动等。

如果使用RPC进行通讯需要几个步骤的?最简单的情况就是直接调用相应函数就可以了,由RPC框架把上面的这五部操作都完成了。

List employees;
RPC.getEmployeeList(dep_id, employees);
复制代码

是不是感觉这个世界都变得清爽了?

典型的RPC框架介于传输层和应用中间,它会帮助处理:

  1. 可靠性
    比如传输层遇到的错误。
  2. 平台无关性
    比如Windows平台是否可以和Linux平台进行通讯?64位的系统是否可以和32位系统进行通讯?
  3. 服务发现和路由选择
    RPC调用实际上是对某个服务的调用,那么RPC框架需要解决具体调用需要落到哪台机器的哪个进程上。
  4. 消息分发
    一般一个进程上会提供多种RPC调用,RPC框架需要提供区别不同类型RPC消息并转到相应处理函数上。
  5. 安全性

RPC框架的设计

RPC框架的核心部分有以下几个:

  1. RPC接口
  2. 对象序列化和反序列化
  3. 传输协议和传输层
  4. RPC消息分发

RPC接口

RPC的作用就是让使用者调用远程请求就像调用本地函数,所以不管是本地客户端还是远程服务端需要使用一套统一的接口,然后两边分别实现自己的逻辑。

举个例子:

比如在设计一个获取部门员工列表的RPC请求,接口可能如下设计,传入一个部门ID,返回部门成员的列表:

void getEmployeeList(const DepartmentID &id, EmployeeList &list);
复制代码

然后在客户端部分根据接口可以这样实现:

// clientvoid getEmployeeList(const DepartmentID &id, EmployeeList &list){
    auto result = Transport.send(GET_EMPLOYEE_LIST, encode(id));
    list.decode(result.buffer());}
复制代码

服务端部分同样根据接口实现具体逻辑:

// servervoid getEmployeeList(const DepartmentID &id, EmployeeList &list){
    auto dep = department_map[id];
    list = dep.get_employees();}
复制代码

接口的作用就是:告诉客户端你只能这样调用这个RPC,同时告诉服务端客户端那边只会这样调用。

上面三段代码中,具体RPC实现者只需要关心接口和服务端的实现逻辑。所以一般RPC框架会提供友好的封装,最简单的形式就是只需要实现一个函数,函数的签名就是接口,实现就是服务端的代码,客户端的代码则由框架自动填充。

对象序列化和反序列化

对象序列化的方式有很多种,比如比较通用的JSON、Protocol Buffers等,也可以按照自己的需求自己定制序列化和反序列化方式。

OceanBase就是设计了自己的对象序列化方式,以满足使用过程中对高性能、少资源占用和易用性各方面的权衡。它相比protobuf可以拥有更高的序列化性能和更小的空间占用,并且和OceanBase的数据结构深度结合,不用像protobuf那样使用自己的DSL语言定义数据类型。

传输协议和传输层

RPC要实现网络上的两台计算机间的通讯,必须依赖具体的网络传输层才能完成。

传输层的选择也有很多,用得比较多的比如TCP、UDP这类的,也有为了追求性能选择RDMA/RoCE/DPDK,或者像gRPC那样选择更上层的HTTP2。

传输层的选择需要考虑几个因素:

  • 物理限制,有些协议需要在特定的硬件环境中才能运行
  • 传输特性,比如TCP协议对可靠性有一定保证,但是需要用户自己处理黏包问题
  • 性能、安全性、是否好调试等因素也可以进行参考

OceanBase的RPC框架使用的传输层主要是基于TCP协议的封装。我们没有选择UDP是为了简化传输层的逻辑,TCP相比UDP而言拥有更完善的控制能力,框架不需要再为拆包组包这些逻辑编写额外的代码。并且,我们为TCP封装了链接复用的功能,避免因为网络延迟等原因需要创建过多的TCP连接。当然我们目前正在探索更多的协议加入至传输层,以满足不同场景下的传输需求。

RPC消息分发

当服务端收到一个RPC消息后,需要根据RPC类型和相应规则分发请求到相应的处理函数上。常见的做法有:

  1. 暴力switch/if else,由编译器做这方面的优化
  2. 实现查找表,以RPC类型(常见为枚举类型)作为下标定位到处理函数
  3. hashmap

其中,使用hashmap的方式最为灵活。前两种方案的实现都有一定的局限性。

OceanBase早期用的是if else分支判断:如果RPC请求包是该类型的,就选择这个处理函数,否则选择另外的处理函数。原先这样做的是因为简单易实现,并且当时请求的种类也很少,效率上并没有太大差别。

后来随着请求种类的增加,使用分支判断风格分发请求在性能上的劣势就慢慢凸显出来了,我们就改用了使用RPC Code作为索引到数组中去查找对应的处理函数。使用查找表最大的问题是如果RPC Code跨度很大,就需要一个非常大的数组来保存这个隐射关系,它会使用更多的内存以及内存访问的局部性变差。Hashmap是更通用一些的方式,后续OceanBase会考虑迁移到这个方案上。

主流C++ RPC框架对比

gRPC

gRPC是一个高性能、通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf(Protocol Buffers)序列化协议开发。它支持众多开发语言,当然也包含了C++了。

gRPC最大的优势是因为IDL基于ProtoBuf的缘故所以非常简单易用,生成的代码也比较小巧易懂,文档非常健全。缺点则是传输层绑定了HTTP/2,序列化层绑定了ProtoBuf,两者都不支持动态定制。HTTP/2对于分布式数据库场景下的对性能有极致追求的场景不太友好。

Thrift

Thrift是一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。它被当作一个远程过程调用(RPC)框架来使用,是由Facebook为“大规模跨语言服务开发”而开发的,目前由Apache基金会管理。

Thrift最大的优势是灵活性非常高,每一层都可以让用户做出不同的选择从而构建出合适自己场景的RPC需求。各层基本都可以用户自己进行定制,比如用户自定义的协议、传输方式和路由分发规则等。这个通过官网的模块图就可以看出来:

PhxRPC

PhxRPC是微信后台团队推出的一个非常简洁小巧的RPC框架,编译生成的库文件非常小巧。根据官网的介绍,它使用ProtoBuf作为IDL,并且只支持ProtoBuf。使用半同步半异步模式,也就是说有专门的IO线程处理epoll。支持ucontext和过载保护。缺点是功能简单,文档也比较少。

brpc

brpc是百度开源的RPC框架。它是一套比较完整的RPC框架,和Thrift一个量级。brpc使用ProtoBuf作为IDL,也可以使用Thrift工具生成代码整合至brpc后和Thrift服务进行通讯。brpc也是一套非常灵活的框架,支持各层的自定义控制;并且从一些网站的介绍和对比测试来看,它的性能也很有竞争力。

总结

RPC是实现网络间不同计算进行通讯的手段,它很好的屏蔽了网络层的细节,让用户跨计算机访问就像调用本地函数那样方便。但是它并不适用于所有的通讯场景,比如做大数据传输时可能直接使用传输层的API更加方便直观,再比如它在处理服务端消息推送的时候会比较麻烦。

实现RPC框架也需要权衡很多因素,比如安全性和通讯效率的平衡、异常处理机制的完善程度等等。

无论如何,一个好用的RPC框架在绝大多数的分布式系统中都扮演着非常重要的角色。

符风邀请你加入OceanBase技术交流群

想跟本文作者 符风 深入交流吗?

想认识蚂蚁金服OceanBase的一线技术专家吗?

扫描下方二维码联系蚂蚁金服加群小助手,快速加入OceanBase技术交流群!



关注下面的标签,发现更多相似文章
评论