RPC了解
RPC定义
RPC(Remote Procedure Call Protocol)远程过程调用协议。
通俗的描述:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。
正式的描述:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
- RPC是协议
- 网络协议和网络IO模型对其透明:既然RPC的客户端认为自己是在调用本地对象。那么传输层使用的是TCP/UDP还是HTTP协议,又或者是一些其他的网络协议它就不需要关心了。既然网络协议对其透明,那么调用过程中,使用的是哪一种网络IO模型调用者也不需要关心。
- 信息格式对其透明
- **应该有跨语言能力:**调用方实际上也不清楚远程服务器的应用程序是使用什么语言运行的。那么对于调用方来说,无论服务器方使用的是什么语言,本次调用都应该成功,并且返回值也应该按照调用方程序语言所能理解的形式进行描述。
RPC框架的的两条路线:
- 跨语言,服务端采用不同语言实现,客户端也可以用不同的语言实现,不同的语言实现的客户端和服务端可以互相调用。支持不同的语言,需要基于那种语言实现相同协议的框架。
- 支持服务治理,主要精力放在服务发现,路由、容错处理等方面
RPC主要组成部分
- **Client:**RPC协议的调用方。就像上文所描述的那样,最理想的情况是RPC Client在完全不知道有RPC框架存在的情况下发起对远程服务的调用。但实际情况来说Client或多或少的都需要指定RPC框架的一些细节。
- **Server:**在RPC规范中,这个Server并不是提供RPC服务器IP、端口监听的模块。而是远程服务方法的具体实现(在JAVA中就是RPC服务接口的具体实现)。
- Stub/Proxy:RPC代理存在于客户端,因为要实现客户端对RPC框架“透明”调用,那么客户端不可能自行去管理消息格式、不可能自己去管理网络传输协议,也不可能自己去判断调用过程是否有异常。这一切工作在客户端都是交给RPC框架中的“代理”层来处理的.
- Message Protocol: 一次完整的client-server的交互肯定是携带某种两端都能识别的,共同约定的消息格式。RPC的消息管理层专门对网络传输所承载的消息信息进行编码和解码操作。
- Transfer/Network Protocol:传输协议层负责管理RPC框架所使用的网络协议、网络IO模型。例如Hessian的传输协议基于HTTP(应用层协议);而Thrift的传输协议基于TCP(传输层协议)。传输层还需要统一RPC客户端和RPC服务端所使用的IO模型;
- **Selector/Processor:**存在于RPC服务端,用于服务器端某一个RPC接口的实现的特性(它并不知道自己是一个将要被RPC提供给第三方系统调用的服务)。所以在RPC框架中应该有一种“负责执行RPC接口实现”的角色。包括:管理RPC接口的注册、判断客户端的请求权限、控制接口实现类的执行在内的各种工作。
- IDL:实际上IDL(接口定义语言)并不是RPC实现中所必须的。但是需要跨语言的RPC框架一定会有IDL部分的存在。这是因为要找到一个各种语言能够理解的消息结构、接口定义的描述形式。
RPC框架的性能影响的几个因素:
使用的网络IO模型:RPC服务器可以只支持传统的阻塞式同步IO,也可以做一些改进让RPC服务器支持非阻塞式同步IO,或者在服务器上实现对多路IO模型的支持。这样的RPC服务器的性能在高并发状态下,会有很大的差别。特别是单位处理性能下对内存、CPU资源的使用率。
基于的网络协议:一般来说您可以选择让您的RPC使用应用层协议,例如HTTP或者HTTP/2协议,或者使用TCP协议,让您的RPC框架工作在传输层。工作在哪一层网络上会对RPC框架的工作性能产生一定的影响,但是对RPC最终的性能影响并不大。但是至少从各种主流的RPC实现来看,没有采用UDP协议做为主要的传输协议的。
消息封装格式:选择或者定义一种消息格式的封装,要考虑的问题包括:消息的易读性、描述单位内容时的消息体大小、编码难度、解码难度、解决半包/粘包问题的难易度。当然如果您只是想定义一种RPC专用的消息格式,那么消息的易读性可能不是最需要考虑的。消息封装格式的设计是目前各种RPC框架性能差异的最重要原因,这就是为什么几乎所有主流的RPC框架都会设计私有的消息封装格式的原因。dubbo中消息体数据包含dubbo版本号、接口名称、接口版本、方法名称、参数类型列表、参数、附加信息。
Schema 和序列化(Schema & Data Serialization):序列化和反序列化,是对象到二进制数据的转换,程序是可以理解对象的,对象一般含有 schema 或者结构,基于这些语义来做特定的业务逻辑处理。考察一个序列化框架一般会关注以下几点:
- Encoding format 。是 human readable(是否能直观看懂 json) 还是 binary(二进制)。
- Schema declaration 。也叫作契约声明,基于 IDL,比如 Protocol Buffers/Thrift,还是自描述的, 比如 JSON、XML。另外还需要看是否是强类型的。
- 语言平台的中立性 。比如 Java 的 Native Serialization 就只能自己玩,而 Protocol Buffers 可以跨各种语言和平台。
- 新老契约的兼容性 。比如 IDL 加了一个字段,老数据是否还可以反序列化成功。
- 和压缩算法的契合度 。跑 benchmark (基准)和实际应用都会结合各种压缩算法,例如 gzip、snappy。
- 性能 。这是最重要的,序列化、反序列化的时间,序列化后数据的字节大小是考察重点。
- 序列化方式非常多,常见的有 Protocol Buffers, Avro,Thrift,XML,JSON,MessagePack,Kyro,Hessian,Protostuff,Java Native Serialize,FST 。