远程函数调用(RPC - Remote Promote Proceduce Calls)
远程函数调用需要解决的问题:
- 函数映射
- 数据转换成字节流
- 网络传输
与RPC有关的知识点
- IDL(Interface description language)文件:IDL通过一中中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信。
- 生成代码:通过编译器工具把IDL文件转换成语言对应的静态库。
- 编解码:从内存中表示到字节序列的转换称为编码,反之为解码,也常叫做系列化和反序列化。
- 通信协议:规范了数据在网络中的传输内容和格式。除必须的请求/响应数据外,通常还会包含额外的元数据。
- 网络传输:通常基于成熟的网络库走TCP/UDP传输。
一次RPC的完整过程:
RPC的好处:
- 单一职责,有利于分工协作和运维开发。
- 可扩展性强,资源使用率更优。
- 故障隔离,服务的整体可靠性更高。
RPC自身含有众多优势的同时,也带来了一系列的问题,比如在面对服务宕机、网络异常或者请求量激增时都会产生许多问题,为了解决这些问题,也就出现了 — RPC框架。
RPC框架的分层设计
Apache Thrift的分层设计:
那么接下来让我们依次学习这些层次的内容。
编解码层 — 生成代码:
我们可以使用不同的语言编写RPC服务,但是需要依赖同一份IDL文件,也就是同一个约束,来生成不同语言的codeGen,那么就需要固定一些IDL文件的构成数据,IDL文件中的数据格式可以有很多种,比如语言特定的格式、文本格式和二进制编码,其中二进制编码具有跨语言和高性能的有点,所以一般常用二进制编码作为IDL的数据构成格式。
二进制编码 — TLV编码:
选型需要注意的问题:
- 兼容性
- 通用性
- 性能(空间复杂度和时间复杂度)
协议层:
相关概念:
特殊结束符:一般我们在某种协议下进行传包时,需要具有一种特殊字符作为每个协议单元结束的标志,称为特殊结束符。一般特殊结束符存在于定长协议中。
变长协议:协议由定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度。
协议构造:
当我们拿到数据的时候,会从内存中先取出信息,通过magicNumber 解析出是什么协议和什么类型的协议,然后拿到数据体的编码方式信息,依靠编码方式进行解码,最终得到数据。
网络通信层:
socket编程过程:
网络库:
现实中开发的时候一般使用封装好的网络库来进行网络通信,下面介绍几个网络库的衡量指标,可以作为选型时要考察的核心指标:
-
提供易用API
- 封装底层Socket API
- 连接管理和事件分发
-
功能
- 协议支持:tcp、udp和uds等
- 优雅退出、异常处理等
-
性能
- 应用层buffer减少copy
- 高性能定时器、对象池等
RPC关键指标
稳定性:
保障策略:
熔断 — 保护调用方,防止被调用的服务出现问题而影响到整个链路
限流 — 保护被调用方,防止大流量把服务压垮
超时控制 — 避免浪费资源在不可用的服务节点上
三种保障策略都可以归为**降级措施**。
提高请求成功率:
这里提到两种方式 — 负载均衡和重试
通过负载均衡,每个服务设置多个节点,将网络请求负担均匀的平坦给每个结点,防止在一个服务中请求压力过大导致请求失败。
当我们进行请求时,有些请求可能会比较慢,被当做传送失败处理,重试即每次进行请求时重试多次,只有当重试三次之后,才视为真正的传送失败。
长尾请求:
长尾请求 — 明显高于平均响应时间的占比比较小的那些请求。
提高长尾请求命中率 — Backup Request(右图)
左图为常规请求
RPC框架通常以注册中间件的方式将这些措施应用起来。
易用性:
- 开箱即用:合理的默认参数选项、丰富的文档。
- 周边工具 — 生成代码工具、脚手架工具。
扩展性:
- Middleware
- Option
- 编解码层
- 协议层
- 网络传输层
- 代码生成工具插件扩展
中间件执行流程:
观测性:
-
传统三件套
- Log(日志)
- Metric(监控)
- Tracing(跟踪)
-
框架内置观测性服务:RPC框架自主暴露出用户使用时配置是什么、环境变量、线程、协程和使用了什么中间件。
高性能:
场景:
- 单机多机
- 单连接多连接
- 单/多client — 单/多server
- 不同大小的请求包
- 不同请求类型:例如pingpong、streaming等
目标:
- 高吞吐
- 低延迟
优化手段:
- 连接池
- 多路复用
- 高性能编解码协议
- 高性能网络库
企业实践
字节内部实现的RPC框架 — Kitex
Kitex介绍:
整体架构:
自研网络库 — Netpoll
Go原生的网络库有部分缺陷,比如原生库无法感知连接状态,原生库存在goroutine暴涨的风险,一个连接一个goroutine的模式,连接利用率低下,存在大量goroutine占用调度开销,影响性能。
解决的问题:
- 解决无法感知连接状态问题:引入epoll主动监听机制,感知连接状态。
- 解决goroutine暴涨的风险:建立goroutine池,复用goroutine。
- 提升性能:引入Nocopy Buffer,向上层提供NoCopy的调用接口,编解码层面零拷贝。
扩展性设计
性能优化
- 网络库优化:包括调度优化、LinkBuffer和Pool
- 编解码优化:Codegen、JIT
合并部署
微服务本身带来了很多好处,但它也带来了微服务过微的问题,传输和序列化开销越来越大,所以字节内部采用了将亲和性强的服务实例尽可能调度到同一个物理机,远程RPC调用优化为本地IPC调用的思路来减少不必要的微服务调用带来的问题,减少序列化所需要的开销。也就是合并部署。