远程函数调用,Remote Procedute Calls,简称RPC。RPC需要解决的问题有:完成远程函数的映射,并且将数据转化为字节流,使用网络进行传输
RPC基本概念
IDL文件:IDL通过一种中立的方式来描述接口,使得不同平台上运行的对象和用不同语言编写的程序可以相互通信
生成代码:通过编译工具将IDL文件转换成语言对应的静态库
编解码:从内存中表示到字节序列的转化称之为编码,反之为解码,常称之为序列化和反序列化
通信协议:规范了数据在网络中传输的内容和格式,除了必要的响应请求数据外,通常还会包含额外的元数据
RPC的好处在于:
- 函数职责单一,有利于分工协作和运维开发
- 可扩展性强,可以独立扩充资源,资源使用率更优秀
- 故障隔离,单个服务故障不影响整体,服务的整体可用性更高
但是RPC也带来一定的挑战:
- 如果调用的函数所属的服务宕机应该如何处理
- 调用过程中网络异常,如何保证消息可达
- 请求量突增导致服务无法及时处理,如何应对
这些问题一般由RPC框架来解决
分层设计
以Apache Thrift为例子
编解码层
客户端和服务端通过同一份IDL文件生成不同的语言
其中IDL可采用的文件数据格式有:
- 语言特定格式:许多编程语言都内建了将内存对象编码为字节序列的支持,比如Java的java.io.Serializable序列化格式。缺点是和语言深度绑定了
- 文本格式:JSON\XML\CSV等,具有人类可读性,但是描述不够严谨,而且缺乏模型约束,只能采用文档约束
- 二进制编码:具有跨语言和高性能等优点,比如说Thrift的BinaryProtocal等
以BinaryProtocal为例,他的实现是TLV(Tag-Length-Value)编码,其IDL中的示例如下:
我们可以看到该结构体中的第一行属性,string是它的类型,1是他的编号
那么一个Person(“Martin”,1337,[“daydreaming”, “hacking”])的实例在编码后的字节流如下:
我们可以看到,前4个字节表示的是userName的类型,为string,二进制编码为0b,因为在TLV中11表示string。接下来8个字节为field tag,指明是哪个属性,其中值为00 01,表示1号tag,也就是username;接下来的16字节表示该属性的长度,“Martin”总共6个字符因此长度为6;后面跟着的24字节为“Martin”的ASCII码表示。
第二个favoriteNumber的类型为INT64,长度已经固定,因此不需要指出length。
编码层的选型需要考虑以下因素:
- 兼容性:支持新增字段但不影响老得服务
- 通用性:跨平台跨语言
- 性能:从空间和时间两个维度来考虑
协议层
Thrift协议层通用协议格式:
网络通信层
主要是用SocketAPI进行通信,其中SocketAPI位于应用层和传输层之间
其中SocketAPI执行流程如下
一般开发的时候,一般使用网络库来操作,用于提供易用的API,支持多种协议,并且要求有优异的性能
RPC框架关键指标
稳定性
熔断:保护调用方,防止被调用的服务出现问题而影响整个链路
限流:保护被调用方,防止大流量把服务压垮
超时控制:避免浪费资源在不可用的结点上
另外还有一个很重要的指标:请求成功率
提高成功率的办法:负载均衡和重试
另外需要注意的问题是长尾请求,指的是响应时间明显长于平均的请求,也就是响应时间超过99%的其他请求的请求。
解决长尾请求的方法是响应备份Backup Request。如右图,发送第一次请求后,在t3时间内没有收到请求的时候发送备份请求Request2。t3的值是前99%请求的响应时间。
增加稳定性的手段,比如超时、熔断等,主要是通过注册中间件来实现
易用性
易用性主要特点是:
- 开箱即用:提供合理的默认参数选项、丰富的文档
- 丰富的周边工具:比如提供生成代码工具、脚手架工具
扩展性
能够提供足够多的扩展点,比如:
- 中间件
- Option
- 编解码层和协议层的扩展
- 支持扩展代码生成工具
- 支持自行添加更多的网络传输层协议