这是我参与「第三届青训营 -后端场」笔记创作活动的的第12篇笔记.
RPC
- 需要解决的问题
-
函数映射
-
网络传输
-
数据转换成字节流
-
《Implementing Remote Procedure Calls》
- User
- User-Stub
- RPC-Runtime
- Server-Stub
- Server
一次RPC的完整过程
- IDL文件 Interface description Language 需要有一种方式声明方法及其参数,因此使用IDL文件来描述接口,各接口遵循该规范,可以让不同平台的对象和不同语言的程序相互通信
- 生成代码 编译器工具把IDL文件转换为语言对应的静态库
- 编解码 编码是内存中的格式转换为字节序列,反之为解码。
- 网络传输 TCP/UDP
- 通信协议 规范了数据在网络中的传输格式和内容,除必须的请求/响应数据外,可以还会包含额外元数据。
好处
- 单一职责
- 可扩展性强,资源利用率高
- 故障隔离,可靠性高
带来的问题
- 依赖服务宕机后,如何处理?
- 调用过程中出现异常,如何保证可达性?
- 请求量突增导致服务无法及时处理,如何应对?
分层设计
以Apache Thrift为例。
编码层
客户端和服务端对于同一份IDL文件生成不同语言不同场景下的CodeGen。
- 语言格式特定:内建了将内存对象编码为字节序列的支持,如java.io.Serializable
- 文本格式:如Json、Xml、Csv
- 二进制编码:跨语言,高性能,如Thrift的BinaryProtocal、Google的Protobuf。 以BinaryProtocal为例,它的底层实现方式是TLV编码
TLV编码
- Tag
- Length
- Value.也可以嵌套TLV
struct Person{
1: required string userName,
2: optional i64 favoriteNumber,
3: optional list<string> interests
}
协议层
- 特殊结束符:以特殊字符作为协议单元结束标志。如HTTP协议
- 变长协议:以定长+不定长的部分组成,定长的部分需要描述不定长部分的长度
协议构造
- LENGTH
- HEADER MAGIC:标识版本信息,协议解析时快速效验
- SEQUENCE NUMBER: 标识数据包seqid,可用于多路复用(多个请求流在发送,单连接内递增
- HEADER SIZ:头部长度,从第14个字节计算到PAYLOAD前
- PROTOCAL_ID:编解码方式,Binary/Compact
- TRANSFORM_ID,压缩方式,zlib/snappy
- INFO I:传递定制meta信息
- PAYLOAD: 消息体
那么 框架如何解析协议?
首先 框架从内存读取部分数据(MAGIC NUMBER),知道是什么类型的协议。而后读取编码方式,知道用什么解码,解码后交给对方处理。
-----Peek--->MagicNumber-----Peek--->PayloadCodec---Decode--->Payload
网络通信层(Socket API
SOCKET API介于应用层和传输层中间。
socket创建套接字会有bind操作,会把套接字返回到ip+port,而后去listen,并把监听到的信息放在队列。队列有一定的长度限制(BACKLOAD),LINUX默认128。客户端发起请求会进行connect(),得到客户端的ip后使用read和write进行通信。默认阻塞默认读取数据。读取数据完成使用close关闭套接字。如果一方关闭,另一方尝试读会返回EOF,尝试写会返回错误。
一般会使用封装好的网络库作为rpc的网络通信层。
关键指标
稳定性(使用降级
- 熔断 避免雪崩,保护调用方
- 限流
- 超时控制
稳定性的一个指标是请求成功率。可以通过负载均衡和重试来提高。
还有长尾请求,明显高于平均响应时间但是占比较小的请求。有个指标就是PC99,后面的1%就是长尾请求。
如何提高长尾请求的成功率?
Backup Request.发送请求后设置一个时间阈值,如果在时间阈值内没有返回,再次发送请求,一般会很快得到返回。
以上是提高稳定性的策略,那么框架如何应用这些策略?
一般框架使用中间件方式,将这些策略设置为可选的配置。
易用性
- 开箱即用,合理的参数配置,丰富的文档。
- 生成代码工具/脚手架工具。
扩展性
需要尽可能提供多的扩展点。
- 中间件.有序调用链添加策略
- Option
- 编解码
- 协议层
- 传输层
- 代码生成工具插件扩展
观测性
- Log
- Metric 监控qps或延迟
- Tracing
高性能
- 目标: 高吞吐,低延迟
- 场景,不同场景下表现不同
- 单机多机
- 单链接多连接
- 单/多client 单/多server
- 不同大小请求包
- 不同请求类型,例如pingpong/streaming
- 优化手段都是通用的
- 连接池
- 多路复用
- 高性能编解码协议
- 高性能网络库
企业实践
Kitex
- Kitex Core 核心组件
- Kitex Byted 与公司内部基础设施集成
- Kitex Tool 代码生成工具
自研网络库的原因:
- 原生库go/net无法感知连接状态,使用连接池时池中出现失效连接,影响连接池复用。 * 原生库中,一个连接一个Goroutinue,连接利用率低下,存在大量goroutinue占用调度开销,影响性能。 自研网络库-epoll
- 引入epoll主动监听机制,感知连接状态
- 建立goroutine池,复用goroutine
- 引入Nocopy Buffer,向上层提供NoCopy的调用接口,编解码界面零拷贝
性能优化之网络库优化
- 调度优化
- epoll_wait
- gopool重用goroutinue
- LinkBuffer
- 读写并行无锁,支持nocopy流式读写
- 高效扩缩容
- Nocopy Buffer池化,减少GC
- Pool
- 引入内存池和对象池
性能优化之编解码优化
- Codegen
- 预结算并预分配内存,减少内存操作次数,包括内存分配和拷贝
- Inline减少次数调用次数,避免不必要反射
- ThriftGo
- JIT(Just in time
- Frugal
合并部署
将亲和性强的服务实例尽可能调度到同一个物理机,远程RPC调用优化为本地IPC调用
- 中心化的部署调度和流量控制
- 基于共享内存的通信协议
- 定制化的服务发现和连接池实现
- 定制化的服务启动和监听逻辑