RPC框架|青训营笔记
这是我参与【第五届青训营】伴学笔记创作活动的第13天。
一、重点内容
- RPC框架分层设计
- RPC关键指标分析
- 企业实践
二、详细知识点
1. 基本概念
1.1 本地函数调用
- 将a、b值压栈
- 通过函数指针找到calculate函数,进入函数取出值2,3赋予x,y
- 计算x*y,存储至z
- 将z值压栈,从calculate返回
- 从栈中取出z返回值,赋给result
1.2 远程函数调用(Remote Procedure Calls)
- 需要解决的问题
- 函数映射
- 数据转换成字节流
- 网络传输
1.3 RPC概念模型
- 由5个模型组成
- User
- User-Stub
- RPC-runtime
- Server-Stub
- Server
1.4 一次RPC的完整过程
- IDL文件 Interface Description Language文件
- 通过中立方式描述接口
- 使得在不同平台上运行的对象和不同语言编写的程序可以相互通信
- 生成代码
- 通过编译器工具把IDL转换成语言对应的静态库
- 编解码
- 从内存中表示到字节序列的转换称为编码
- 反之为解码
- 常称作序列化与反序列化
- 通信协议
- 规范了数据在网络中的传输内容和格式
- 除必须的请求/响应数据外还包含额外的原数据
- 网络传输
- 基于成熟的网络库
- TCP/UDP传输
1.5 RPC优势
- 单一职责,有利于分工协作和运维开发
- 可扩展性强,资源使用率更优
- 故障隔离,服务整体可靠性更高
1.6 RPC带来的问题
- 问题
- 服务宕机如何处理?
- 调用过程中发生网络异常如何保证消息可达性?
- 请求量突增导致服务无法及时处理,如何应对?
- 解决:RPC框架
2. 分层设计
2.1 分层设计
2.2 编解码层
- 生成代码
- 客户端和服务端依赖同一份IDL文件
- 生成不同语言的CodeGen
- 数据格式
- 语言特定格式:编程语言内建内存对象编码为字节序列的支持
- java.io.Serializable
- 语言特定,其他无法读取
- 文本格式:JSON、XML、CSV 人类可读性
- 约束较为宽松
- 二进制编码
- 跨语言、高性能
- Thrift binaryProtocol、Protobuf等
- 语言特定格式:编程语言内建内存对象编码为字节序列的支持
- 二进制编码
- TLV编码:Tag-Length-Value
- 标签:类型
- Length:长度
- Value:值(也可以是TLV结构)
- TLV编码:Tag-Length-Value
- 编解码选型
- 兼容性:需要支持自动增加新字段而不影响老服务,增加灵活度
- 通用性:支持跨平台、跨语言
- 性能:空间、时间,编码后数据大小、编码耗费时长
2.7 协议层
- 概念
- 特殊结束符:一个特殊字符作为每个协议单元结束 CRLF
- 变长协议:以定长加不定长的部分组成,定长部分描述不定长内容长度
- 协议构造
- LENGTH:数据包大小,不包含自身
- HEADER MAGIC:标识版本信息,协议解析时快速校验
- SEQUENCE NUMBER:表示数据包seqID,多路复用,单连接内递增
- HEADER SIZE:头部长度,从第14字节开始计算一直到PAYLOAD前
- PROTOCOL ID:压缩方式,如zlib和snappy
- INFO ID:传递一些定制meta信息
- PAYLOAD:消息体
- 协议解析
- 从内存中读取MagicNumber
- 读取编解码方式PayloadCodec
- 解码Payload
2.8 网络通信层
- Sockets API
- 网络库
- 提供易用API
- 封装底层SocketAPI
- 连接管理和时间分法
- 功能
- 协议支持:tcp/udp,uds
- 优雅退出,异常处理等
- 性能
- 应用层buffer减少copu
- 高性能定时器、对象池
- 提供易用API
3. 关键指标
3.1 稳定性
- 保障策略
- 熔断:保护调用方,防止被调用的服务出现问题影响整个链路
- 限流:保护被调用方,防止大流量把服务压垮
- 超时控制:避免浪费资源在不可用节点上
- 请求成功率:通过负载均衡+重试提高成功率
- 长尾请求:使用backup request,在发送一个请求后经过t再次发送一个请求(根据经验应该在t内收到回复),可以减小请求延时
- 注册中间件
3.2 易用性
- 开箱即用
- 合理的默认参数选项
- 丰富的文档
- 周边工具
- 生成代码工具
- 脚手架工具
- 生成服务代码脚手架,支持protobuf和thrift,支持功能丰富,支持自定义生成代码插件
3.3 扩展性
- 尽可能多的扩展点:Middleware、Option、编解码层、协议层、网络传输层、代码生成工具插件扩展
3.4 观测性
- log Metric Tracing
- 内置观测性服务
- 分析上述内容获得问题
3.5 高性能
- 场景
- 单机多机
- 单连接多连接
- 单/多client,单、多server
- 不同大小请求包
- 不同请求类型:pingpong、streaming
- 目标
- 高吞吐
- 低延迟
- 手段
- 连接池
- 多路复用
- 高性能编解码协议
- 高性能网络库
三、实践例——字节内部实践
1. 整体架构
1.1 Kitex
- core:核心组件
- byted:与内部基础设施集成
- tool:代码生成工具
2. 自研网络库
- 问题
- 原生库无法感知连接状态:使用连接池时池中存在失效连接,影响连接池复用
- 原生库存在Goroutine暴涨风险:一个连接一个Goroutine,连接利用率低下,大量goroutine占用调度开销影响性能
- Netpoll
- 引入epoll主动监听机制感知连接状态
- 建立goroutine池,复用goroutine
- 提升性能,引入nocopy buffer,向上提供NoCopy调用接口,实现编解码层面零拷贝
3.扩展性设计
- 支持多协议
- 支持自定义协议扩展
4. 性能设计优化
- 网络库优化
- 调度优化:epoll_wait调度上控制,gopool复用goroutine
- LinkBuffer:读写并行无锁,支持nocopy流式读写;高效扩缩容,Nocopy buffer池化,减少GC
- Pool:引入内存、对象池,减少GC开销
- 编解码优化
- codegen
- 预计算预分配内存减少内存操作次数
- Inline减少函数调用次数和不必要反射操作
- 自研go语言实现Thrift IDL解析和代码生成器,支持完善的Thrift IDL语法和语义检查,支持插件机制 Thriftgo
- JIT
- 使用JIT编译技术改善用户体验同时带来强大编解码性能,减轻用户维护生成代码的负担
- 基于JIT编译技术的高性能动态Thrift编解码器-Frugal
- codegen
5. 合并部署
- 问题:微服务过微导致传输和序列化开销越来越大
- 解决方法:
- 将亲和性强的服务实例尽可能调度一个物理机
- 远程RPC调用优化为本地IPC
- 框架进行改造
- 中心化的部署调度和流量控制
- 基于共享内存的通信协议
- 定制化服务发现和连接池实现
- 定制化的服务启动和监听逻辑
四、个人总结
本节课主要介绍了RPC的基础定义、框架的分层设计,核心三层(编解码、协议和网络传输层),以及RPC框架的核心指标。在今后的学习中还需要结合Kitex的代码实例进行理解。