这是我参与「第五届青训营 」伴学笔记创作活动的第 10 天
RPC原理
论文基础web.eecs.umich.edu/~mosharaf/R…
RPC 框架分层设计
编解码层
-
数据格式
- 语言特定格式:例如 java.io.Serializable
- 文本格式:例如 JSON、XML、CSV 等,可读性好
- 二进制编码:常见有 Thrift 的 BinaryProtocol,Protobuf,实现可以有多种形式,例如 TLV 编码 和 Varint 编码,跨语言,高性能
编号1,2,3就是标签,可以表示类型
-
选型考察点
-
生成代码和编解码层相互依赖,框架的编解码应当具备扩展任意编解码协议的能力
协议层
概念
协议构造
- LENGTH 字段 32bits,包括数据包剩余部分的字节大小,不包含 LENGTH 自身长度
- HEADER MAGIC 字段16bits,值为:0x1000,用于标识 协议版本信息,协议解析的时候可以快速校验
- FLAGS 字段 16bits,为预留字段,暂未使用,默认值为 0x0000
- SEQUENCE NUMBER 字段 32bits,表示数据包的 seqId,可用于多路复用,最好确保单个连接内递增
- HEADER SIZE 字段 16bits,等于头部长度字节数/4,头部长度计算从第14个字节开始计算,一直到 PAYLOAD 前(备注:header 的最大长度为 64K)
- PROTOCOL ID 字段 uint8 编码,取值有:
- ProtocolIDBinary = 0 - ProtocolIDCompact = 2
- NUM TRANSFORMS 字段 uint8 编码,表示 TRANSFORM 个数
- TRANSFORM ID 字段 uint8 编码,表示压缩方式 zlib or snappy
- INFO ID 字段 uint8 编码,具体取值参考下文,用于传递一些定制的 meta 信息 -
- PAYLOAD 消息内容
协议解析
先读取魔法数,知道是什么类型协议
读取编码方式,便知道如何解码
Thrift 的 THeader 协议
- 协议解析
网络通信层
-
阻塞 IO 下,耗费一个线程去阻塞在 read(fd) 去等待用足够多的数据可读并返回。
-
非阻塞 IO 下,不停对所有 fds 轮询 read(fd) ,如果读取到 n <= 0 则下一个循环继续轮询。
第一种方式浪费线程(会占用内存和上下文切换开销),第二种方式浪费 CPU 做大量无效工作。而基于 IO 多路复用系统调用实现的 Poll 的意义在于将可读/可写状态通知和实际文件操作分开,并支持多个文件描述符通过一个系统调用监听以提升性能。 网络库的核心功能就是去同时监听大量的文件描述符的状态变化(通过操作系统调用),并对于不同状态变更,高效,安全地进行对应的文件操作。
RPC 框架核心指标
稳定性
从某种程度上讲超时、限流和熔断也是一种服务降级的手段 。
-
请求成功率
- 负载均衡
- 重试:限制重试次数,不要一直重试,因为重试会导致下游负载升高
-
长尾请求
- 解决措施:BackupRequest
长尾请求是指明显高于均值的那部分占比-较小的请求。业界关于延迟有一个常用的P99际准,单个请求响应耗时从小到大排列,前99%的请求都正常,最后的1%请求就是长尾请求。复杂的系统中,长尾延时总是会存在。造成这个的原因非常多,常见的有网络抖动,GC,系统调度
解决措施:我们预先设定一个阈值t3(比超时时间小,通常建议就是满足正常前99%的请求能达到的耗时),当Req1发出去后超过t3时间都没有返回,那我们直接发起重试请求Req2,这样相当于同时有两个请求运行。然后等待请求返回,只要Resp1或者Resp2任意一个返回成功的结果,就可以立即结束这次请求,这样整体的耗时就是t4,它表示从第一个请求发出到第一个成功结果返回之间的时间,相比于等待超时后再发出请求,这种机制能大大减少整体延时
易用性
扩展性
- Middleware:middleware 会被构造成一个有序调用链逐个执行,比如服务发现、路由、负载均衡、超时控制等
- Option:作为初始化参数
- 核心层是支持扩展的:编解码、协议、网络传输层
- 代码生成工具也支持插件扩展
观测性
-
三件套:Log、Metric 和 Tracing
-
内置观测性服务,用于观察框架内部状态
- 当前环境变量
- 配置参数
- 缓存信息
- 内置 pprof 服务用于排查问题
高性能
- 高吞吐:请求时间多
- 低延迟:请求返回时间近可能短
- 连接池和多路复用:复用连接,减少频繁建联带来的开销
- 高性能编解码协议:Thrift、Protobuf、Flatbuffer 和 Cap'n Proto 等
- 高性能网络库:Netpoll 和 Netty 等
小结
- 框架通过中间件来注入各种服务治理策略,保障服务的稳定性
- 通过提供合理的默认配置和方便的命令行工具可以提升框架的易用性
- 框架应当提供丰富的扩展点,例如核心的传输层和协议层
- 观测性除了传统的Log、Metric和Tracing之外,内置状态暴露服务也很有必要
- 性能可以从多个层面去优化,例如选择高性能的编解码协议和网络库