「解析 RPC 的本质。」
《深入篇》我们主要围绕 RPC 的功能目标和实现考量去展开,一个基本的 RPC 框架应该提供什么功能,满足什么要求以及如何去实现它?
RPC 功能目标
RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。 为实现该目标,RPC 框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用, 在前文《浅出篇》中给出了一种实现结构,基于 stub 的结构来实现。 下面我们将具体细化 stub 结构的实现。
RPC 调用分类
RPC 调用分以下两种:
- 同步调用
客户方等待调用执行完成并返回结果。 - 异步调用
客户方调用后不用等待执行结果返回,但依然可以通过回调通知等方式获取返回结果。 若客户方不关心调用返回结果,则变成单向异步调用,单向调用不用返回结果。
异步和同步的区分在于是否等待服务端执行完成并返回结果。
RPC 结构拆解
《浅出篇》给出了一个比较粗粒度的 RPC 实现概念结构,这里我们进一步细化它应该由哪些组件构成,如下图所示。
RPC 服务方通过 RpcServer
去导出(export)远程接口方法,而客户方通过 RpcClient
去引入(import)远程接口方法。 客户方像调用本地方法一样去调用远程接口方法,RPC 框架提供接口的代理实现,实际的调用将委托给代理 RpcProxy
。 代理封装调用信息并将调用转交给 RpcInvoker
去实际执行。 在客户端的 RpcInvoker
通过连接器 RpcConnector
去维持与服务端的通道 RpcChannel
, 并使用 RpcProtocol
执行协议编码(encode)并将编码后的请求消息通过通道发送给服务方。
RPC 服务端接收器 RpcAcceptor
接收客户端的调用请求,同样使用 RpcProtocol
执行协议解码(decode)。 解码后的调用信息传递给 RpcProcessor
去控制处理调用过程,最后再委托调用给 RpcInvoker
去实际执行并返回调用结果。
RPC 组件职责
上面我们进一步拆解了 RPC 实现结构的各个组件组成部分,下面我们详细说明下每个组件的职责划分。
-
RpcServer
负责导出(export)远程接口 -
RpcClient
负责导入(import)远程接口的代理实现 -
RpcProxy
远程接口的代理实现 -
RpcInvoker
客户方实现:负责编码调用信息和发送调用请求到服务方并等待调用结果返回
服务方实现:负责调用服务端接口的具体实现并返回调用结果 -
RpcProtocol
负责协议编/解码 -
RpcConnector
负责维持客户方和服务方的连接通道和发送数据到服务方 -
RpcAcceptor
负责接收客户方请求并返回请求结果 -
RpcProcessor
负责在服务方控制调用过程,包括管理调用线程池、超时时间等 -
RpcChannel
数据传输通道
RPC 实现分析
在进一步拆解了组件并划分了职责之后,这里以在 java 平台实现该 RPC 框架概念模型为例,详细分析下实现中需要考虑的因素。
导出远程接口
导出远程接口的意思是指只有导出的接口可以供远程调用,而未导出的接口则不能。 在 java 中导出接口的代码片段可能如下:
DemoService demo = new ...;
RpcServer server = new ...;
server.export(DemoService.class, demo, options);
我们可以导出整个接口,也可以更细粒度一点只导出接口中的某些方法,如:
// 只导出 DemoService 中签名为 hi(String s) 的方法
server.export(DemoService.class, demo, "hi", new Class<?>[] { String.class }, options);
java 中还有一种比较特殊的调用就是多态,也就是一个接口可能有多个实现,那么远程调用时到底调用哪个? 这个本地调用的语义是通过 jvm 提供的引用多态性隐式实现的,那么对于 RPC 来说跨进程的调用就没法隐式实现了。 如果前面 DemoService 接口有 2 个实现,那么在导出接口时就需要特殊标记不同的实现,如:
DemoService demo = new ...;
DemoService demo2 = new ...;
RpcServer server = new ...;
server.export(DemoService.class, demo, options);
server.export("demo2", DemoService.class, demo2, options);
上面 demo2 是另一个实现,我们标记为 demo2 来导出, 那么远程调用时也需要传递该标记才能调用到正确的实现类,这样就解决了多态调用的语义。
导入远程接口与客户端代理
导入相对于导出远程接口,客户端代码为了能够发起调用必须要获得远程接口的方法或过程定义。 目前,大部分跨语言平台 RPC 框架采用根据 IDL 定义通过 code generator 去生成 stub 代码, 这种方式下实际导入的过程就是通过代码生成器在编译期完成的。 我所使用过的一些跨语言平台 RPC 框架如 CORBAR、WebService、ICE、Thrift 均是此类方式。
代码生成的方式对跨语言平台 RPC 框架而言是必然的选择,而对于同一语言平台的 RPC 则可以通过共享接口定义来实现。 在 java 中导入接口的代码片段可能如下:
RpcClient client = new ...;
DemoService demo = client.refer(DemoService.class);
demo.hi("how are you?");
在 java 中 import
是关键字,所以代码片段中我们用 refer 来表达导入接口的意思。 这里的导入方式本质也是一种代码生成技术,只不过是在运行时生成,比静态编译期的代码生成看起来更简洁些。 java 里至少提供了两种技术来提供动态代码生成,一种是 jdk 动态代理,另外一种是字节码生成。 动态代理相比字节码生成使用起来更方便,但动态代理方式在性能上是要逊色于直接的字节码生成的,而字节码生成在代码可读性上要差很多。 两者权衡起来,个人认为牺牲一些性能来获得代码可读性和可维护性显得更重要。
协议编解码
客户端代理在发起调用前需要对调用信息进行编码,这就要考虑需要编码些什么信息并以什么格式传输到服务端才能让服务端完成调用。 出于效率考虑,编码的信息越少越好(传输数据少),编码的规则越简单越好(执行效率高)。 我们先看下需要编码些什么信息:
调用编码
- 接口方法
包括接口名、方法名 - 方法参数
包括参数类型、参数值 - 调用属性
包括调用属性信息,例如调用附件隐式参数、调用超时时间等
返回编码
- 返回结果
接口方法中定义的返回值 - 返回码
异常返回码 - 返回异常信息
调用异常信息
除了以上这些必须的调用信息,我们可能还需要一些元信息以方便程序编解码以及未来可能的扩展。 这样我们的编码消息里面就分成了两部分,一部分是元信息、另一部分是调用的必要信息。 如果设计一种 RPC 协议消息的话,元信息我们把它放在协议消息头中,而必要信息放在协议消息体中。 下面给出一种概念上的 RPC 协议消息设计格式:
消息头
-
magic
: 协议魔数,为解码设计 -
header size
: 协议头长度,为扩展设计 -
version
: 协议版本,为兼容设计 -
st
: 消息体序列化类型 -
hb
: 心跳消息标记,为长连接传输层心跳设计 -
ow
: 单向消息标记, -
rp
: 响应消息标记,不置位默认是请求消息 -
status code
: 响应消息状态码 -
reserved
: 为字节对齐保留 -
message id
: 消息 id -
body size
: 消息体长度
消息体
采用序列化编码,常见有以下格式
-
xml
: 如 webservie SOAP -
json
: 如 JSON-RPC -
binary
: 如 thrift; hession; kryo 等
格式确定后编解码就简单了,由于头长度一定所以我们比较关心的就是消息体的序列化方式。 序列化我们关心三个方面:
- 序列化和反序列化的效率,越快越好。
- 序列化后的字节长度,越小越好。
- 序列化和反序列化的兼容性,接口参数对象若增加了字段,是否兼容。
上面这三点有时是鱼与熊掌不可兼得,这里面涉及到具体的序列化库实现细节,就不在本文进一步展开分析了。
传输服务
协议编码之后,自然就是需要将编码后的 RPC 请求消息传输到服务方,服务方执行后返回结果消息或确认消息给客户方。 RPC 的应用场景实质是一种可靠的请求应答消息流,和 HTTP 类似。 因此选择长连接方式的 TCP 协议会更高效,与 HTTP 不同的是在协议层面我们定义了每个消息的唯一 id,因此可以更容易的复用连接。
既然使用长连接,那么第一个问题是到底 client 和 server 之间需要多少根连接? 实际上单连接和多连接在使用上没有区别,对于数据传输量较小的应用类型,单连接基本足够。 单连接和多连接最大的区别在于,每根连接都有自己私有的发送和接收缓冲区, 因此大数据量传输时分散在不同的连接缓冲区会得到更好的吞吐效率。 所以,如果你的数据传输量不足以让单连接的缓冲区一直处于饱和状态的话,那么使用多连接并不会产生任何明显的提升, 反而会增加连接管理的开销。
连接是由 client 端发起建立并维持。 如果 client 和 server 之间是直连的,那么连接一般不会中断(当然物理链路故障除外)。 如果 client 和 server 连接经过一些负载中转设备,有可能连接一段时间不活跃时会被这些中间设备中断。 为了保持连接有必要定时为每个连接发送心跳数据以维持连接不中断。 心跳消息是 RPC 框架库使用的内部消息,在前文协议头结构中也有一个专门的心跳位, 就是用来标记心跳消息的,它对业务应用透明。
执行调用
client stub 所做的事情仅仅是编码消息并传输给服务方,而真正调用过程发生在服务方。 server stub 从前文的结构拆解中我们细分了 RpcProcessor
和 RpcInvoker
两个组件, 一个负责控制调用过程,一个负责真正调用。 这里我们还是以 java 中实现这两个组件为例来分析下它们到底需要做什么?
java 中实现代码的动态接口调用目前一般通过反射调用。 除了原生的 jdk 自带的反射,一些第三方库也提供了性能更优的反射调用, 因此 RpcInvoker 就是封装了反射调用的实现细节。
调用过程的控制需要考虑哪些因素,RpcProcessor 需要提供什么样地调用控制服务呢? 下面提出几点以启发思考:
- 效率提升
每个请求应该尽快被执行,因此我们不能每请求来再创建线程去执行,需要提供线程池服务。 - 资源隔离
当我们导出多个远程接口时,如何避免单一接口调用占据所有线程资源,而引发其他接口执行阻塞。 - 超时控制
当某个接口执行缓慢,而 client 端已经超时放弃等待后,server 端的线程继续执行此时显得毫无意义。
RPC 异常处理
无论 RPC 怎样努力把远程调用伪装的像本地调用,但它们依然有很大的不同点,而且有一些异常情况是在本地调用时绝对不会碰到的。 在说异常处理之前,我们先比较下本地调用和 RPC 调用的一些差异:
- 本地调用一定会执行,而远程调用则不一定,调用消息可能因为网络原因并未发送到服务方。
- 本地调用只会抛出接口声明的异常,而远程调用还会跑出 RPC 框架运行时的其他异常。
- 本地调用和远程调用的性能可能差距很大,这取决于 RPC 固有消耗所占的比重。
正是这些区别决定了使用 RPC 时需要更多考量。 当调用远程接口抛出异常时,异常可能是一个业务异常, 也可能是 RPC 框架抛出的运行时异常(如:网络中断等)。 业务异常表明服务方已经执行了调用,可能因为某些原因导致未能正常执行, 而 RPC 运行时异常则有可能服务方根本没有执行,对调用方而言的异常处理策略自然需要区分。
由于 RPC 固有的消耗相对本地调用高出几个数量级,本地调用的固有消耗是纳秒级,而 RPC 的固有消耗是在毫秒级。 那么对于过于轻量的计算任务就并不合适导出远程接口由独立的进程提供服务, 只有花在计算任务上时间远远高于 RPC 的固有消耗才值得导出为远程接口提供服务。
总结
至此我们提出了一个 RPC 实现的概念框架,并详细分析了需要考虑的一些实现细节。 无论 RPC 的概念是如何优雅,但是“草丛中依然有几条蛇隐藏着”,只有深刻理解了 RPC 的本质,才能更好地应用
相关推荐
RPC研究,从浅到深,含研究笔记:超级全面,包懂。代码分等级,从最简单的socket通信,到通过代理获取;再到动态代理;代理接口不暴露;以及序列化方式对比;以及大数据框架下的rpc通信应用。自己整理了很久,超值!
本资料“Python中常见的网络RPC研究和比较”深入探讨了几种主流的Python RPC库,帮助我们理解它们的工作原理、优缺点以及适用场景。 首先,让我们了解RPC的基本概念。RPC允许一个进程(客户端)通过调用API来请求另...
Python中常见的网络RPC研究和比较.pdf
RPC框架的研究,主要是Hessian和其它框架的对比,从性能、易用性等方面着手分析。
源码分析是理解NFS-RPC实现细节的重要途径,可以深入研究其数据结构、状态机、错误处理等方面,这对于优化和自定义NFS-RPC实现非常有帮助。 总的来说,NFS-RPC是分布式系统中实现高效文件共享的关键技术,它的设计...
本文的研究成果可以应用于高分辨率遥感卫星和航空遥感等领域,提高遥感影像的几何校正精度,可以广泛应用于遥感应用领域。 八、结语 本文提出了一种基于严格成像模型的RPC参数求解方法,实验结果表明该方法可以...
通过阅读和研究源码,可以深入了解SOFARPC的内部实现,包括服务调用流程、服务治理策略、以及如何定制和扩展框架等功能。 总的来说,SOFARPC是一个强大且灵活的RPC框架,它为Java开发者构建高可用、高性能的分布式...
RPC(Remote Procedure Call)是一种进程间通信方式,允许一台计算机上的程序调用另一台计算机上的程序,就像调用本地函数一样。...深入研究`rpc文档.doc`和`hellorpc`项目,将有助于你掌握这一重要的技术。
RPCScan2是一款专门用于RPC(Remote Procedure Call,远程过程调用)服务的扫描工具,它在IT领域中常被安全研究人员、系统管理员以及网络运维人员使用,以检测和评估网络中RPC服务的安全性和稳定性。RPC作为一种...
它被设计用来解决不同编程语言之间的数据交换问题,通过提供一套定义数据结构和服务的接口定义语言(IDL),以及自动生成相应的客户端和服务端代码,简化了远程过程调用(RPC)的实现。 Thrift 的核心特点是其轻量...
深入研究源码可以帮助理解RPC的工作原理,学习如何设计和实现分布式系统的组件。 总的来说,"rpc-invoke.zip"提供了一个实践性的学习资源,通过分析和理解源码,开发者可以更好地掌握RPC通信、服务注册发现和负载...
本文将深入探讨Kryonet在RPC协议中的应用与研究。 首先,我们来理解Kryo序列化机制。Kryo是一个高性能的对象图形序列化库,适用于Java和Scala。它的设计目标是速度和低内存占用,这使得Kryo非常适合在网络传输或...
RPC,即Remote Procedure Call(远程过程调用),是一...通过研究这些文件,你可以深入了解RPC的工作原理以及如何在C语言环境下实现它。这将有助于提升你的分布式系统开发能力,尤其是对于网络通信和进程间通信的理解。
RPC(Remote Procedure Call)是远程过程调用,它允许一个程序在执行时调用另一个位于不同计算机上的程序,使得分布式系统中的组件可以相互...对于更复杂的应用场景,比如多线程、并发调用等,还需要进一步研究和探索。
在IT行业中,RPC(Remote Procedure Call)是一种分布式计算技术,允许程序在不同的网络节点上像调用本地...通过分析提供的压缩包文件,我们可以深入研究这些技术的具体实现,提升在分布式系统和网络编程方面的技能。
RPC(Remote Procedure Call)是一种分布式计算技术,允许在一台...通过学习这个简单的RPC调用例子,你可以理解RPC的基本原理,掌握如何在Java中实现客户端和服务端的通信,为后续深入研究复杂的分布式系统打下基础。
用户可以通过研究和运行其中的代码,了解如何在Windows环境下使用ONCRPC和RPCGEN来实现远程过程调用,并理解RPC中间件的工作原理。对于想要深入理解分布式系统通信或者开发RPC应用的开发者来说,这是一个宝贵的资源...
标题 "PHPRPC的源码JAR" 指向的是一个开源项目,它涉及到PHP和Java之间的远程过程调用(RPC)技术。...深入研究`java`目录下的源代码,可以帮助开发者更深入地理解其工作原理,进行定制化开发或优化。
Netty实现RPC是一种常见的方式,尤其在分布式系统和微服务架构中。Netty是一个高性能、异步事件驱动的网络应用程序框架,它简化...文件名"rpcofnetty"可能包含了实现这个过程的相关代码或文档,可以进一步学习和研究。