「深入浅出 RPC 框架」笔记 | 青训营笔记
这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记
RPC 的基本概念
什么是RPC
RPC(Remote Procedure Call Protocol)远程过程调用协议。一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。换句话说就是客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。
相比本地函数调用,RPC调用需要解决的问题
- 函数映射
- 数据转换成字节流
- 网络传输
RPC要素
- Client:RPC协议的调用方。
- Server:RPC协议的被调用方,是远程服务方法的具体实现。其中的代码是最普通的和业务相关的代码,甚至其接口实现类本身都不知道将被某一个RPC远程客户端调用。
- Stub/Proxy:RPC代理存在于客户端,因为要实现客户端对RPC框架“透明”调用,那么这一切工作在客户端都是交给RPC框架中的“代理”层来处理的。
- Message Protocol:一次完整的client-server的交互肯定是携带某种两端都能识别的,共同约定的消息格式。RPC的消息管理层专门对网络传输所承载的消息信息进行编号和解码操作。目前流行的技术趋势是不同的RPC实现,为了加强自身框架的效率都有一套(或者几套)私有的消息格式。
- Transfer/Network Protocol:传输协议层负责管理RPC框架所使用的网络协议、网络IO模型。
- Selector/Processor:存在于RPC服务端,由于服务器端某一个RPC接口的实现的特性(它并不知道自己是一个将要被RPC提供给第三方系统调用的服务)。所以在RPC框架中应该有一种“负责执行RPC接口实现”的角色。它负责了包括:管理RPC接口的注册、判断客户端的请求权限、控制接口实现类的执行在内的各种工作。
- IDL:实际上IDL(接口定义语言)并不是RPC实现中所必须的。但是需要跨语言的RPC框架一定会有IDL部分的存在。这是因为要找到一个各种语言能够理解的消息结构、接口定义的描述形式。如果RPC实现没有考虑跨语言性,那么IDL部分就不需要包括。
ps:说明一点,不同的RPC框架实现都有一定设计差异。
RPC的好处
- 单一职责,有利于分工协作和运维开发
- 可扩展性强,资源使用率更优
- 故障隔离,服务的整体可靠性更高
RPC带来的问题
- 服务宕机,该如何处理
- 在调用过程中发生网络异常,如何保证消息的可达性
- 请求量突增导致服务无法及时处理,有哪些应对措施
RPC 带来的问题将由 RPC 框架来解决
分层设计
以Apache Thrift为例
编解码层
生成代码
数据格式
-
语言特定的格式:许多编程语言都内建了将内存对象编码为字节序列的支持,例如Java有 java.io.Serializable
好处是使用很少的代码实现序列化,不好的是这种编码通常与特定的语言绑定的,其他语言无法读取,兼容性问题
-
文本格式:JSON、XML、CSV等文本格式,具有人类可读
性能可能不好,描述可能并不严谨,例如浮点数的精度等
-
二进制编码:具备跨语言和高性能等优点,常见有Thrift 的 BinaryProtocol,Protobuf 等
协议层
通过编解码层,将数据转换成字节流后并不是直接打包发送给对方,要制定一些通信协议,还会添加一些额外的元数据。
- LENGTH:数据包大小,不包含自身
- HEADER MAGIC:标识版本信息,协议解析时候快速校验
- SEQUENCE NUMBER:表示数据包的seqID,可用于多路复用,单连接内递增
- HEADER SIZE:头部长度,从第14个字节开始计算一直到PAYLOAD前
- PROTOCOL ID:编解码方式,有Binary和Compact两种
- TRANSFORM ID:压缩方式,如zlib和 snappy
- INFO ID:传递一些定制的meta 信息
- PAYLOAD:消息体
网络通信层
Sockets API
操作系统提供的Sockets API介于应用层和传输层之间。
网络库
- 提供易用API 封装底层 Socket APl 连接管理和事件分发
- 功能 协议支持:tcp、udp和uds等 优雅退出、异常处理等
- 性能 应用层 buffer减少copy 高性能定时器、对象池等
关键指标
稳定性
保障策略
- 熔断:保护调用方,防止被调用方出现问题和影响到整个链路
- 限流:保护被调用方,防止大流量把服务压垮
- 超时控制:避免浪费资源在不可用节点上
请求成功率
-
负载均衡:服务A调用服务B时,尽可能均匀调用服务B的某个节点,不至于节点压力过大,可以一定程度提高请求成功率。
- 重试:服务A第一次调用服务B失败,进行第二次请求(重试),直至三次请求都失败才算失败。重试可以增加请求成功率。
长尾请求
长尾请求就是明显高于平均响应时间的占比较小的请求。关于延迟有一个常用的P99标准, 也就是99%的请求延迟要满足在一定耗时以内, 1%的请求会大于这个耗时, 而这1%就可以认为是长尾请求。
什么原因造成长尾
共享资源竞争, 周期性的垃圾回收, 运维活动(比如日志备份), 硬件或者软件故障,网络的抖动,都有可能造成。
如何解决长尾
Backup Request(备份请求)
正常情况下,服务A给服务B发送请求,如果请求失败,会进行第二次重试请求,返回成功的话,总共花费t1+t2的时间将请求成功处理并返回。
使用Backup Request(备份请求),服务A给服务B发送请求,在一定时间内(t3)没有返回,就会发送第二次请求(备份请求),成功响应并返回,那么总共花费t4的时间就完成本次请求。一定程度上减少了长尾请求的延时。
易用性
-
开箱即用
合理的默认参数选项,丰富的文档
-
周边工具
生成代码工具、脚手架工具
扩展性
- Middleware中间件
- Option
- 编解码层
- 协议层
- 网络传输层
- 代码生成工具插件扩展
观测性
- Log日志
- Metric监控:通过面板监控服务的每秒查询数(QPS)、延迟
- Tracing链路跟踪:排查服务超时问题,每个阶段的耗时
- 内置观测性服务
高性能
目标
- 高吞吐:在一定时间内,尽可能多的处理请求
- 低延迟:处理每个请求的时间尽可能快
场景
- 单机多机
- 单连接多连接
- 单/多client - 单/多server
- 不同大小的请求包
- 不同请求类型:例如 pingpong.streaming等
手段
-
连接池、多路复用
尽可能提高连接的复用率,减少重复连接带来的额外开销
-
高性能编解码协议
-
高性能网络库