五分钟让你了解rpc原理详解(代码片段)

author author     2023-04-09     124

关键词:

RPC 功能目标

RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。?为实现该目标,RPC 框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用。 下面我们将具体细化 stub 结构的实现。

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 等

格式确定后编解码就简单了,由于头长度一定所以我们比较关心的就是消息体的序列化方式。 序列化我们关心三个方面:

  1. 序列化和反序列化的效率,越快越好。

  2. 序列化后的字节长度,越小越好。

  3. 序列化和反序列化的兼容性,接口参数对象若增加了字段,是否兼容。

上面这三点有时是鱼与熊掌不可兼得,这里面涉及到具体的序列化库实现细节,就不在本文进一步展开分析了。

传输服务

协议编码之后,自然就是需要将编码后的 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 调用的一些差异:

  1. 本地调用一定会执行,而远程调用则不一定,调用消息可能因为网络原因并未发送到服务方。

  2. 本地调用只会抛出接口声明的异常,而远程调用还会跑出 RPC 框架运行时的其他异常。

  3. 本地调用和远程调用的性能可能差距很大,这取决于 RPC 固有消耗所占的比重。

正是这些区别决定了使用 RPC 时需要更多考量。 当调用远程接口抛出异常时,异常可能是一个业务异常, 也可能是 RPC 框架抛出的运行时异常(如:网络中断等)。 业务异常表明服务方已经执行了调用,可能因为某些原因导致未能正常执行, 而 RPC 运行时异常则有可能服务方根本没有执行,对调用方而言的异常处理策略自然需要区分。

由于 RPC 固有的消耗相对本地调用高出几个数量级,本地调用的固有消耗是纳秒级,而 RPC 的固有消耗是在毫秒级。 那么对于过于轻量的计算任务就并不合适导出远程接口由独立的进程提供服务, 只有花在计算任务上时间远远高于 RPC 的固有消耗才值得导出为远程接口提供服务。

总结

至此我们提出了一个 RPC 实现的概念框架,并详细分析了需要考虑的一些实现细节。 无论 RPC 的概念是如何优雅,但是“草丛中依然有几条蛇隐藏着”,只有深刻理解了 RPC 的本质,才能更好地应用。

共同进步,学习分享

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

海量面试、架构资料分享

技术图片

五分钟告诉你什么是mysql的覆盖索引(代码片段)

文章目录五分钟告诉你什么是MySQL的覆盖索引覆盖索引总结参考五分钟告诉你什么是MySQL的覆盖索引前面我们已经对MySQL索引底层原理多少有一定的了解了,还不是很了解的小伙伴可以看我之前的博文:《不会吧不会吧,难... 查看详情

docker:十五分钟快速了解docker快速部署nginx(代码片段)

Docker目录Dockerdocker为什么出现?Docker的历史和作用Docker安装Docker的组成结构安装Docker阿里云的容器加速底层原理常用命令练习部署nginx总结docker为什么出现?一款产品:开发–上线两套环境,两套配置开发和运维... 查看详情

五分钟了解一下mysql的体系结构(代码片段)

文章目录MySQL体系结构ClientconnectorMySQLServerConnectionpoolServiceutilitiesSQL层SQLinterface解析器(Parser)优化器(Optimizer)缓存(Cache)存储引擎物理存储层参考MySQL体系结构下图来自《MySQL技术内幕(Inno 查看详情

五分钟,让你明白mysql是怎么选择索引《死磕mysql系列六》(代码片段)

一网打尽MySQL的各种锁系列文章一、如何选择索引影响优化器的几大因素扫描行数从何而来?为什么优化器选择了扫描行数多的索引?二、索引选择异常如何处理三、总结系列文章二、一生挚友redolog、binlog《死磕MySQL系列... 查看详情

hadoop详解——hdfs的命令,执行过程,java接口,原理详解。rpc机制(代码片段)

HDFS是Hadoop的一大核心,关于HDFS需要掌握的有:分布式系统与HDFS、HDFS的体系架构和基本概念、HDFS的shell操作、Java接口以及常用的API、Hadoop的RPC机制、远程debugDistributed FileSystem数据量越来越多,在一个操作系统管理的... 查看详情

rpc----rpc入门了解&最简单的rpc的实现(代码片段)

RPC入门了解一、历史背景二、理论知识1、概念2、为什么要用RPC3、工作原理4、RPC解决了什么问题?5、RPCvsHTTP远程调用方式6、常用RPC框架三、实现1、对象序列化1.1序列化的原因1.2概念1.3解决方案(RPC序列化框架)1.4代码实现2、网络... 查看详情

五分钟了解mysql脏读幻读不可重复读mvcc(代码片段)

点击上方关注“终端研发部”设为“星标”,和你一起掌握更多数据库知识首先对多事务并发的问题的思考对innodb引擎执行流程 和bufferpool足够了解的话,那一定知道mysql系统在初始化的时候bufferpool会将内存分为多个缓... 查看详情

五分钟带你了解计算机操作系统——进程与线程(万字详解·图文)(代码片段)

进程线程可以说是操作系统基础,看过很多关于这方面知识的文章都是纯理论讲述,我准备用图解的形式带你学习和掌握进程、线程。文字力求简单明了,对于复杂概念做到一个概念一张图解,在操作系统课程的... 查看详情

详解c#tuplevsvaluetuple(元组类vs值元组)(代码片段)

...利用详尽的例子详解TupleVSValueTuple(元组类VS值元组),10分钟让你更了解ValueTuple的好处和用法。如果您对Tuple足够了解,可以直接跳过章节”回顾Tuple”,直达章节”ValueTuple详解”,查看值元组的炫丽用法。&nbs 查看详情

一篇文章让你精通:java集合讲解(五,哈希表)(代码片段)

相信大家看过前面的内容后,对集合set有一定的了解,当我们重写定义对象时,要对对象的hashCode和equals方法进行重写。关于为什么我相信大家肯定和我有一样想法,所以小编此篇文章就来讲讲什么是哈希表。哈... 查看详情

五分钟上手echarts教程(代码片段)

...案例:https://download.csdn.net/download/TroyeSivanlp/33199899五分钟上手ECharts教程1-Echarts-介绍2-了解Echarts的基础配置3-如何在页面上显示简单的图表首先,初始化echarts实例对象第二步,指定配置项和数据(option)第三步,将配... 查看详情

你真的了解吗?一文详解底层原理!(代码片段)

前言一、SpringMVC简介1.1、SpringMVC引言为了使Spring有可插入的MVC架构,SpringFrameWork在Spring基础上开发SpringMVC框架,从而在使用Spring进行WEB开发时可以选择使用Spring的SpringMVC框架作为web开发的控制器框架。1.2、SpringMVC的优势SpringMVC是一... 查看详情

sparkstreaming架构原理详解!(代码片段)

目录一、SparkStreaming功能介绍(1)概述(2)DStream概述(3)Storm和SparkStreaming比较二、SparkStreaming服务架构及工作原理三、StreamingContext原理详解四、DStream和Receiver详解五、SparkStreaming基于HDFS的实时计算 查看详情

五分钟带你学会javascript闭包(代码片段)

...不会对闭包的概念进行大篇幅描述,直接上干货,让你分分钟学会闭包!1闭包–爱的初体验在接触一个新技术的时候,我首先会做的一件事就是:找它的democode。对于码农们来说,代码有时候比自然语言更能理解一个事物。其实... 查看详情

五分钟带你玩转elasticsearch还不了解elasticsearch?带你全方位认知(代码片段)

 概述Elasticsearch是面向文档(documentoriented)的,这意味着它可以存储整个对象或文档(document)。然而它不仅仅是存储,还会索引(index)每个文档的内容使之可以被搜索。在Elasticsearch中,你可以对文档(而非成行成列的... 查看详情

五分钟带你玩转elasticsearch还不了解elasticsearch?带你全方位认知(代码片段)

 概述Elasticsearch是面向文档(documentoriented)的,这意味着它可以存储整个对象或文档(document)。然而它不仅仅是存储,还会索引(index)每个文档的内容使之可以被搜索。在Elasticsearch中,你可以对文档(而非成行成列的... 查看详情

一文搞懂rpc原理(代码片段)

...是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP/IP或UDP,为通信程序之间携带信息数据。RPC将原来的本地调用转变为调用远端的服务器上的方法,给系统... 查看详情

深入了解androidhandler机制原理详解(代码片段)

前言在android开发中,经常会在子线程中进行一些操作,当操作完毕后会通过handler发送一些数据给主线程,通知主线程做相应的操作。探索其背后的原理:子线程handler主线程其实构成了线程模型中的经典问题生产... 查看详情