RPC 框架学习
基础概念
本地函数调用
func main() {
var a = 2
var b = 3
result := calculate(a, b)
fmt.Println(result)
return
}
func calculate(x, y int) {
z := x * y
return z
}
调用过程:
- 将 a 和 b 的值压栈
- 通过函数指针找到 calculate 函数,进入函数取出栈的值 2 和 3,将其赋予 x 和 y
- 计算 并将结果放在 z
- 将 z 的值压栈,然后从 calculate 中返回
- 从栈中取出 z 返回值,并赋值给 result
远程函数调用(RPC - Remote Procedure Calls)
在本地函数调用的过程中增加了一层网络
RPC 需要解决的问题
- 函数映射
- 数据转换成字节流
- 网络传输
RPC 概念模型
1984 年 NeisonFabio 论文,其中提出了 RPC 的过程由 5 个模块组成:User、User-Stub、RPC-Runtime、Server-Stub、Server。其中具体的调用过程为:
Caller端发起本地调用User-stub,并将参数交给User-stub进行打包- 打包好的参数交给
RPCRuntime,并由RPCRuntime传输到对端。 - 对端
Callee的RPCRuntime接受到数据后,进行数据解压。 - 数据解压后在
callee里进行本地调用函数,并将结果类似的返回给Caller
RPC 相关概念
IDL 文件
IDL 通过一种中立的方式来描述接口,使得在不同平台上运行的对象和使用不同语言编写的程序可以相互 通信
生成代码
通过编译器工具把 IDL 文件转换成语言对应的静态库
编译码
从内存中表示到字节序列的转换称作编码,反之为解码,也长叫做序列化和反序列化。
通信协议
规范了数据在网络中的传输内容和格式,除了必须的请求/响应数据外,通常还会包含额外的元数据
网络传输
通常基于成熟的网络库走 TCP/UDP 传输
RPC 的好处
- 单一职责,有利于分工协作。甚至每个服务可以使用不同的语言编写,然后独立上线调用。
- 可拓展性强,资源使用率更优。当服务器压力大的时候可以单独对某项服务进行扩容。底层的资源也可以进行复用。
- 故障隔离,服务的整体可靠性更高。当某一个服务出现故障的时候,不会影响到整体服务。
RPC 带来的问题
- 服务宕机
- 在调用过程中发生网络异常,
- 请求量徒增导致服务无法及时处理。 以上问题可以有专门的RPC框架解决
RPC 的分层设计
PRC 一般分为三层:编解码层、协议层和网络通信层
以 Apache Thrift 为例
编解码层
生成代码
客户端和服务端依赖用一份 IDL 文件生成不同语言的文件。
数据格式
-
语言特定的格式 许多变成语言都内建了将内存对象编码为字节序列的支持,例如 Java 中有
java.io.Serializable -
文本格式
JSON, xml, csv等文本格式,具有人类可读性,但是可能存在描述不准确的问题。 -
二进制编码 具有跨语言和高性能的特点,常见有
Thrift的BinaryProtocal、Protobuf等
二进制编码
使用 TLV 编码
- Tag:标签,可以理解为类型
- Length:长度
- Value:值,Value 也可以是一个 TLV 结构
struct Person {
1: required string userName,
2: optional i64 favoriteNum,
3: optional list<string> interests
}
缺点: 增加了 tag 和 length 的冗余信息,增加了内存开销。
选型
-
兼容性 支持自动增加新的字段,而不影响老的服务,这将提高系统的灵活性
-
通用性 支持跨平台、跨语言
-
性能 从空间和时间两个维度来考虑,也就是编码后数据大小和编码消耗时长
空间开销:序列化需要在原有的数据上加上描述字段,用来解释反序列化的时候使用,如果 序列化过程引入的额外开销过高,可能会导致过大的无网络,磁盘等各方面的压力,对于海量分布式存储系统,数据量往往以 TB 为单位,巨大的额外空间开销意味着高昂的成本
时间开销:负载的序列化协议会导致较长时间的解析时间,这可能会使得序列化和反序列化成为整个系统的速度瓶颈。
协议层
-
特殊结束符 一个特殊字符作为每个协议单元结束的表示,但是不能过于简单,且要防止用户传输的数据不能以同样的结束符结束,不然就会出现混乱,Http 协议头就是以回车 + 换行符号作为序列结尾。这种构造方式,很简单但是对于一个协议单元必须要全部读入才能进行处理。
-
变长协议 以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度。有 header 和 payload 组成。
协构解析
内存首先先读取 MagicNumber 找到数据所在的存储位置,然后再读取 PayloadCode 读取出编码方式,通过对应的编译器,解码出传输信息。
网络通信层 - Sockets API
提供易用 API
- 封装底层 Socket API
- 连接管理和事件分发
功能
- 协议支持:tcp、udp 和 uds 等
- 优雅退出,处理异常等
性能
- 应用层 buffer 减少 copy
- 高性能定时器,对象池等
RPC 性能关键指标
稳定性
为了保证系统稳定性,会使用熔断、限流、超时控制等方式来缓解服务器压力。其中
- 熔断:保护调用方,防止被调用的服务出现问题影响整个网路
- 限流:保护被调用方,防止大流量把服务压垮
- 超时控制:避免浪费资源在不可用的节点上 这三种措施分别针对调用方,被调用方和资源进行,需要根据实际业务选择,也可以全都用上。另一个稳定性的重要指标是请求的成功率。为了提高请求的成功率,可以使用负载均衡和重试的策略。
- 负载均衡:将请求通过轮询、权重选择等方式均匀的打在不同的服务器上,使得每台服务器上的请求数量均匀,防止全部压在一台服务器上导致服务器压力过大
- 重试:当服务请求失败的时候,不急着判断失败,而是重新请求几次后才认定真正失败。提高请求的成功率
长尾请求的处理: 使用
Backup Request策略,在发送一个请求后,如果在预计的时间内没有返回,则再次发送一次请求。提高请求的成功率
易用性
我们希望我们拿到的 RPC 框架具有以下功能:
- 开箱即用:合理的默认参数选项,丰富的文档。不需要做过多的配置就可以实现熔断、限流、降级等等功能
- 周边工具:RPC 框架支持生成代码工具,脚手架工具。
高性能
RPC 框架需要做到高吞吐、低延迟。使用连接池、多路复用、高性能编码协议和高性能网络库等手段,适用于单机多级、单链接多连接,不同大小的请求包等场景。