转载

RPC 解决了什么问题

RPC(remote procedure call),远程过程调用,相信大家都不陌生。如果有不清楚的同学可以看下wiki定义:

In distributed computing a remote procedure call (RPC) is when a computer program causes a procedure (subroutine) to execute in another address space(commonly on another computer on a shared network), which is coded as if it were a normal (local) procedure call, without the programmer explicitly coding the details for the remote interaction. 

这段定义提到三点:

  1. 分布式环境。

  2. 写起来就跟调用本地函数一样。

  3. 程序员无需关注与远程的交互细节。

这三点也可以用来描述RPC框架需要解决的问题。

RPC本身并不会涉及什么高深的技术,因此在github上随手一搜,就会发现,RPC库跟网络库一样是造轮子的一大重灾区。

话虽如此,写玩具代码也是程序员的一大乐趣。相信看完这篇文章,你也会加入造RPC库轮子的大军中去。

在进入主题之前,我们首先来消除下二义性。

web开发中也有RPC的概念,与现在比较流行的RESTful相反,是基于HTTP协议设计的一类基于「操作」的API:

GET /operation?id=anId

而小说君接下来要讲的RPC,更多地描述的是 「代码」层面的RPC。

比如下面一段逻辑,服务器A处理某个请求时,需要从其他的服务器拿数据,可以这样写:

var data = serviceDelegate.GetData(id);

代码是这样写,GetData内部做的事情实际上就是把服务id、方法id以及参数打包成一个消息发出去,等到其他服务器回过来消息之后,再回调到发起者的相应方法。

三年前,小说君在腾讯参与一款页游项目的开发。可能是因为历史遗留原因,该项目并没有采用RPC。因此作为逻辑狗,代码写起来非常蛋疼。

举个简单的例子,发送一个含有a和b两个参数的请求包这样的简单操作,应用层程序员需要先分配一个请求对象,然后人肉给a和b赋值,再手动调下序列化函数,最后发送。接收包时的操作类似。

如果该项目引入RPC,那发请求就是一行代码,跟调用普通函数的写法一样;收请求则是由框架自动回调一个signature即包参数的函数。应用层程序员根本不需要关注「包」、「序列化」、「反序列化」这些概念,少写不少重复代码。

当然,RPC也并不是尽善尽美,看过《Unix编程艺术》的同学可能对书中批评RPC的章节仍有印象,小说君简单总结一下:

  • RPC接口不具备自我描述性。

  • RPC太容易扩展,容易增加系统复杂度。

  • RPC透明性差,程序员无法直接获知某个RPC接口的调用成本。

  • RPC鼓励程序员视跨机调用为无成本行为。

简单地说,RPC受「人」的因素影响巨大。RPC和本地函数外观一模一样,RPC就会被无节操的程序员扩散在系统各处。

如果只是上层逻辑倒还好,可以通过一定的沙盒机制杜绝级联效应。 但是小说君最近在review新接手的框架时就发现,之前的维护者在框架层面不少主流程中用RPC绕来绕去,简直把后来人绕晕。

RPC本意是减轻程序员编码负担,但是如此一来,就完全违背了设计本意。

云风写过一篇博客是「RPC之恶」,其中也提到了一个滥用RPC的例子:系统库内置的sort函数往往需要用户传入comparer,有的程序员就会在comparer中调用RPC,甚至comparer的后续执行还需要RPC的远程返回结果。

这显然是不对的, 「流程」获取数据, 「算法」处理数据;而不是 「算法」执行过程中自行根据 「流程」获取数据,再自行处理数据。

不过,排除了 「人」的因素之后,RPC带来的收益还是相当明显的,特别对于游戏项目或者应用项目的后端开发来说,服务定义可控,流程可控,即使出现前述问题,要么是能较快定位到问题,要么是不至于给项目带来灾难性影响。

是否在项目中采用RPC,是一个见仁见智的抉择问题。本文接下来就聊聊RPC框架需要解决什么问题,以及如何设计RPC框架。

RPC框架需要解决什么问题?

我们看文章开头提到的三点:

  1. 分布式环境。

  2. 写起来就跟调用本地函数一样。

  3. 程序员无需关注与远程的交互细节。

接下来逐条展开。

第一点,分布式环境,也就是说,RPC框架需要帮程序员做好方法调用到数据的转换,然后再借助网络库发出去;接收侧的网络库收到后将数据推给接收侧的RPC框架,RPC框架再帮程序员做好数据到方法回调的转换。

流程很简单,网络库相关的实现可以参考服务端系列的第一篇文章「从零手写服务端框架」;方法调用与数据的互转就更简单了——想办法将文章开头小说君吐槽的人肉打解包逻辑自动化完成,再序列化即可。

序列化的方案通常有两种:一种是有一定自描述能力的,常见于protobuf、msgpack等第三方库;一种是基于纯数据流,通常是项目组自行维护的。

当然,一个完备的RPC框架理应可以透明替换序列化方案。

第二点, 写起来就跟调用本地函数一样。 这点决定了RPC框架设计的好坏。

RPC框架的核心设计意图就是让应用层程序员调用起来非常自然、不需要有太多包袱。魔兽世界以及网易很多游戏采用的类bigworld服务端框架,其RPC甚至还向上层程序员隐藏了客户端session切进程的细节。

本地函数可以大概分为两类:同步和异步。

对于异步函数,基本任何语言和平台都能做到RPC与本地函数写法一致,比如下面两行方法调用,分辨不出哪次调用需要发网络包。

remoteServiceDelegate.PostMessage(msg); 
localServiceDelegate.PostMessage(msg);

而同步函数的实现就会有些棘手,在有些语言或平台上甚至无法实现。

先看一个本地同步函数的调用例子:

var ret = localServiceDelegate.SendMessage(msg);

现在,我们把SendMessage改成一个需要网络通信的异步RPC调用。

如果想实现语义上跟本地函数版本一致,那语言或平台就需要有保存执行上下文的能力,等到 SendMessage的远端结果返回时,再恢复上下文,并把返回结果赋给ret。

在支持异步语法的语言/平台,这种语义可以原生支持,写成这样:

var ret = await localServiceDelegate.SendMessage(msg);  //支持async/await语义
var ret = yield localServiceDelegate.SendMessage(msg);  //支持yield语义

如果不支持异步语法,但是支持闭包的话,也没问题,可以这样写:

localServiceDelegate.SendMessage(msg, (ret) =>
{
});

如果闭包都不支持的话,就麻烦了。

前述两种,之所以接近本地函数的调用形式,是因为语义上有保证——异步数据返回时,执行上下文与异步请求发送时一致。

不支持闭包,如果想要保存现场,就需要人肉定义执行上下文结构——发请求时hold住相关环境,收到回应时取出相关环境,并回调注册的回调函数。

所以只是写应用层的话,小说君认为,如果采用闭包都不原生支持的语言,那应该已经离现代编程太远了。

第三点,程序员无需关注与远程交互的细节。

何谓交互细节?

回想一下,小说君在之前的几篇服务端文章中分别引入了 「 网关 」 、 「 消息队列 」 、 「 数据服务 」 、 「 分布式一致性设施 」 。

这些设施 「外部设施 」(比如第三方的SDK)有本质区别,每一种设施都专注建模一类特定问题并解决,因此小说君称其为 「基础设施抽象 」 。

与这些基础设施抽象打交道,就不免需要关注与其的交互细节。比如,与网关打交道需要指定组播的组id,与消息队列打交道需要指定频道,与数据服务打交道要指定的内容就更多了。

如果这些细节全部暴露在应用层的话,那应用层程序员的负担就会大大增加。 因此,RPC框架还应该解决这类问题——提供额外的中间层,让程序员认知统一。

我们用「服务」这个概念来统一认知。

对于同一个服务,调用方需要借助服务的委托(Delegate)发起服务提供的某条RPC;接收方则有该服务的对应实现(Implementation),RPC会回调Impl的对应方法。

不同的基础设施抽象有不同的设施层的协议,因此RPC框架还需要针对不同的协议定制适配器(Adaptor)。适配器这个概念(Concept)是面向RPC层定义的—— 对于 Implementation 来说,Adaptor是一个持续产出消息的流;对于 Delegate 来说,Adaptor是一个可以接受消息的传输器。

与此同时, 应用层不需要有统一的Adaptor概念,因此Adaptor可以向应用层提供特化的接口。

下面我们来看一些实现细节。

首先是RPC层的协议定义,简单分为两部分:

  • 一部分用来标识一次调用session,调用方分配sessionId,实现方处理完返回数据时带上sessionId,调用方就能回调之前注册的闭包,还原上下文,还可以实现超时管理。

  • 一部分用来做方法的dispatch。不论是用第三方序列化库还是自行维护,都至少需要序列化方法Id、参数等信息。

而至于整体的消息流,就是:

应用 -> RPC -> Adaptor -> 基础设施协议 -> Adaptor -> RPC -> 应用

其中,Adaptor与RPC层的关系满足下面几点:

  • RPC层与Adaptor层完全无关。 不同Adaptor需要针对RPC层提供相同接口。 两者互不关注具体实现。

  • 服务的 Delegate构 可以基于不同类型的Adaptor构造,可以向Adaptor发送消息, 可以 定制 服务特定的路由规则。

  • 服务的Implementation可以注册在不同的Adaptor上。

本来打算服务端系列先到此为止,结果写着写着发现只讲RPC就已经篇幅这么长了,而剩下还有大约一半内容,只能强行续一篇了。

RPC的实现本身非常容易,相信 动手做过的同学都很清楚。 如果协议中的方法dispatch部分基于文本实现,在 python、lua这种动态语言中,甚至连自动化工具都不需要,一百行代码就能搞定一个RPC库。

现在,我们所说的RPC就只是普通的远程方法调用,虽然对应用层完全隐藏了其他设施的协议细节,但是这样一来其他设施的强大特性我们也就无法利用了。

还有另一种与RPC平行的抽象来特化RPC的形式,这种抽象与RPC共同组成了应用层的开发规范。

下篇文章,我们来聊聊消息流的模型 定义,以及 节点间通信的pattern 定义

如果对文章有疑问或者对新话题有兴趣请务必直接留言回复!

服务端系列文章的链接,以及后续的主题(按顺序阅读更佳):

从零手写服务端框架

面向中间件的开发模式

如何快速搭建数据服务

面向微服务的服务端架构

以消息队列为中心的服务端架构

聊聊无状态服务

聊聊分布式锁

基于redis构建数据服务

RPC解决了什么问题(本篇)

聊聊消息流模型()

个人订阅号:gamedev101「说给开发游戏的你」,聊聊服务端,聊聊游戏开发。

RPC 解决了什么问题

原文  http://mp.weixin.qq.com/s?__biz=MzIwNDU2MTI4NQ==&mid=2247483772&idx=1&sn=ee3d6e3937dffb2d45d555ae753482ad&chksm=973f0f96a04886804f90e36e1e79408fe4bb666c0a6b8d1ecc17c40b74f761dcf3658aa64d74
正文到此结束
Loading...