深入浅出rpc——深入篇(转载)

朝闻道 朝闻道     2022-08-20     764

关键词:

本文转载自这里是原文

《深入篇》我们主要围绕 RPC 的功能目标和实现考量去展开,一个基本的 RPC 框架应该提供什么功能,满足什么要求以及如何去实现它?

RPC 功能目标

RPC的主要功能呢个目标是让构建分布式计算更加容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC框架提供一种透明的调用机制让使用者不必显式的区分本地调用和远程调用,在前文《浅出篇》给出了一种时间结构,基于stub的是结构来实现,下面我们将细化stub结构的实现。

RPC调用分类

RPC调用分以下两种:

  • 同步调用 > 客户方等待调用执行完成并返回结果

  • 异步调用 > 客户调用后不用等待执行结果返回,但依然可以通过回调通知等方式获取返回结果,若客户方不关系调用返回结果,则变成单向异步调用,单向调用不用返回结果。

RPC结构拆解

《浅出篇》给出了一个比较粗粒度的RPC实现概念结构,这里我们进一步细化它该由哪些组件构成,如下图所示: 技术分享

RPC服务方通过RpcServer导出远程接口方法,而客户方通过RpcClient去引入远程接口方法。客户方向调用本地方法一样去调用远程方法,RPC框架提供接口的代理实现,实际的调用委托给RpcProxy。代理封装调用信用信息将调用转交给RpcInvoker去实际执行,在客户端的RpcInvoker通过连接器RpcConnector去维持服务器端的通道RpcChannel,并使用RpcProtocol执行协议编码(encode)并将编码后的消息通过通道发给服务方。

RPC服务方的RpcAcceptor去接受客户方的调用请求,同样使用RPCProtocol执行协议解码(decode)。解码后的信息传递给RPCProcessor去控制处理调用过程,最后在委托调用给RPCInvoker去实际执行并返回调用结果。

RPC组件职责

上面我们进一步拆解了RPC实现结构的哥哥组件部分,下面我们详细说明每个部分的职责:

    1. RPCServer > 负责导出接口
    1. RpcClient > 负责导入接口
    1. RpcProxy > 远程接口代理实现
    1. RpcInvoker > 客户方实现:负责编码调用信息,并发送调用信息并等待调用返回。 > 服务方实现:负责调用服务端接口的具体实现并返回调用结果。
    1. RpcProtocol > 负责协议编解码
    1. RpcConnector > 负责维持客户方和服务方的谅解通道和发送数据到服务方
    1. RpcAcceptor > 负责接收客户方的请求并返回处理结果
    1. RpcProcessor > 负责在服务方管理调用过程,包括线程池和调用的超时时间等。
    1. RpcChannel > 数据传输通道

RPC实现分析

在进一步拆解了组件并划分了职责以后,这里以在Java平台实现该RPC框架概念模型为例,详细分析下实现中需要考虑的因素:

导出远程接口

导出远程接口的意思指的只有导出的接口可以供远程调用,而未导出的接口则不能用,在java中导出接口的代码片段可能如下:

	DemoServer demo = new ...;
    RpcSerber server = new ...;
    server.export(Demoserver.class, demo, options)

我们可以导出整个接口也可以更细粒度一点只导出接口中的某些方法,如:

	//只导出Demoserver中签名为hi(string s) 的方法
    server.export(DemoServer.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 ...;
    DemoServer 	demo = client.refer(DemoServer.class);
    demo.hi("how are you");

在 java 中 import 是关键字,所以代码片段中我们用 refer 来表达导入接口的意思。 这里的导入方式本质也是一种代码生成技术,只不过是在运行时生成,比静态编译期的代码生成看起来更简洁些。 java 里至少提供了两种技术来提供动态代码生成,一种是 jdk 动态代理,另外一种是字节码生成。 动态代理相比字节码生成使用起来更方便,但动态代理方式在性能上是要逊色于直接的字节码生成的,而字节码生成在代码可读性上要差很多。 两者权衡起来,个人认为牺牲一些性能来获得代码可读性和可维护性显得更重要。

协议编解码

客户端代理在发起调用前需要对调用信息进行编码,这就要考虑需要编码些什么信息并以什么格式传输到服务端才能让服务端完成调用。 出于效率考虑,编码的信息越少越好(传输数据少),编码的规则越简单越好(执行效率高)。 我们先看下需要编码些什么信息:

调用编码

  1. 接口方法

包括接口名、方法名

  1. 方法参数

包括参数类型、参数值

  1. 调用属性

包括调用属性信息,例如调用附件隐式参数、调用超时时间等

返回编码

  1. 返回结果

接口方法中定义的返回值

  1. 返回码

异常返回码

  1. 返回异常信息

调用异常信息

除了以上这些必须的调用信息,我们可能还需要一些元信息以方便程序编解码以及未来可能的扩展。 这样我们的编码消息里面就分成了两部分,一部分是元信息、另一部分是调用的必要信息。 如果设计一种 RPC 协议消息的话,元信息我们把它放在协议消息头中,而必要信息放在协议消息体中。 下面给出一种概念上的 RPC 协议消息设计格式:

消息头

技术分享

  • magic:协议魔数,为解码设计
  • header size: 协议头长度,为扩展设计
  • version: 协议版本,为扩展时使用
  • st: 消息体序列化类型
  • hb: 心跳信息标记,为长连接传输层心跳设计
  • ow: 单向消息标记
  • rp: 相应消息标记。不置位默认是请求消息
  • status code: 响应消息状态码
  • reserved: 为字节对齐保留
  • message id:消息id
  • body size:消息体长度

消息体

采用序列化编码,常见有以下格式:

  • XML:如webservice(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 的本质,才能更好地应用。

http://daodaoliang.com/blog/2015/08/27/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BARPC(%E6%B7%B1%E5%85%A5%E7%AF%87).html

深入浅出rpc-深入篇

《深入篇》我们主要围绕RPC的功能目标和实现考量去展开,一个基本的RPC框架应该提供什么功能,满足什么要求以及如何去实现它? RPC功能目标RPC的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用... 查看详情

rpc

参考:为什么需要RPC,而不是简单的HTTP接口深入浅出RPC-浅出篇深入浅出RPC-深入篇概念RPC的全称是RemoteProcedureCall是一种进程间通信方式。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不... 查看详情

转载和积累系列-深入理解http协议

深入理解HTTP协议1. 基础概念篇1.1 介绍 HTTP是HyperTextTransferProtocol(超文本传输协议)的缩写。它的发展是万维网协会(WorldWideWebConsortium)和Internet工作小组IETF(InternetEngineeringTaskForce)合作的结果,(他们)最终发布... 查看详情

elasticsearch检索分类深入详解—基础篇

转载自服务号【铭毅天下】Elasticsearch中当我们设置Mapping(分词器、字段类型)完毕后,就可以按照设定的方式导入数据。有了数据后,我们就需要对数据进行检索操作。根据实际开发需要,往往我们需要支持包含但不限于以下... 查看详情

深入了解javaclass文件格式

  转载:http://blog.csdn.net/zhangjg_blog/article/details/22432599  经过前八篇关于class文件的博客,关于class文件格式的内容也基本上讲完了。本文是关于class文件格式的最后一篇。在这篇博客中,将会讲解关于方法的几个属性。理解这... 查看详情

深入了解javaclass文件格式

...t/zhangjg_blog/article/details/22205831在本专栏的第一篇文章 深入理解Java虚拟机到底是什么 中,我们主要讲解了什么是虚拟机,这篇博客是对JVM的一个概述。在随后的几篇文章中,一直在讲解class文件格式。在今天这篇博客中,... 查看详情

转载--深入浅出的讲解傅里叶变换

  从电子发烧友转载而来--深入浅出的讲解傅里叶变换-工程师博客-电子发烧友网 http://www.elecfans.com/engineer/blog/20140527344277.html    转载--原贴地址:傅里叶分析之掐死教程(完整版)更新于2014.06.06-与时间无关的故事-知... 查看详情

深入解析开源项目之universal-image-loader缓存篇

珍惜作者劳动成果,如需转载,请注明出处。 http://blog.csdn.net/zhengzechuan91/article/details/50292871Universal-Image-Loader是一个优秀的图片加载开源项目,Github地址在 (Github地址) ,很多童鞋都在自己的项目中用到了。优秀的... 查看详情

(转载)深入了解mybatis参数

原文地址:http://blog.csdn.net/isea533/article/details/44002219深入了解MyBatis参数相信很多人可能都遇到过下面这些异常:"Parameter‘xxx‘notfound.Availableparametersare[...]""Couldnotgetproperty‘xxx‘fromxxxClass.Cause:"Theexpression‘ 查看详情

转载dom事件深入浅出

在DOM事件深入浅出(一)中,我主要给大家讲解了不同DOM级别下的事件处理程序,同时介绍了事件冒泡和捕获的触发原理和方法。本文将继续介绍DOM事件中的知识点,主要侧重于DOM事件中Event对象的属性和方法。那么什么是DOM事... 查看详情

深入浅出java并发编程指南「原理分析篇」深入分析aqs的工作原理(前传)

查看详情

深入理解javaclass文件格式

...t/zhangjg_blog/article/details/21658415前情回顾 在上一篇博客深入理解JavaClass文件格式(三) 中,介绍了常量池中的两种类型的数据项,分别是CONSTANT_Utf8_infoCONSTANT_NameAndType_info。CONSTANT_Utf8_info中存储了几乎所有类型的字符 查看详情

深入理解javaclass文件格式

...下关于class文件格式的之前两篇博客的主要内容。在 深入理解JavaClass文件格式(一) 中,讲解了class文件在整个java体系结构中的位置和作用,讲解了class文件中的魔数和版本号相关的信息,并且对常量池进行了概述。在 查看详情

转载xilinx高速收发器serdes深入研究

  此篇文章深入浅出介绍了关于高速串行收发器的几个重要概念和注意事项,为方便知识点复习总结和后续查阅特此转载,原文标题及链接为:xilinx高速收发器Serdes深入研究-CSDN博客  https://blog.csdn.net/u010161493/article/detai... 查看详情

自定义rpc的完整实现---深入理解rpc内部原理(代码片段)

倘若不使用RPC远端调用的情况下,代码如下:local.py#coding:utf-8#本地调用除法运算的形式classInvalidOperation(Exception):def__init__(self,message=None):self.message=messageor‘involidoperation‘defdivide(num1,num2=1):ifnum2==0:raiseInv 查看详情

[serverlet][转载:深入理解httpsession](代码片段)

[serverlet][转载:深入理解HTTPSession]标签(空格分隔):未分类原创作品,允许转载,转载时请务必以超链接形式标明文章原始出处、作者信息和本声明。否则将追究法律责任。http://lavasoft.blog.51cto.com/62575/275589ses... 查看详情

深入理解javaclass文件格式

...M识别,加载并执行的class文件的格式。 对于理解JVM和深入理解Java语言,学习并了解class文件的格式都是必 查看详情

转载深入理解phpopcode缓存原理

转载地址:深入理解PHPOpcode缓存原理什么是opcode缓存?当解释器完成对脚本代码的分析后,便将它们生成可以直接运行的中间代码,也称为操作码(OperateCode,opcode)。Opcodecache的目地是避免重复编译,减少CPU和内存开销。如果... 查看详情