基本概念
本地函数调用
通俗来说就是传统的函数之间的调用方式,首先定义函数和对应的变量,然后再去调用它,其中函数调用时涉及到的参数传递可以直接通过声明的实参传递给形参,比如下图中的函数调用即为本地函数调用方式
远程函数调用(RPC Remote Produre Calls)
还是以上面的程序为例,只不过这一次专门将
calculate
函数的定义专门放到一台机器上去(有点大材小用了),然后再本地机器去调用这个函数实现对应的方法功能。
通俗来说本地函数调用就类比你和你的女朋友在同一个城市谈恋爱,如果两个人吵架了就当面可以去哄了,相反,远程函数调用就是异地恋了,如果吵架了只能打电话或者视频去哄了,实在不行还要✈️过去呢,代价还是蛮大的呢。。。
RPC需要解决的核心问题
- 函数映射
- 数据转换成字节流(参数如何传递到远程去)
- 网络传输
简单的RPC模型如下图所示:
一次RPC的完整过程
- IDL(Interface Description Lanage)文件:IDL通过一种比较通用的方式来描述接口,使得在不同平台运行的对象和用不同语言编写的程序之间可以相互通信
- 生成代码:把IDL文件转换成对应的静态库
- 编解码:从内存表示转换成字节序列表示,也可以叫做序列化和反序列化
- 通信协议:规定了数据在网络中的传输内容和格式
- 网络传输:基于成熟的网络库(TCP/UDP)去进行数据传输
RPC的好处
- 有利于分工协作和运维开发
- 可扩展性强
- 故障隔离,服务可靠性更高
RPC带来的问题
- 服务不可用时对方如何处理?
- 发生网络异常时如何保证消息的可达性?
- 请求量级突增导致服务无法及时处理该如何应对?
针对以上问题,就诞生了专门的RPC框架来应对
RPC框架解析
经典的RPC框架采用分层设计的思想,一般包含编解码层、协议层、网络通信层。
以Apache Thrift
为例
编解码层
生成代码
客户端和服务端依赖同一份IDL文件,然后生成不同语言对应的生成代码
IDL文件数据格式
- 语言特定格式:比如各种编程语言支持将内存对象编码为字节序列,好处是非常方便,坏处就是兼容性比较差,不同语言之间的数据格式不能直接交互
- 文本格式:JSON、XML、CSV
- 二进制编码,具备跨语言和高性能的优点,eg:
thrift
的BinaryProtocol
选型
- 兼容性
- 在移动互联网时代,业务系统需求的更新周期变得更快,新的需求不断涌现,老的系统还需要继续维护,如果序列化协议具备良好的可扩展性,即支持新增的业务字段,同时又不影响老的服务,有利于提高系统的灵活度
- 通用性
- 支持跨平台、跨语言
- 流行程度,上手快、学习成本也会降低
- 性能
- 空间开销:不仅会增加存储成本也会增加网络传输压力
- 时间开销:复杂的序列化往往也伴随较长时间的解析时间,很有可能会成为框架的瓶颈
协议层
编解码层只是将数据转换成字节流的形式,数据并没有进行传输,所以此时也没有服务调用发生。
- 以特殊结束符结尾:以一个特殊的字符作为每个协议单元结束的标志,比如
\r\n
- 变长协议:由定长+不定长部分组成,其中定长部分需要描述不定长的内容长度
网络通信层
在实际的框架中我们往往会直接选择封装好的网络库去进行通信操作(拿来主义)
网络库选取标准
- 提供易用API
- 功能:支持比较多的协议、优雅退出、异常处理
- 性能:高性能定时器、对象池
构建RPC框架之关键指标
- 稳定性(注册中间件实现)
- 保障策略:熔断、限流、超时控制->降级
- 请求成功率:负载均衡、重试
- 长尾请求:明显高于均值的那部分占比较小的请求,
backup request
- 易用性
- 开箱即用
- 周边工具,有良好的生态
- 扩展性
- Middleware
- Option
- 编解码层
- 协议层
- 网络传输层
- 代码生成工具
- 观测性
- Log
- Metric
- Tracing
- 高性能
- 目标:高吞吐、低延迟
- 方法:连接池、多路复用、高性能编解码协议、高性能网络库
本文正在参加技术专题18期-聊聊Go语言框架