RPC对于分布式/微服务不可或缺。
一次RPC的过程:以调用方为例
-
首先定义IDL文件
- IDL指以中立方式描述接口的语言,它使得不同平台上运行的对象/不同语言编写的程序可以相互通信。caller/callee依赖同一份IDL文件。
-
通过工具把IDL文件转换成语言对应的静态库代码(内部封装了编解码的逻辑)
-
编解码/序列化
-
通信协议封装
-
网络传输
一个RPC框架需要解决的问题有:
- 函数映射
- data to byte stream
- 网络传输
- 服务宕机时怎么办?网络异常时如何保证消息送达?请求量突增导致服务无法及时处理时如何应对?
分层设计
与调用过程对应,RPC框架采用分层设计,分为 业务代码层 -- 编解码层(包含代码生成) -- 协议层 -- 网络通信层
编解码层
编码后的数据格式可以是:
-
语言特定格式(许多语言内置了将内存对象直接编码为byte stream的功能)
-
文本格式(json, xml, csv等)
- 易读,但数字的编码易有歧义。json在一些语言中的序列化反序列化需要采用反射机制,因而性能较差
-
二进制编码(如Thrift的BinaryProtocol或Protobuf)
-
T(ag)L(ength)V(alue)编码
Tag可以理解为类型。除了类型和长度,在Thrift中,编码时不会把field的字符串编码进去,而是用field tag(简单理解为记录在被序列化struct中的序号)来代替,这样就压缩了序列化后的大小
- 结构清晰,扩展性好。但T与L占据了额外的内存开销,大部分字段都是基本类型的时候,就很浪费空间
-
选型时要考虑兼容性(支持添加新字段等)、通用性、性能(编码耗时及序列化后的大小)。
协议层
最简单的一种协议,是以特殊字符作为一个unit结束的标志(如HTTP协议头以CRLF作为结尾)。这种做法,一是要防止用户传的数据里带有结束符,而是必须将整个unit读完才能进行处理。
升级成变长协议,以定长+不定长部分组成;定长的部分会有fields描述不定长的长度。
协议构造
协议定长部分包含的信息总行,值得注意的是
- sequence number,是多路复用的关键
- header magic,表示版本信息,用于解析时的快速校验
通信层
通信基于Sockets API。 网络库需要做到:
- 封装底层sockets API,提供连接的管理和事件分发的API
- 支持tcp等协议,提供graceful exit,exception handling功能
- 应用层buffer减少copy,使用定时器、对象池提升性能等
评价RPC框架的关键指标
- 稳定性(具体可以看微服务相关笔记关于治理的内容)
- 易用性
- 合理的default vals,丰富的docs和脚手架工具
- 扩展性
- 于不同层提供丰富的扩展点,可以方便地添加中间件等
- 可观测性
- log, metrics, tracing, 内置观测服务
- 高性能(高吞吐低延迟)
- 通过连接池、多路复用、高性能编解码协议与网络库等实现
延伸阅读: