大家好,我是goodspeed,现在是一名后端工程师。昵称比较奇怪哈,名字来自烂片之王尼古拉斯凯奇早期电影,意思是祝你好运。
这是我的公号二维码,每年都会更新一些文章?,大家有兴趣可以关注一下。
今天分享的题目是rpc的前世今生。主要内容是rpc发展历史,以及在在这几十年中rpc的进化过程。如果看完这个分享,能理解rpc为什么会进化成现在这个样子有一个认识,那我的分享就算是成功了。
开始之前我们先考虑一个问题,这个问题可以先在脑子里思考一下,接下来我们开始分享。
今天的分享主要有三部分
远程过程调用(Remote Procedure Call,RPC)是一种允许两个实体通过通用请求/响应机制的通信通道进行通信的设计范例。RPC 的定义在过去三十年中发生了重大的变化和演变,因此 这里RPC 范式是一个广义的分类术语,指的是过去四十年中出现的所有 RPC 式系统。RPC 的定义经过几十年的发展。它已经从一个简单的客户端-服务器设计转移到一组相互连接的服务。虽然最初的 RPC 实现被设计为将计算外包给分布式系统中的服务器的工具,但 RPC 经过多年的发展,已经构建了一个与语言无关的应用程序生态系统。RPC 范式已经成为创建真正革命性的分布式系统的驱动力的一部分,并且在不同系统之间产生了各种通信方案和协议。
最简单的 RPC 实现如图1所示。在这种情况下,客户端(或调用方)和服务器(或被调用方)被一个物理网络分开。系统的主要组件是客户端例程/程序、客户端存根、服务器例程/程序、服务器存根和网络例程。存根是一个小程序,通常用作较大程序(Rouse,n.d.)的替代程序(或接口)。客户端存根向客户端例程公开服务器例程提供的功能,而服务器存根向服务器例程提供类似于客户端的程序(Taing,n.d.)。客户端存根从客户端程序获取输入参数并返回结果,而服务器存根向服务器程序提供输入参数并获取结果。客户端程序只能与客户端存根交互,后者为客户端提供远程服务器的接口。这个存根还序列化客户端例程发送到存根的输入参数。类似地,服务器存根为服务器例程提供客户端接口,并处理发送到客户端的数据序列化。
当客户端例程执行远程过程时,它调用客户端存根,该存根序列化输入参数。这个序列化数据使用 OS 网络例程(TCP/IP)(Taing,n.d.)发送到服务器。然后,服务器存根将数据反序列化,并使用给定的参数提供给服务器例程。来自服务器例程的返回值再次序列化,并通过网络发送回客户端,在那里客户端存根对其进行反序列化,并显示给客户端例程。这个远程过程通常对客户端例程隐藏,并作为本地过程显示给客户端。RPC 服务还需要一个发现服务/主机解析机制来引导客户端和服务器之间的通信。
RPC 的发展历史我把它分为三个部分,上升,下降,再上升。可以从图中看到,从1969年阿帕网建立到1984年RPC 第一次实现,是RPC 发展的上升期,之后到1994年 A Note on Distributed Computing 发表是RPC 的成熟下降阶段,之后一直到现在是RPC的再次上升期。
接下来我们详细看一下RPC 的发展历史。
1996年,美国国防部高级研究计划管理局(ARPA全称:Advanced Research Projects Agency)开始建立一个命名为ARPAnet的网络。最开始只有4个结点,这是现代互联网的前身。
过程调用最早可以追溯到 Jon Postel 和 Jim White 在1974 年发表的 Procedure Call Protocol Documents Version 2(RFC674)。这个协议试图定义一种通用的方法,用于解决 NSW 项目中多个计算节点通信的问题。
协议发表后,引起了非常大的争议,1975年,RFC674的注释篇RFC684 发布。
RFC 684 不是一个独立的协议, 主要对 RFC674 的争议进行讨论。讨论内容可以总结为以下几点:
- RFC674 认为过程调用应该是一个原语操作,它应该在操作系统底层进行操作
- 本地调用和远程调用是不同的,远程调用可能会发生故障,并且发生故障后可能无法恢复。
- 异步消息传递,或者显示的声明什么时候需要同步等待消息返回应该是一个更好的模型。
从这几点出发,关于这个编程范型的担忧成了RPC40多年历史中一个永恒的话题,即:
- 故障或错误后怎么恢复?重试、抛出异常?
- 顺序操作非常困难。比如一系列同步请求,如果其中某些请求失败,怎么保证错误的请求重新执行,以及请求还是顺序的?
- RPC 请求是同步模型,方法被调用后会等待响应,但是由于请求是同步的,在系统负载高时如果希望优先响应优先级高的请求则变成了非常困难的事情。
此时的协议还是基于阿帕网(ARPANET),互联网还没有出现,已经在讨论分布式系统间调用的问题了。分布式系统竟然比互联网的历史还久,真的是有点出乎意料呢。
RFC 707 概括了 RFC 684 的思想,并讨论了诸如 TELNET 和 FTP 等服务的资源共享问题,这些服务中的每一个都提供了与之交互的不同接口,这就要求操作员知道与该服务交互的具体协议。针对这种问题,作者提出了一个新的想法:与其需要知道远程计算机上所有可用的命令和协议,我们能否定义一个通用的接受参数并遵循调用/响应模型的接口来执行一个远程过程。
1983年1月1日,ARPA网将其网络核心协议由网络控制程序改变为 TCP/IP 协议,互联网的种子开始发芽。
Birrell 和 Nelson 在 1984 发表于 ACM Transactions on Computer Systems 的论文《Implementing remote procedure calls》对 RPC 做了经典的诠释。RPC 是指计算机 A 上的进程,调用另外一台计算机 B 上的进程,其中 A 上的调用进程被挂起,而 B 上的被调用进程开始执行,当值返回给 A 时,A 进程继续执行。调用方可以通过使用参数将信息传送给被调用方,而后可以通过传回的结果得到信息。而这一过程,对于开发人员来说是透明的。之后的几年RPC一直被认为是建立分布式操作系统的最合适的范式。
上图是论文中的rpc架构图,可以看到user,uset-sub和其中一个RPCRuntime的实例在调用者机器上执行;server,server-sub和另外一个RPCRuntime实例在被调用者机器上执行。执行流程和现代rpc基本一致,当user发起远程调用时,其实是执行了一个完全正常的本地调用,而这个调用会去调用user-stub中相应的程序。user-stub负责将目标程序的规范和参数放置在一个或多个包中(打包),并请求RPCRuntime将这些包可靠地传输给被调用者机器。一旦接收到这些包,被调用者机器上的RPCRuntime就这些包传送给server-stub。server-stub将它们解包,像是执行一个完全正常的本地调用一样,该本地调用会调用server中对应的程序。与此同时,调用者机器上的调用进程将被挂起,并等待结果包的返回。当server中的调用完成时,它将结果返回给user-stub打包,然后结果包将由RPCRuntime再传送回给调用者机器上挂起的进程(RCPCRuntime负责重传,确认,数据包路由和加密)。这些包将被user-stub解包并返回给user。除去多机器间机器绑定或者通信失败的影响,调用就仿佛user直接在server上调用程序一样。确实是这样,如果user和server的代码放置在同一个机器上,并被直接绑定在一起(无需stub),程序将仍能工作。
1987年,Tanenbaum 和 Renesse发表文章《A Critique of the Remote Procedure Call Paradigm》。在文章的开始,作者首先肯定了rpc的成绩,夸赞rpc是解决分布式问题最好的解决方法。然后话锋一转,开始讨论RPC 模型的概念问题、实现技术问题、客户端和服务端崩溃后的处理问题、不同系统间的问题以及性能等多方面的问题,并对存在的问题进行了分析。
文章指出,
一个通用的范例不应该要求程序员将自己限制在所选择的编程语言的一个子集中,或者强迫他们采用某种编程风格(例如,不要一刀切的使用指针,因为 RPC 不能处理它们)
同时还讨论了以下几个问题:
两军问题
网络是不可靠的,无法保证数据可以100%无误的通过网络传递。
参数问题
参数编组,参数顺序,参数传递等。特别是指针类型的参数传递。
全局变量
既然是RPC 可以像本地调用一样使用,那么全局变量是否可以通用?
性能问题
异常处理
通常当主程序调用过程时,如果代码是正确的,那么该过程最终将返回给调用者。如果机器崩溃,主程序和程序都会死亡,整个程序必须重新运行。因此,基本上有两种操作模式: 整个程序工作或整个程序失败。 RPC 引入了另一种故障模式: 客户端工作正常,但服务器崩溃。如果一个主程序调用一个过程,但是没有响应,那么应该怎么做呢?在某些系统中,客户端会永远挂起。 另一种可能是让客户端存根在向服务器发送消息时启动计时器。如果在某个时间间隔之后没有响应,它会一次又一次地尝试。在 n 次重试之后,依然失败那么则返回一个错误码标识服务不可用。
幂等问题
时间走到1988年
Sun 公司是第一个提供商业化 RPC 库和 RPC 编译器。在1980年代中期, Sun 计算机提供 RPC,并在 Sun Network File System(NFS) 得到支持。该协议被主要以 Sun 和 AT&T 为首的 Open Network Computing (开放网络计算)作为一个标准来推动。最终sun成功了,sunrpc 成了第一个rpc的标准。
1989年,蒂姆·伯纳斯-李发明了万维网。第二年9月,开发了第一个网页浏览器。到1990年圣诞节,蒂姆·伯纳斯-李创建运行万维网所需的所有工具:超文本传输协议(HTTP)、超文本标记语言(HTML)、第一个网页浏览器、第一个网页服务器和第一个网站,实现了超文本传输协议客户端与服务器的第一次通讯。他也因此而获得了2016年的图灵奖,还得到了爵士爵位。
到1995年,互联网在美国已完全商业化。
CORBA(Common Object Request Broker Architecture) 是面向对象语言的一个抽象,发布与1991年,由 C 开发,它允许你在不同的语言和不同的机器上运行的不同的地址空间之间进行通信。CORBA 依赖于使用接口定义语言(IDL)来指定远程对象类的接口; 这种 IDL 用于生成远程系统对象接口在本地机器上的接口。这些 IDL 将用于生成 IDL 提供的抽象接口与 C 和 Java 等语言的实际实现之间的映射。
CORBA 试图为应用程序开发人员提供几个好处:语言独立性、操作系统独立性、体系结构独立性、通过 IDL 中的抽象类型映射到这些类型的机器和语言特定实现的静态类型,以及对象传输,其中对象可以通过不同机器之间的连接进行迁移。CORBA 的承诺是,通过使用映射,远程调用可以作为本地调用出现,分布式系统相关的异常可以映射到本地异常,并由本地异常处理机制处理。
1994年12月,CORBA 2.0 就已经发布规范,该规范希望能够解决不同厂商根据COBRA规范所开发的产品“互联互不通”的严重问题,但直到1997年,Corba2.0 才正式发布,但是最后还是失败了。至于COBRA失败的原因,COBRA阵营的技术大牛、COBRA技术的推动者,即后来加入反COBRA阵营的Michi Henning,在他的《The rise and fall of CORBA》书里做了如下深刻的总结。
- 规范巨大而复杂:许多特性都未曾被实现,甚至概念性的证明都没有做过;有些技术特性根本不可能实现,即使实现,也无法提供可移植性。
- CORBA学习曲线陡峭:平台的学习曲线陡峭,技术复杂,不容易正确使用,这些因素导致开发周期长、易出错。早期的实现常常充满Bug并且缺乏有质量的文档,有经验的CORBA程序员稀缺。
- 编程开发过于复杂:有经验的CORBA开发者发现编写实用的CORBA应用程序相当困难。许多API都很复杂、不一致,甚至让人感觉神秘,使得开发者必须关注许多细节问题。相比之下,组件模型的简单性,例如同时代的EJB,使得编程简单很多。
- 费用昂贵:使用商用CORBA产品时,开发者一般都需要花费几千美元购买开发者License,此外,部署CORBA产品与部署Oracle数据库一样,还需要客户支付企业License费用,而且这个费用很可能与部署在CORBA平台上的应用数量挂钩,因此对很多潜在的客户来说,CORBA这样的平台太昂贵了。
- Sun与Java成为COBRA最大的竞争对手:商业公司转向了Sun的Java与新兴的Web,并且开始构建基于Web浏览器、Java和EJB的电子商务基础设施。
- XML技术的兴起加速了COBRA的没落:20世纪90年代后期,XML成为计算机工业新的银弹,几乎所有定义为XML的东西都是好的。在放弃了DCOM之后,微软并没有把电子商务市场留给竞争对手,没有再参与一场不可能打赢的战争,而是使用XML开辟了新的战场。
1994年,Jim Waldo 等人发表了一篇 名为 《A Note on Distributed Computing》的论文。 这篇论文详细讨论了为什么 RPC 模型扩展到对象,是有很大问题的。
在这篇论文中,作者认为忽视本地和分布式计算之前的差异是很危险的,同时它还讨论了一个统一的对象视图,并列举了在 RPC 中将这些对象划分为分布式计算的4个主要问题: 通信延迟、解决空间分离、部分故障和并发问题(由于通过两个并发的客户端请求访问同一个远程对象而导致)。这些问题中的大多数(除了部分故障)都与分布式计算本身有着内在的联系,但是对于 RPC 系统来说,部分故障即意味着 RPC 系统并不总是可用的。
同时,作者也认为分布式计算的难题不在于如何在线上或者线下进行操作,并且每隔10年,我们就会遇到试图统一本地计算和远程计算的观点的问题,并且每次都会遇到同样的问题:远程计算和本地计算是不同的。
而且最关键的问题不是“你能让远程方法调用看起来像本地方法调用吗?而是使远程方法调用与本地方法调用相同的代价是什么?
到这里为止我们看到针对RPC 的讨论基本都是在讨论设计、实现、面向对象、性能、分布式问题如何解决。有一点好像被忽略了,那就是易用性。为什么呢?是因为当时的程序员喜欢复杂的技术么?
我以前老大有一次分享的时候说,他认为并不是所有的开发者都是合格的程序员,合格的程序员应该是像林纳斯、丹尼尔、蒂姆那样,尝试改变世界并且为之努力的人。互联网早期,开发者数量较少,程序员是一个相对小众精英的团体,这种程序员占得比例也大,协议制定的时候更多考虑的也是如何压榨计算机性能,易用性可能也不在第一优先级范围内。
而到了90年代后期,互联网已经开始普及,随着web 开发的兴起,开发者也以指数的速度增长,这时开发框架就不仅仅要考虑小部分人的使用体验而是要照顾大多数人的使用体验了。
1996年,HTTP1.0 发布。
1998 年 XML 1.0 发布,被 W3C (World Wide Web Consortium) 推荐为标准的描述语言。同年,微软和DevelopMentor发布SOAP(Simple Object Access Protocol),随后提交给W3C作为标准。SOAP是一个严格定义的信息交换协议,使用XML作为RPC新的对象序列化机制,用于在Web Service中把远程调用和返回封装成机器可读的格式化数据。
可以看到soap 中的s 是simple的意思,易用性已经开始成为关键指标了。
不过SOAP也有很多不足:
- 效率低。因为报文基于XML,报文内容除了数据以外,还有很多荣誉用在格式的定义上,并且对于XML的序列化和反序列化解析速度也慢。
- 协议WSDL 复杂,程序员不友好。
针对SOAP速度慢的特点,有两种解决方案:
- 使用简单的文本格式传递,比如JSON
- 采用二进制的格式传递,比如 Protocol Buffers 或 Avro。
可以看到自90年代后期进入了web 开发的时代,web1.0、web2.0、web3.0 相继出现。以 http 为基础的请求/响应方案(XML、REST) 开始流行并占领了大部分的市场。RPC也开逐渐被开发者抛弃,进入了沉默期。
当然,RPC 并没有消失,而是在特定的领域继续生长。比如:Sun 微系统的网络文件系统 (NFS) 就是建立在 RPC 之上,是最早获得普及的分布式文件系统之一。
而随着互联网的指数扩张,微服务架构开始成了业界的“银弹”,分布式系统开始变的无处不在,基于HTTP的RESTful的缺点开始放大:
- 只支持请求/响应方式的通信
- 单个请求中获取多个资源具有挑战性
- 有时很难将更多操作映射到HTTP动词
- 基于JSON或者XML 的消息冗余严重,性能底下。
而天生就是为分布式计算出现的RPC也开始重新走入开发者的视野。
2008年,Vinoski 在他的论文中提出了我们开头的提问:“开发者的便利性真的比正确性、可扩展性、性能、关注点分离、可扩展性和偶然复杂度更重要吗?”
现在我们再来考虑一下开头的那个问题,易用性真的这么重要吗?
答案可能是是的!
2008年,Google 开源Protocol Buffers,Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
同年,Facebook 开源 thrift, Thrift 是一个跨语言的服务部署框架,最初由Facebook于2007年开发,2008年进入Apache开源项目。Thrift通过一个中间语言(IDL, 接口定义语言)来定义RPC的接口和数据类型,然后通过一个编译器生成不同语言的代码(目前支持C ,Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk和OCaml),并由生成的代码负责RPC协议层和传输层的实现。
Thrift 和 Protocol Buffer 不同,它不仅仅是一个数据序列化工具,而是一个完整的RPC 框架。另一个不同点在于,Protobuf 标准化了单一的二进制编码方式,但Thrift 则包含了多种不同的序列化方式(Thirft 称之为协议)。
Avro是一个基于二进制数据传输高性能的中间件,在2009年成为Hadoop 中的一个子项目,并与2015年脱离Hadoop,加入Apache成为一个独立的项目。
Avro 同样支持跨编程语言实现(C, C , C#,Java, Python, Ruby, PHP),Avro 提供着与诸如 Thrift 和 Protocol Buffers 等系统相似的功能
Avro 和动态语言结合后,读/写数据文件和使用 RPC 协议都不需要生成代码,而代码生成作为一种可选的优化只需要在静态类型语言中实现。
可以看到的是,avro 相对pb 和 thrift 来说更简单一点。
可以看到自2000年之后,RPC 的发展开始向用户的方易用性的方向发展。像 Google 的 gRPC 和 Twitter 的 Finagle 这样的框架不断的降低了构建应用程序的复杂性,也将 RPC 带给更多的用户。
让我们再来回顾一下文章开头提到的问题:
“开发者的便利性真的比正确性、可扩展性、性能、关注点分离、可扩展性和偶然复杂度更重要吗?”