这是我参与「第五届青训营」伴学笔记创作活动的第21天
基本概念
RPC调用需要解决的问题
- 函数映射:每个函数都需要有自己的ID,并传输参数
- 数据转换成字节流
- 网络传输
一次 RPC 的完整过程
- IDL文件:通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以互相通信
- 生成代码:通过编译工具把IDL文件转换成对应的静态库
- 编解码:==序列化与反序列化
- 通信协议:规范了数据在网络中的传输内容和格式。除去必须的请求/相应数据外,通常还会包含额外的元数据
- 网络传输:通常基于成熟的网络库走TCP/UDP传输
RPC优点
- 单一职责,有利于分工协作与运维开发
- 可扩展性强,资源使用率更优
- 故障隔离,服务的整体性可靠性更高
RPC 的问题
- 服务宕机如何感知?
- 遇到网络异常应该如何应对?
- 请求量暴增怎么处理?
分层设计
IDL数据格式
- 语言特定格式:例如Java有java.io.Serializable
文本格式:例如 JSON、XML、CSV 等,具有人类可读性,但是不太严谨二进制编码:常见有 Thrift ,Protobuf,实现可以有多种形式,例如 TLV 编码 和 Varint 编码
TLV编码
Tag:标签,类型 Length:长度 Value:值,Value也可以是一个TLV结构
- 兼容性:支持自动增加新的字段,而不影响老的服务,提高系统灵活性
- 通用性:支持跨平台,跨语言,流行程度
- 性能:从空间和时间两个角度来考虑,即编码后数据大小和编码所需时长
协议层
概念:
特殊结束符:以一个特殊字符作为每个协议单元结束的标志,eg:\r\n
变长协议:以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度,eg:length
THeader 协议:
LENGTH:数据包大小不包含自身HEADER MAGIC:版本号SEQUENCE NUMBER:数据包的seqIS,用于多路复用,单连接内递增HEADER SIZE:头部长度,从14个字节开始一直阶段到PAYLOAD之前PROTOCOL ID:编码方式,Binary和Comoact两种TRANSFORM ID:压缩方式INFO ID:传递一些metta信息RAYLOAD:消息体
网络通讯层
使用socket传输
RPC关键指标
稳定性
- 保障策略
- 熔断:保护调用方
- 限流:保护被调用方
- 超时控制:避免资源浪费在不可用节点上
- 请求成功率
- 负载均衡
- 重试
- 长尾请求:明显高于平均响应时间的请求
- BackupRequest:备份请求,如右图,t3为设置的阈值,接受Resp2,而忽略掉Resp1
易用性
- 开箱即用:合理的默认参数选项、丰富的文档
- 周边工具:生成代码工具、脚手架工具
扩展性
- Middleware(中间件):middleware 会被构造成一个有序调用链逐个执行
- 比如服务发现、路由、负载均衡、超时控制等
- Option:作为初始化参数
- 核心层是支持扩展的:编解码、协议、网络传输层
- 代码生成工具也支持插件扩展
观测性
- 三件套:
Log、Metric(监控面板) 和 Tracing(链路追踪) - 内置观测性服务,用于观察框架内部状态
- 当前环境变量
- 配置参数
- 缓存信息
- 内置 pprof 服务用于排查问题
高性能
- 连接池和多路复用:复用连接,减少频繁建联带来的开销
- 高性能编解码协议:Thrift、Protobuf、Flatbuffer 和 Cap'n Proto 等
- 高性能网络库:Netpoll 和 Netty 等
企业实践
字节自研网络库 [Netpoll]
(link.juejin.cn?target=https%3A%2F%2Fwww.cloudwego.io%2Fzh%2Fdocs%2Fnetpoll%2F)
背景:
- 原生库
无法感知连接状态- 使用连接池时,池中存在失效连接,影响连接池的复用
- 原生库存在
goroutine 暴涨的风险- go原生的net库时一个连接一个协程的模式,由于连接利用率低下,存在大量的协程占用调度开销,影响性能
解决方案:
- 解决无法主动感知连接状态问题:
- 引入epoll主动监听机制,感知连接状态
- 解决协程暴涨风险:
- 建立gorutine池,复用gorutine
- 提升性能:
- 引入
Nocopy Buffer,向上层提供NoCopy的调用接口,编码层面实现**零拷贝 **
- 引入
扩展性
支持多协议,也支持灵活的自定义协议扩展
性能优化
参考 字节跳动 Go RPC 框架 KiteX 性能优化实践
网络库优化
调度优化
epoll_wait在调度上的控制,提高调度效率gopool重用goroutine降低同时运行的协程数
LinkBuffer
- 读写并行无锁,支持nocopy地流式读写
- 高效扩缩容
- ** NoCopy Buffer池化**,减少内存拷贝,从而减少 GC
POOL
- 引入内存池和对象池,减少GC开销
编解码优化
Codegen:
- 预计算提前分配内存,减少内存操作次数,包括内存分配与拷贝
- inline减少函数调用次数和避免不必要的反射操作
JIT:即时编译
- 无生产代码,将编译过程移到了程序的加载(或首次解析)阶段,可以一次性编译生成对应的 codec 并高效执行,改善用户体验同事有更强的编解码性能
- 基于JIT编译技术的高性能Thrift编解码器
合并部署
- 微服务过微,传输和序列化开销越来越大
- 将亲和性强的服务实例尽可能调度到同一个物理机,远程RPC调用优化为本地IPC调用
改造:
- 中心化的部署调度和流量控制
- 基于共享内存的通讯协议
- 定制化的服务发现和连接池实现
- 定制化的服务启动和监听逻辑