Remote Procedure Call
远程函数调用指在本地通过网络调用一个服务端的函数。
RPC需要解决的问题:
- 函数映射:与本地函数不同,我们无法获取远程函数的内存地址,一个远程函数必须被映射到本地才能被调用,例如通过字符ID表示一个远程函数
- 将数据转换成字节流:远程函数的输入/输出必须被编码为字节流并通过网络传输,而不是简单的压入/弹出栈内存。
- 网络传输:远程函数的调用通过网络完成。
1984年Nelson提出的RPC概念模型:
- 用户代码调用远程函数
- 函数参数通过User-stub打包
- 数据通过网络被发送到远程机器
- 远程机器通过Server-stub解包参数
- 远程机器调用指定的函数
- 远程机器通过Server-stub打包返回数据
- 用户机器收到结果后,通过User-stub解包
- 调用函数得到返回值
为了实现RPC,我们需要定义远程函数的调用方式,输入/输出编解码,以及通信协议:
- IDL (Interface Description Language) 文件:通过一种语言中立的、跨平台的方式来描述接口,是不同平台上运行的不同语言编写的程序可以互相通信。
- 代码生成工具:用于把IDL文件描述的函数转换成对应一个特定语言的静态库
- 编解码:数据在内存中的二进制表示到字节序列的相互转换
- 通信协议:规定数据在网络中的传输内容与格式,通常包含额外的元数据
-
- 通常使用TCP/UDP传输
RPC的好处
- 函数/服务的单一职责,有利于分工协作和运维开发
- 可扩展性强
- 故障隔离,提高整体服务可靠性
RPC的问题
- 远程服务宕机时,调用方应该如何处理?
- RPC调用过程中发生网络异常,如何保证消息可达?
- 请求量突增导致服务无法及时处理,应该如何应对?
RPC框架需要解决以上这些问题。
RPC框架设计
RPC框架通过分层设计完成RPC调用概念模型的每个步骤:
数据格式
函数的输入/输出数据在不同情况下有不同的表现形式:
- 语言特定的二进制格式:编程语言内建的将对象编码为字节序列的支持,例如Java的
java.io.Serializable - 文本格式:JSON、XML、CSV(对人类可读性高)
- 二进制格式:跨语言、高性能的二进制格式,例如:Thrift的BinaryProtocol,Protobuf
Thrift BinaryProtocal使用TLV编码:
- Tag: 标签,表示类型和结构中的指定成员
- Length: 对象字节序列长度
- Value: 对象的值,可以是任何二进制序列,值也可以是一个TLV结构(允许嵌套)
选择合适的编码方案需要考虑:
- 兼容性:支持增加新的字段时向前兼容
- 通用性:跨平台、跨语言
- 性能:编码解码开销、编码后的数据大小
协议层
定义协议单元
- 通过特殊字符作为结束标识
- 在开头添加一个定长整数表示长度,用于实现变长协议
Thrift协议:
协议解析
网络通信层
- 提供易用API
-
- 封装底层 Socket API
- 连接管理和事件分发
- 功能
-
- 协议支持:TCP,UDP,UDS等
- 优雅退出、异常处理
- 性能
-
- 应用层buffer减少copy
- 高性能定时器、对象池等
RPC框架关键指标
稳定性
保障策略:
- 熔断:一个服务调用另一个服务时,如果被调用的服务出现问题导致请求超时,那么调用方应该停止使用这个出现问题的服务,防止大量请求积压在调用方导致更多服务崩溃。
- 限流:保护被调用的服务,防止大流量把服务压垮
- 超时控制:避免浪费资源在不可用节点上
提高请求成功率:
- 负载均衡:尽可能将请求分配给负载低的实例
- 重试:请求服务超时时可以尝试重试
备份请求与长尾请求:
- 有时候一个请求可能由于某些原因比正常情况需要更长的响应时间,这样的请求被称为长尾请求。
- 可以根据经验计算一个请求预期的响应时间指标,例如p99(99%的请求响应时间小于该值)
- 在超过阈值且没有收到响应时,立即发送备份请求Req2进行重试,如果备份请求正常返回,那么不必等待长尾请求的结果。
RPC框架通常允许通过注册中间件的方式配置这些策略。
易用性
- 开箱即用,包含合理的默认参数选项、丰富的文档
- 周边工具:生成代码工具、脚手架工具
扩展性
允许对RPC调用过程的各个层级进行扩展
观测性
内置Log, Metric, Tracing(链路跟踪)等服务