Class7:RPC框架
RPC框架
- 本地函数调用:
func main (){
var a = 2
var b = 3
result := calculate(a, b)
fmt.Println(result)
return
}
func calculate(x, y int){
z := x*y
return z
}
- 调用流程:
- 将a和b的值压栈
- 通过函数指针找到calculate函数,进入函数取出栈中的值2和3,将其赋予x和y
- 计算x*y,并将结果存在z
- 将z的值压栈,然后从calculate返回
- 从栈中取出z的返回值,并赋值给result
RPC: Remote Procedre Calls,远程函数调用
-
要解决的问题:函数映射(函数指针),数据转换成字节流(压栈),网络传输
-
RPC概念模型
1984年Nelson提出了RPC过程的概念模型,User、User-Stub、RPC-Runtime、Server-Stub、Server
User发起本地调用请求 - User-Stub打包参数 - RPC-Runtime将数据通过互联网传送给对端RPC-Runtime - 对端Server-Stub解压数据 - Server端响应 - Server处理业务并得到返回值 - Server-Stub打包数据 - RPC-Runtime传送数据返回 - 解压数据 - User得到响应的返回值
- 一次RPC的完整函数
IDL(interface description language)文件:IDL通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信
生成代码:通过编译器工具把IDL文件转换成语言对应的静态库
编解码:从内存中表示到字节系列的转化称为编码,反之为解码,也叫做序列号和反序列化
通信协议:规范了数据在网络中的传输内容和格式,除了必须的请求/响应数据外,通常还包含额外的元数据
网络传输:通常基于成熟的网络库走TCP/UDP传输
- RPC优势
- 单一职责
- 可扩展性强,资源使用率优
- 故障隔离高,服务整体可靠性更高
- RPC问题
被调用端服务宕机,调用过程网络异常,请求量突增无法及时处理等 -- RPC框架
RPC框架的分层设计
编解码层
- 数据格式的分类:
语言特定的格式:java的Java.io.Serializable
文本格式:JSON、XML、CSV等,具有人类可读性
二进制编码:跨语言且高性能,Thrift的BinaryProtocol,Protobuf等
- 二进制编码中的TLV编码
TLV: Tag Length Value,标签/类型,长度,值 (尤其是简单内容,T和L比较浪费字节,有改进空间,例如Vint)
- 选型
兼容性,支持自动增加新字段,而不影响老服务,这将提供系统灵活度
通用性,支持跨平台、跨语言,流行性
性能,从时间和空间两个维度考虑,即编码后数据大小和编码耗费时长
协议层
特殊结束符:message body \r\n
变长协议:length message body,以定长加不定长部分组成,其中定长部分需要描述不定长部分的长度
协议解析:magicnumber - PayloadCode - Payload
网络通信层 - Sockets API
介于应用层和传输层之间
API:应用编程接口
- 网络库选型的核心指标
提供简单易用的API:封装底层Socket API,连接管理和事件分发
功能:协议支持TCP, UDP, UDS等,优雅推出、异常处理等
性能:应用层buffer减少复制,可通过高性能定时器、对象池等提升性能
RPC关键指标
稳定性
-
保障策略:降级 - 熔断(保护调用方,A依靠B,B依靠C,C断,则熔断BC依赖以确保AB正常运转),限流(保护被调用方,防止大流量压垮服务),超时控制(避免浪费资源在节点上)
-
请求成功率:负载均衡,重试
-
长尾请求:backup request,RC99,99%的请求都在t3实际内返回,如果t3时还没有收到response,则重新发起一起请求,降低延时
-
提高稳定性的方法:注册中间件
易用性
- 开箱即用:合理的默认参数选项,丰富的文档
- 周边工具:生成代码工具、脚手架工具(辅助生成重复代码)、支持尽可能多的协议(protobuf和thrift)内置功能丰富的选型、支持自定义的生成代码插件
扩展性
- Middleware
- Option
- 编解码层
- 协议层
- 网络传输层
- 代码生成工具插件扩展
观测性
- Log(日志)、Metric(监控)、Tracing(链路跟踪)
- 内置观测性服务
高性能
-
目标:高吞吐、低延迟
-
手段:连接池、多路复用(提高连接复用率)、高性能编解码协议、高性能网络库
企业实践
-
整体架构:Kitex Core、Kitex Toll(代码生成工具)、Kitex Byted
-
自研网络库
问题:原生库无法感知连接状态,使用连接池时可能有失效连接,影响连接池复用率。
原生库存在goroutine暴涨的风险,一个连接一个goroutine模式,由于连接利用率低下,存在大量goroutine占用调度开销,影响性能。
自研网络库:Netpoll
引入epoll主动监听机制,解决问题1,建立goroutine池,解决问题2,引入Nocopy Buffer,向上层提供NoCopy调用接口,编解码层面零拷贝,提升性能
-
扩展性设计:支持多协议,也支持灵活的自定义协议扩展
-
性能优化-网络库优化
- 调度优化:epo_wait在调度上控制,gopool重用goroutine降低同时运行协程数
- LinkBuffer:读写并行无锁,支持nocopy流式读写,高效扩缩容,Nocopy buffer池化,减少GC
- Pool:引入内存池和对象池,减少GC开销
-性能优化-编解码优化
- Codegen:预计算并预分配内存,减少内存操作次数,包括内存分配与拷贝。Inline减少函数调用次数和不必要的反射操作。自研Go语言实现的Thrift IDL解析和代码生成器,支持插件等的Thriftgo
- JIT(Just In Time):动态翻译或运行时编译,使用JIT编译技术改善用户体验,减轻用户维护生成代码的负担。基于JIT编译技术的高性能动态Thrift编解码器:Frugal
- 合并部署
问题:由于微服务过微,传输和序列化开销越大
解决方式:将亲和性强的服务实例尽可能调度到同一个物理机,远程RPC调用优化为本地IPC调用
框架改造:中心化的部署调度和流量控制、基于共享内存的通信协议、定制化的服务发现和连接池实现、定制化的服务启动和监听逻辑
总结
由本地函数调用拓展到远程函数调用时,需要经历较长的链路,通过搭建RPC框架,降低函数调用时长,通过分层设计编解码层、协议层、网络传输层容灾和提高调用性能。核心指标包括稳定性可扩展性和高性能等。字节的自研网络库Netpoll相比原生网络库,性能大大提升。