7.Go语言高并发与微服务实战
第 7 章 远程过程调用 RPC
7.1 RPC 机制和实现过程
7.1.1 RPC 机制
1.客户端进程以正常的方式调用客户存根
2.客户存根生成一个消息,然后调用本地操作系统的网络模块,存根进入阻塞状态
3.客户端操作系统将网络消息发送给远程操作系统
4.远程操作系统将网络消息交给服务端存根
5.服务端存根将参数调取出来,而后调用服务端程序
6.服务端程序执行相应的操作,操作完成后将结果返回给服务端存根
7.服务端存根将结果打包成一个消息,而后调用本地操作系统
8.服务端操作系统将含有结果的消息发送给客户端操作系统
9.客户端操作系统将消息交给客户存根,存根从阻塞状态恢复,进入运行状态
10.客户端存根将结果从消息中提取,返回给调用它的客户端过程
要实现一个RPC远程过程调用,需要考虑以下几个主要方面:参数传递、通信协议定制、出错和超时处理等。
7.1.2 传递参数
1.传递值参数
比较简单,只需要将参数的值复制到网络消息的数据中即可。
2.传递引用参数
传递应用参数比较困难。单纯传递参数的引用(也包含指针)是完全没有意义的,因为引用地址传递给远程计算机,其指向的内存位置困难跟
远程系统上完全不同。如果你想支持传递引用参数或者传递对象,你就必须发送参数的副本,将它们防止在远程系统内存中,向它们传递指向服务器
内存的指针,然后将对象发送回客户端,复制它的引用,这一过程是很麻烦并且容易出错的。因此,RPC一般不支持直接传递引用对象。
3.数据格式的统一问题
在本地系统上不存在数据不相容的问题,因为数据格式总是相同的;而在分布式系统中则不同,不同远程机器上可能有不同的字节顺序,不同
的大小整数,以及不同的浮点表示。对于RPC,如果想与异构系统通信,我们就需要制定一个"标准"来对所有数据类型进行编码,让其可以作为
参数传递。数据表示格式可以使用隐式或者显式类型。隐式类型,是指只传递值,而不传递变量的名称或者类型。也可以使用显式传递,指需要
指出每个字段的类型以及值。
7.1.3 通信协议制定
rpc调用的通信协议选择是指协议栈的设计和选择。
广义上协议栈可以分为公有协议和私有协议,例如http、smpp、webservice等都是公有协议;如果是某个公司或者组织内部自定义、自己使用
的协议,没有被国际标准化组织接纳和认可的,往往化为私有协议,例如Thrift协议和蚂蚁金服的bolt协议。
分布式架构所需要的企业内部通信模块,往往采用私有协议来设计和研发。私有协议虽然有很多弊端,比如在通用性上、公网传输的能力相比公有
协议会有劣势。然而,高度定制化的私有协议可以最大程度的提升性能,降低成本,提高灵活性与效率。定制私有协议,可以有效的利用协议里的各个
字段,灵活满足各种通信功能需求:比如CRC校验,Server Fail-Fast 机制和自定义序列化器。
整个协议栈的组成部分,包括编码器、解码器、心跳、命令协议和命令处理器。
1.协议设计
协议设计上,我们需要考虑几个关键问题:协议包括的必要字段与主要业务负载字段:协议里设计的每个字段都应该被使用到,避免
无效字段;需要考虑通信功能特性的支持:比如crc校验,安全校验,数据压缩机制等;需要考虑协议的升级机制:毕竟是私有协议,没有
长期的验证,字段新增或者修改,是有可能发生的,因此升级机制是必须考虑的。
2.私有协议的编解码
协议相关的编解码来说,私有协议需要有核心的编码和解码过程,并且针对业务负载能支持不同的编码与解码机制。需要为不同的私有
协议分别设计对应的编解码过程。
3.命令定义和命令处理器
协议的通信过程,会有各种命令定义,一般分为两种:一种是负载命令,另外一种是控制命令。
负载命令一般是指传输业务的具体数据,比如请求参数,响应结果的命令;控制命令一般为功能管理命令,心跳命令等,它们通常完成
复杂的分布式跨节点的协调功能,以此来保证负载命令通信过程的稳定,是必不可少的一部分。
定义了通信命令,我们还需要定义命令处理器,用来编写各个命令对应的业务处理逻辑。同时,我们需要保存命令与命令处理器的映射
关系,以便在处理阶段,走到正确的处理器。
4.命令协议
读者一般比较了解的协议是序列化协议,它也是协议栈的一部分,同一种协议也可以承载多种序列化方式,以http为例,它既可以承载
文本类序列化方式,例如:xml、json等,也可以承载二进制序列化方式,例如ProtoBuffer、Thrift和Avro。
不同协议在编解码效率和传输效率上有所不同。
5.通信模式
通信模式也是通信协议栈的一部分,包括4种模式,分别为 oneway、sync、future和callback。
1.oneway
不关心响应,请求线程不会被阻塞,但使用时需要注意控制调用流量,进行蓄洪,防止压垮下游业务服务。
2.sync
请求会阻塞请求线程,待响应返回后才能进行下一个请求。这是最常见的一种通信模型。
3.future
调用,在调用过程中不会阻塞线程,但获取结果的过程会阻塞线程。
4.callback
真正的异步调用,不会阻塞线程,结果处理是在异步线程里执行。
7.1.4 出错和超时处理
注意幂等调用和非幂等调用。
7.1.5 通用 RPC 接口
rpc的机制和实现过程中需要处理的重要问题,比如说参数传递、通信协议和出错以及超时处理等。rpc框架的实现往往就是解决上述这些
问题的框架,这些框架都提供了一套类似的支持库组件,用来解决相同的问题,比如说协议转码问题和网络通信问题。
1.名称服务操作
2.绑定操作
3.终端操作
4.安全操作
5.国际化操作
6.封装处理/数据转换操作
7.存根内存管理和垃圾回收
8.程序标识操作
7.2 简易的 Go 语言原生 RPC
Go语言原生的rpc过程调用实现起来非常简单。服务端只需要对外提供的远程过程方法和结构体,然后将其注册到rpc服务中,然后客户端
就可以通过其服务名称和方法名称进行rpc方法调用。
7.2.1 实践案例:Go 语言 RPC 过程调用实践
1.首先定义远程过程调用相关接口传入参数和返回参数的数据结构。
type StringRequest s