1. 基本概念
1.1 本地函数调用
1.2 远程函数调用 RPC-remote procedure calls
网上商城,调用支付服务,这两个服务在不同的机器上,中间隔着一个网络,是远程的
RPC需要解决的问题:
-
函数映射:网上商城要告诉支付服务要调用支付功能,而不是退款等其他功能。
本地调用是直接通过函数指针调用,但是远程不行。 所以,每个函数要有一个id,在做rpc的时候附上这个id,通过id找到对应函数。 -
数据转换成字节流:
把参数告诉远程函数;
本地压栈就好了(内存),远程不行;
要先把数据转换成字节流传送给远程函数,然后再把字节流转换成自己能读取的格式。 -
网络传输:
远程调用往往发生在网络上,如何保证网络上稳定安全传输数据?
1.3 RPC概念模型
caller machine(调用端)
user:发起本地调用,调用user-stub,把参数打包;
user-stub 打包完成之后,交给RPCruntime
RPCruntime 把数据发送到对端
callee machine(被调用端)
RPCruntime 接收数据,并交给server-stub解压数据
server-stub 解压完成后,调用server
server 处理完业务就返回,返回也要把返回结果打包,通过RPCruntime传给对端
对端 user-stub 解压数据,传给user
1.4 一次RPC的完整过程
IDL文件(interface description language)不知道对方有哪些方法和参数的要求,需要一个东西声明描述方法和参数长什么样。
通过一种中立的方式描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信。
生成代码:
通过编译器工具把IDL文件转换成语言对应的静态库
编解码:
从内存中表示的数据格式到 字节序列 的转换称为编码,反之为解码,也常叫做序列化和反序列化。解决了跨语言的数据交互格式。
通信协议:
规范了数据在网络中的传输格式和内容。除必须的请求、响应数据外,通常还会包括额外的元数据。
网络传输:
通常基于成熟的网络库走TCP/UDP传输
caller 生成代码 通过encoder 编码 转成字节流,按照协议的约定,放到对应位置,整体打包,传输给对端。
对端接收到,把数据解出来,然后把数据交给上层的callee去处理。
1.5 RPC的好处
- 单一职责,有利于分工协作和运维开发。不同的服务可以用不同的语言开发,部署、运维、上线都是独立的,可以不同的团队来操作。
- 可扩展性强,资源使用率更优。压力大的时候,可以对相关的服务进行扩容,其他的就不变。底层服务可以复用。
- 故障隔离,某个服务故障不会导致整体的崩溃。服务的整体可靠性更高。
1.6 RPC带来的问题
被调用的服务宕机了,怎么办?
调用过程中发生网络异常,如何保证消息的可达性?
请求量突增导致服务无法及时处理,有哪些应对措施?
以上问题都有RPC框架来处理
2. 分层设计
编解码层、协议层和网络通信层
2.1 以apache thrift为例
client和server,分别是调用端和非调用端
code:用户自己编写的业务代码,不在框架范畴
生成代码:通过代码生成工具把IDL文件转换成不同语言对应的lib代码,里面封装了编解码逻辑;
TProtocal编解码层;
生成代码层也可以看成编解码层,因为它里面也封装了几个编解码逻辑。
协议层;
网络通信层;
2.3 编解码层-生成代码
client和server依赖同一份IDL文件生成不同语言的生成代码codegen
2.4 编解码层-数据格式
语言特定的格式:许多编程语言都内建了将内存对象编码为字节序列的支持,例如Java有java.io.Serializable
好处:方便,用少量的额外代码就可以实现内存对象的保存和恢复
缺点:和特定语言绑定,兼容性不好
文本格式:
json、xml、csv等文本格式,就有人类可读性
描述不严谨
二进制编码:
具有跨语言和高性能等优点,常见有thrift的binaryprotocol,protobuf等
把数据转换成二进制流
2.5 编解码层-二进制编码
TLV编码:分别是tag、length、value
tag:标签,可以理解为类型;
length:长度;
value:值,value也可以是个TLV结构(嵌套);
增加了length和tag冗余信息
2.6 编解码层-选型
兼容性:维护IDL文件,新增字段不能影响其他服务; 支持自动增加新的字段,而不影响老的服务,这将提高系统的灵活度。
通用性:支持跨平台、跨语言;流行程度;
性能:从空间和时间两个维度考虑,也就是编码后数据大小和编码耗费时长。
2.7 协议层
2.8 协议层-概念
特殊结束符:一个特殊字符作为每个协议单元结束的标示。如\r\n
变长协议:以定长+不定长的部分组成,其中定长的部分需要描述不定长的内容,如定长的长度,然后就读取那个长度的内容,就知道读到哪里结束。
2.9 协议层-协议构造
length:数据包大小,不包含自身;
header magic:标识版本信息,协议解析时候快速校验;
sequence number:标识数据包的seqID,可用于多路复用,单连接内递增;
header size:头部长度,从第14个字节开始计算一直到payload前;
protocol ID:编解码方式,如binary和compact;属于header;
transform ID:压缩方式,如zlib和snappy;属于header;
info ID:传递一些定制的meta信息(元数据);属于header;
payload:消息体;
2.10 协议层-协议解析
读取magicnumber,知道协议的类型;
读取编码方式;
解码;
得到payload;
2.11 网络通信层
2.12 网络通信层-sockets API
在应用层和传输层(TCP/UDP)之间
再往下是网络层、物理层
要知道IP和端口
创建套接字的时候,bind(把套接字反映到一个地址上),listen(监听进来的连接,放到一个队列backlog,队列可能阻塞,有长度规定;),accept(客户端的connect发起连接请求connect request,server接收accept一个请求;若没有请求,就阻塞,等待连接进来;得到id之后,调用read函数进行通讯;)close(关闭套接字)
2.13 网络通信层-网络库
提供易用API :
封装底层socket API;连接管理和事件分发;
功能:
协议支持:tcp、udp和uds等;
优雅退出、异常处理等;
性能:
用应用层buffer减少减少copy;
高性能定时器、对象池等提高性能;
3. 关键指标
稳定性、易用性、扩展性、观测性、高性能
3.1 稳定性-保障策略
熔断:保护调用方,防止被调用的服务出现问题而影响到整个链路
例子:A->B->C,C响应超时了,B的逻辑就一直在等待,返回给A也超时,A就一直频繁调用B,B因为堆积了大量的请求导致服务宕机,服务雪崩,影响整个链路。
限流:保护被调用方,防止大流量把服务压垮;
调用端发起请求的时候,被调用端在执行业务逻辑之前,先检查线路逻辑,若返回量过大,超过了限制条件,就直接返回异常。
超时控制:被调用端避免浪费资源在不可用节点上
被调用端因为某种原因响应过慢了,调用端主动停止一些不太重要的调用请求,快速返回,及时释放资源。
以上都可以归为降级措施。
3.2 稳定性-请求成功率
要提高请求成功率:
- 负载均衡
- 重试
3.3 稳定性-长尾请求
长尾请求:响应时间长于平均,占比较少的请求
长尾请求通常都是存在的、
正常情况下:A发出请求,返回,失败,耗时t1;重试一次,返回,成功,耗时t2;
backup request :
发送请求req1,间隔t3(根据过往经验预测这次返回所需时间)时间后,req1仍没有返回,则再次发出请求,记作req2;而req2很快得到返回。这样就缩短了请求得到处理的时间。
3.4 稳定性-注册中间件
以可选的方式把上面的功能加上。
3.5 易用性
开箱即用:提供合理的默认参数选项、丰富的文档,拿到框架就能使用,有问题,看文档。
周边工具:提供生成代码工具、脚手架工具(减少重复性工作); 简单易用的命令行工具:生成服务代码脚手架;支持protobuf、thrift;内置功能丰富的选项;支持自定义的生成代码插件
3.6 扩展性
提供较多的丰富的 扩展点
middleware;option(可选的参数);编解码层; 协议层;网络传输层;代码生成工具创建扩展;
3.7 观测性
log日志 、
matric 监控、
tracing 链路跟踪
内置观测性服务:框架主动暴露
3.8 高性能
目标:高吞吐、低延迟
场景:单机多级、单连接多连接、不同大小的请求包、不同请求类型 不同场景的性能表现不一样;
手段:连接池、多路复用、高性能编解码协议、高性能网络库
4. 企业实践
整体架构、自研网络库、扩展性设计、性能优化、合并部署
主要是视野的扩展
4.1 整体结构-kitex
核心组件