当前位置: 首页 > 科技观察

RPC概念模型与实现分析

时间:2023-03-12 09:13:11 科技观察

当今流行分布式应用、云计算、微服务。作为其技术的基石之一,您对RPC了解多少?一篇关于RPC的技术总结文章,算5k+字,略长,可能不适合碎片化闲暇阅读,可以先收藏再仔细阅读:)全文目录如下:定义origintarget分类结构模型拆解组件实现导出导入协议编解码消息头消息体传输执行异常总结参考二几年前写过两篇关于RPC的文章,现在回头看发现结构和逻辑有点乱,而且我将它重新整理成一篇专门的文章。想了解RPC原理的同学可以看看。在近几年的项目中,面向服务和面向微服务逐渐成为中大型分布式系统架构的主流方式,而RPC在其中起到了关键作用。在平时的日常开发中,我们都在或隐或显地使用RPC。一些刚入行的程序员会觉得RPC比较神秘,而一些有多年RPC使用经验的程序员对RPC的使用经验丰富,但也有一些不明白其中的原理。不要太多。缺乏对原理层面的理解,往往会导致开发中出现一些误操作。定义RPC的全称是RemoteProcedureCall,是一种进程间通信的方式。它允许程序调用另一个地址空间(通常是共享网络上的另一台机器)中的过程或函数,而无需程序员显式编码远程调用的细节。也就是说,无论程序员调用的是本地函数还是远程函数,编写的调用代码本质上是一样的。起源RPC的概念术语是由BruceJayNelson(参考文献[1])在1980年代提出的。这里我们回到最初开发RPC的动机是什么?在Nelson的论文ImplementingRemoteProcedureCalls(参考[2])中,他提到了几点:简单:RPC概念的语义非常清晰和简单,使分布式计算的建立变得更加容易。高效:过程调用看似简单高效。概述:在单机计算中,“过程”往往是不同算法部分之间最重要的通信机制。通俗地说,一般程序员对本地过程调用都很熟悉,那么如果我们把RPC做成与本地调用完全相似,就会更容易接受和使用,没有障碍。纳尔逊的论文发表于30年前,他的观点在今天看来颇具远见。我们今天使用的RPC框架基本实现了这个目的。TargetRPC的主要目标是让构建分布式计算(应用程序)变得更加容易,同时又不失本地调用的语义简洁性,同时提供强大的远程调用能力。为了实现这个目标,RPC框架需要提供一种透明的调用机制,让用户不需要明确区分本地调用和远程调用。RPC调用分为两种:同步调用:客户端等待调用完成,获取执行结果。异步调用:客户端调用后,不需要等待执行结果返回,仍然可以通过回调通知等方式获取返回结果。如果客户端不关心调用的返回结果,就变成了单向异步调用,单向调用不需要返回结果。异步和同步的区别在于是否等待服务器执行并返回结果。结构下面我们从理论模型到实际组件一步步讲解RPC的结构。该模型首先在Nelson的论文中指出,实现RPC的程序包括五个理论模型部分:UserUser-stubRPCRuntimeServer-stubServer这五个部分之间的关??系如下图所示:这里的User是Client。当User要进行远程调用时,实际上是在本地调用User-stub。User-stub负责将调用的接口、方法和参数通过约定的协议规范进行编码,通过本地RPCRuntime实例传输到远程实例。远程RPCRuntime实例收到请求后,将其发送给Server-stub进行解码,然后向本地Server发起调用,并将调用结果返回给User。拆解上面给出了一个比较粗粒度的RPC实现理论模型的概念结构。这里我们进一步细化它应该由哪些组件组成,如下图所示。RPC服务器使用RpcServer导出(export)远程接口方法,client使用RpcClient导入(import)远程接口方法。客户端像调用本地方法一样调用远程接口方法,RPC框架提供接口的代理实现,实际调用会委托给代理RpcProxy。代理封装调用信息并将调用转发给RpcInvoker以实际执行。客户端的RpcInvoker通过连接器RpcConnector与服务端维护通道RpcChannel,并使用RpcProtocol进行协议编码(encode),将编码后的请求报文通过通道发送给服务端。RPC服务器接收方RpcAcceptor接收到客户端的调用请求,同样使用RpcProtocol进行协议解码(decode)。解码后的调用信息传递给RpcProcessor来控制和处理调用过程,最终将调用委托给RpcInvoker去真正执行并返回调用结果。组件上面我们进一步拆解了RPC实现结构的各个组件。下面我们详细描述各个组件的职责划分。1.RpcServer负责导出(export)远程接口2.RpcClient负责导入(import)远程接口的代理实现3.RpcProxy远程接口的代理实现4.RpcInvokerclient:负责编码调用信息并向服务端发送调用请求并等待调用结果返回给服务端:负责调用服务端接口的具体实现并返回调用结果5.RpcProtocol负责协议编码/解码6.RpcConnector负责维护客户端和服务端的连接通道,向服务端发送数据7.RpcAcceptor负责接收客户端8.RpcProcessor`负责控制服务端的调用过程,包括管理调用线程池,超时时间等9.RpcChannel数据传输通道实现了Nelson论文中给出的概念模型,即h也成为了大家以后参考模板的标准。十多年前,我初接触分布式计算时使用的CORBAR(参考[3])实现结构与此基本类似。为了解决异构平台的RPC,CORBAR使用IDL(InterfaceDefinitionLanguage)来定义远程接口,并将其映射到特定的平台语言。后来大部分跨语言平台的RPC基本都采用了这种方式,比如大家熟悉的WebService(SOAP),还有近年开源的Thrift。大部分通过IDL定义,并提供工具映射生成不同语言平台的User-stub和Server-stub,通过框架库提供RPCRuntime支持。但是,似乎每一个不同的RPC框架都定义了自己不同的IDL格式,进一步增加了程序员的学习成本。虽然WebService试图建立行业标准,但流氓标准规范复杂且效率低下,否则就不需要更高效的RPC框架,如Thrift。IDL是跨平台语言实现RPC的最后选择,解决更广泛的问题自然会导致更复杂的解决方案。对于同平台的RPC,显然不需要创建中间语言,比如Java原生的RMI,对于Java程序员来说更加直接简单,降低了使用它的学习成本。在对上述组件进行进一步拆解和职责划分后,下面以Java平台上RPC框架概念模型的实现为例,详细分析实现中需要考虑的因素。ExportExport是指暴露远程接口的意思。只有导出的接口才能用于远程调用,未导出的接口不能。Java中导出接口的代码片段可能是这样的:我们可以导出整个接口,也可以更细粒度的只导出接口中的部分方法,如下:Java中还有一个特殊的调用,就是多态,也就是一个接口可能有多个实现,那么远程调用的时候调用哪个呢?这种本地调用的语义是通过JVM提供的引用多态隐式实现的,所以对于RPC来说,跨进程调用是无法隐式实现的。如果前面的DemoService接口有两个实现,那么在导出接口的时候需要特别标注不同的实现,如下:上面的demo2是另外一个实现,我们标注为demo2进行导出,那么远程调用也需要通过这个标记调用正确的实现类解决了多态调用的语义。ImportImport与export相比,客户端代码必须获得远程接口的方法或过程定义才能发起调用。目前大多数跨语言平台的RPC框架都是使用代码生成器根据IDL定义生成User-stub代码。这样,真正的导入过程在编译时由代码生成器完成。我用过的一些跨语言平台的RPC框架如CORBAR、WebService、ICE、Thrift都是这种方式。代码生成方式是跨语言平台RPC框架的必然选择,而对于同语言平台的RPC,可以通过共享接口定义来实现。Java中引入接口的代码片段可能如下:import是Java中的关键字,所以在代码片段中我们使用refer来表达引入接口的意思。这里的import方法本质上是一种代码生成技术,只不过是在运行时生成的,看起来比静态编译时的代码生成更简洁。Java至少提供了两种技术来提供动态代码生成,一种是JDK动态代理,一种是字节码生成。动态代理比字节码生成使用起来更方便,但是动态代理方式的性能不如直接字节码生成,而且字节码生成在代码可读性上差很多。权衡两者,作为底层通用框架,个人更倾向于选择性能优先。Protocol协议是指RPC调用在网络传输中约定的数据封装方式,包括三部分:codec、消息头和消息体。Codec客户端代理在发起呼叫前需要对呼叫信息进行编码,这就需要考虑需要对哪些信息进行编码,以何种格式传输给服务器,让服务器完成呼叫。为了效率,编码的信息越少越好(传输的数据越少),编码规则越简单越好(执行效率高)。我们先来看一下需要对哪些信息进行编码:调用编码1.接口方法包括接口名和方法名2.方法参数包括参数类型和参数值3.调用属性包括调用属性信息,比如调用附加隐式参数,调用超时时间等返回码1.返回result接口方法中定义的返回值2.返回码异常返回码3.返回异常信息调用异常信息消息头除了上面必要的调用信息,我们可能还需要一些meta为方便起见的信息程序编解码器和未来可能的扩展。这样我们的编码消息就分为两部分,一部分是元信息,一部分是调用的必要信息。如果我们设计一个RPC协议消息,我们把元信息放在协议消息头中,把必要的信息放在协议消息体中。下面给出了一个概念性的RPC协议消息头设计格式:Magicprotocolmagicnumber,headersizeprotocolheaderlengthfordecoding,version协议version用于扩展设计,stmessagebodyserializationtypehbheartbeatmessagemarkforcompatibilitydesign,Theowone-waymessageflag是为长连接传输层的心跳设计的,没有设置rpresponsemessageflag。默认为请求消息状态代码。响应消息状态代码保留用于字节对齐。messageidmessageidbodysize消息正文长度使用序列化编码,常用的序列化方式有以下几种:xml如webservieSOAPjson如JSON-RPCbinary如thrift;序列化方法。我们关心序列化的三个方面:效率:序列化和反序列化的效率,越快越好。Length:序列化后的字节长度,越小越好。Compatibility:序列化和反序列化的兼容性,如果接口参数对象添加字段,是否兼容。以上三点有时鱼和熊掌不可兼得,涉及到序列化库的具体实现细节,本文不再深入分析。传输协议经过编码后,自然是将编码后的RPC请求报文传输给服务器,服务器执行后返回结果报文或确认报文给客户端。RPC的应用场景本质是一种可靠的请求-响应消息流,类似于HTTP。因此,选择长连接方式的TCP协议效率更高。与HTTP不同的是,我们在协议层面为每条消息定义了一个唯一的id,这样更容易重用连接。既然使用了长连接,那么第一个问题就是客户端和服务端之间需要多少个连接?其实单连接和多连接没有区别。对于小数据传输的应用,单连接基本够用了。单连接和多连接最大的区别在于每个连接都有自己私有的发送和接收缓冲区,所以当传输大量数据时,会分散在不同的连接缓冲区中,以获得更好的吞吐效率。因此,如果你的数据传输量不足以让单个连接的缓冲区保持饱和,那么使用多个连接不会产生明显的改善,反而会增加连接管理的开销。'连接由客户端建立和维护。如果客户端和服务器直连,一般不会中断连接(当然物理链路故障除外)。如果客户端和服务器是通过一些负载转移设备连接的,当连接一段时间不活动时,连接可能会被这些中间设备中断。为了保持连接,需要为每个连接周期性发送心跳数据,以保持连接不中断。心跳消息是RPC框架库使用的内部消息。之前的协议头结构中还有一个特殊的心跳位,用来标记心跳消息,对业务应用是透明的。客户端存根所做的只是对消息进行编码并将其传输到服务器,实际的调用过程发生在服务器上。Server-sidestub从上面的结构拆解来看,我们细分了两个组件,RpcProcessor和RpcInvoker,一个负责控制调用过程,一个负责实际调用。这里我们还是以这两个组件在Java中的实现为例,分析一下它们需要做什么。Java中实现代码的动态接口调用目前一般都是通过反射来调用。除了原生JDK自带的反射外,一些第三方库也提供了性能更好的反射调用,所以RpcInvoker封装了反射调用的实现细节。调用过程的控制需要考虑哪些因素,RpcProcessor需要提供什么样的调用控制服务?这里有几点可以启发思考:1.效率提升每个请求都应该尽快执行,所以我们不能为每个请求创建一个线程。执行需要线程池服务。2.资源隔离当我们导出多个远程接口时,如何避免单个接口调用占用所有线程资源导致其他接口阻塞。3.超时控制当一个接口执行缓慢,客户端已经超时放弃等待,此时服务端的线程继续执行是没有意义的。异常无论RPC如何努力使远程调用看起来像本地调用,它们仍然有很大的不同,并且有一些异常是您在本地调用时永远不会遇到的。在讲异常处理之前,我们先比较一下本地调用和RPC调用的一些区别:本地调用会执行,但远程调用不一定会执行,而且可能因为网络原因导致调用消息没有发送到服务器。本地调用只会抛出接口声明的异常,而远程调用在RPC框架运行时也会跑出其他异常。本地调用和远程调用的性能可能会有很大差异,具体取决于RPC固有开销的权重。正是这些差异,在使用RPC时需要多加考虑。当调用远程接口抛出异常时,异常可能是业务异常,也可能是RPC框架抛出的运行时异常(如:网络中断等)。业务异常表示服务端已经执行了调用,可能由于某些原因没有正常执行,而RPC运行时异常则可能表示服务端根本没有执行,调用者的异常处理策略自然需要杰出的。由于RPC的固有消耗比本地调用高几个数量级,所以本地调用的固有消耗是纳秒级的,而RPC的固有消耗是毫秒级的。那么对于太轻的计算任务,不适合导出远程接口由独立进程服务。只有当计算任务花费的时间远高于RPC的固有消耗时,才值得导出来为远程接口提供服务。小结至此我们提出了RPC实现的概念框架,分析了一些需要详细考虑的实现细节。不管RPC的概念多么优雅,但“草丛中还藏着几条蛇”,只有深入理解RPC的本质才能更好地应用。看到这里的同学可能会疑惑,是不是真的可以按照这个概念模型和实现分析来开发实现一个RPC框架库呢?这个问题我可以肯定的回答,确实有可能。由于本人基于此模型开发实现了一个最小的RPC框架库来学习验证,所以相关代码放在了Github上,有兴趣的同学可以自行阅读。这是我自己的开源项目,用于实验学习和验证。地址为https://github.com/mindwind/craft-atom,其中craft-atom-rpc是按照该模型实现的微型RPC框架库。代码量远少于工业级使用的RPC框架库,方便阅读和学习。***,看到这里的小伙伴一定是渴望学习的,谢谢你的宝贵时间,让我的写作更有意义:)。【本文为专栏作家胡风原创文章,转载请联系作者获得授权】点此阅读该作者更多好文