深入浅出RPC框架 | 豆包MarsCode AI刷题
课程目录
- RPC框架分层设计
- 关键指标分析与企业实践
RPC框架分层设计
课程目录
- 基本概念
- 分层设计
- 关键指标
- 企业实践
基本概念
本地函数调用
假定我们需要调用一个本地函数,我们只需要定义一个栈,把值压入栈中,在函数中取出栈内的值,进行计算,然后把结果压栈,最后在主函数再把结果从栈中取出,赋值给result
远程函数调用(RPC调用)
RPC - Remote Procedure Calls
RPC需要解决的问题:
- 函数映射
- 数据转换成字节流
- 网络传输
例如:网上商城调用支付服务
RPC概念模型
RPC的过程有五个模型组成:
- User
- User-Stub
- RPC-Runtime
- Server-Stub
- Server
调用端与被调用端,中间隔着网络
用户调用用户stub,打包数据交给RPCRuntime,然后发送给对端的RPCRuntime,服务stub解析,把数据交给服务。服务处理好后,将返回数据原路返回
一次RPC的完整过程
-
IDL文件(Interface description language)
- IDL通过同一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信
-
生成代码
- 通过编译器工具把IDL文件转换成语言对应的静态库
-
编解码
- 从内存中表示到字节序列的转换称为编码,反之为解码,也常叫作序列化和反序列化
-
通信协议
- 规范了数据在网络中饭传输内容和格式。除了必须的请求/响应数据外,通常还会包含额外的元数据
-
网络传输
- 通常基于成熟的网络库走TCP/UDP传输
RPC的好处
- 单一职责,有利于分工协作和运维开发
- 可扩展性强,资源使用率更优
- 故障隔离,服务的整体可靠性更高
RPC带来的问题
- 服务宕机,对方应该如何处理?
- 在调用的过程中发生网络异常,如何保证消息的可达性?
- 请求量突增导致服务无法及时处理,有哪些应对措施?
以上这些问题,由RPC框架来处理
分层设计
- 编解码层
- 协议层
- 网络通信层
分层设计
以Apache Thrift为例
- 用户自己编写的业务逻辑代码
- 通过代码生成工具把IDL文件转换成不同语言对应的lib代码,里面封装了编解码逻辑
- 框架的编解码层
- 框架的协议层
- 框架的网络通信层
编解码层
生成代码
客户端与服务端依赖同一份IDL文件,生成不同语言的CodeGen
你可以用不同的语言去编写这个RPC服务,但需要IDL文件来约束
数据格式
-
语言特定的格式
- 许多编程语言都内建了将内存对象编码为字节序列的支持,如Java有java.io.Serializable
-
文本格式
- JSON、XML、CSV等文本格式,具有人类可读性
-
二进制编码
- 具备跨语言和高性能等优点,常见有Thritf的BinaryProtocol,Protobuf等
二进制编码
TLV编码:
- Tag:标签,可以理解为类型
- Lenght:长度
- Value:值,Value也可以是个TLV结构
选型
-
兼容性
- 支持自动增加新的字段,而不影响老的服务,这将提高系统的灵活度
-
通用性
- 支持跨平台跨语言
-
性能
- 从空间和时间两个维度来考虑,也就是编码后数据大小和编码耗费时长
协议层
概念
-
特殊结束符
- 一个特殊字符作为每个协议单元结束的标志
-
变长协议
- 定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度
协议构造
- LENGTH:数据包大小,不包含自身
- HEADER MAGIC:标志版本信息,协议解析时候快速校验
- SEQUENCE NUMBER:表示数据包的seqID,可用于多路复用,单链接内递增
- HEADER SIZE:头部长度,从第十四个字节开始计算一直到PAYLOAD前
- PROTOCOL ID:编解码方式,有Binary和Compact两种
- TRANSFORM ID:传递一些特定的meta信息
- PAYLOAD:消息体
协议解析
- 读取MagicNumber魔法数所在位置,知道你这是什么类型的协议
- 读取PayloadCodec编码方式
- 解码Payload
网络通信层
Sockets API
首先必须要知道两个信息:IP和端口
- 最开始的时候客户端和服务器都是处于CLOSED关闭状态。主动打开连接的为客户端,被动打开连接的是服务器
- 第一次握手: 客户端向服务器端发送报文 证明客户端的发送能力正常
- 第二次握手:服务器端接收到报文并向客户端发送报文 证明服务器端的接收能力、发送能力正常
- 第三次握手:客户端向服务器发送报文 证明客户端的接收能力正常
- 第三次握手是为了保证,不会出现情况"来自客户端的已经失效的连接请求报文突然又抵达服务器"
网络库
-
提供易用API
- 封装底层Socket API
- 连接管理和时间分发
-
功能
- 协议支持:TCP,UDP和UDS等
- 优雅退出,异常处理等
-
性能:
- 应用层buffer减少copy
- 高性能定时器,对象池等
RPC关键指标分析与企业实践
- 稳定性
- 易用性
- 扩展性
- 观测性
- 高性能
稳定性
保障策略
- 熔断:保护调用方,防止被调用的服务出现问题而影响整个链路
- 限流:保护被调用方,防止大流量把服务压垮
- 超时控制:避免浪费资源在不可用节点上
请求成功率
负载均衡:分流流量
重试:超时控制
长尾请求
明显高于平均响应时间的那部分,占比比较小的请求(TCP99)
- A向B发送Req1
- B返回AResp1
- A返回BReq2
- B返回AResp2
这是正常的情况
考虑长尾请求的长响应时间,可能会出现,Resp1返回A之前,A就已经发出了Req2,于是发生错误
注册中间件
框架如何处理这些问题?——注册中间件
基本上创建的时候通过一个可选的方式,就可以在中间件把这些功能给加上
易用性
-
开箱即用
- 合理的默认参数选项、丰富的文档
-
周边工具
- 生成代码工具,脚手架工具
简单易用的命令行工具
- 生成服务代码脚手架
- 支持protobuf和thrift
- 内置功能丰富的选项
- 支持自定义的生成代码插件
扩展性
- Middleware
- Option
- 编解码层
- 协议层
- 网络传输层
- 代码生成工具插件扩展
观测性
- Log、Metric、Tracing
- 内置观测性服务
RPC Framework
高性能
-
场景
- 单机多机
- 单连接多连接
- 单/多client 单/多server
- 不同大小的请求包
- 不同请求类型:譬如pingpong、streaming
-
目标
- 高吞吐
- 低延迟
-
手段
- 连接池
- 多路复用
- 高性能编解码协议
- 高性能网络库
企业实践
- 整体架构
- 自研网络库
- 扩展性设计
- 性能优化
- 合并部署
整体架构 - Kitex
- Kitex Core:核心组件
- Kitex Byted:与公司内部基础设施集成
- Kitex Tool:代码生成工具
自研网络库 - 背景
-
原生库无法感知连接状态
- 在使用连接池时,池中存在失效连接,影响连接池的复用
-
原生库存在goroutine暴涨的风险
- 一个链接一个goroutine的模式,由于链接利用率底下,存在大量goroutine占用调度开销,影响性能
-
解决无法感知连接状态问题
- 引入epoll主动监听机制,感知连接状态
-
解决goroutine暴涨的风险
- 建立goroutine池,复用goroutine
-
提升性能
- 引入Nocopy Buffer,向上层提供NoCopy的调用接口,编解码层面零拷贝
扩展性设计
支持多协议,也支持灵活的自定义协议扩展
性能优化 - 网络库优化
-
调度优化
- epoll_wait
- gopool重用goroutine降低同时运行协程数
-
LinkBuffer
- 读写并行无锁,支持nocopy地流式读写
- 高效扩缩容
- Nocopy Buffer池化,减少GC
-
Pool
- 引入内存池和对象池,减少GC开销
性能优化 - 编解码优化
-
Codegen
- 预计算并预分配内存,减少内存操作次数,包括内存分配和拷贝
- Inline减少函数调用次数和避免不必要的反射操作等
- 自研了Go语言实现的Thrift IDL解析和代码生成器,支持完善的Thrift IDL语法和语义检查,并支持了插件机制 - Thriftgo
-
JIT
- 使用JIT编译技术改善用户体验的同时带来更强的编解码性能,减轻用户维护生成代码的负担
- 基于JIT编译技术的高性能动态Thrift编解码器 - Frugal
合并部署
微服务过微,传输和序列化开销越来越大
将亲和性强的服务实例尽可能调度到一个物理机,远程RPC调用优化为本地IPC调用
- 中心化的部署调度和流量控制
- 基于共享内存的通信协议
- 定制化的服务发现和连接池实现
- 定制化的服务启动和监听逻辑