深入浅出RPC框架 | 青训营笔记

80 阅读10分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第7篇笔记。

一、基本概念

1.1 本地函数调用

image.png

  1. 将a和b的值压栈
  2. 通过函数指针找到calculate函数,进入函数取出栈中的值2和3,将其赋予x和y。
  3. 计算x * y,并将结果存在z。
  4. 从栈中取出z返回值,并赋值给result。

注:以上步骤知识为了说明原理。事实上编译器经常会做优化,对于参数和返回值少的情况会直接将其存放在寄存器,而不需要压栈弹栈的过程,甚至都不需要调用call,而直接做inline操作。

1.2 远程函数调用(RPC - Remote Procedure Calls)

image.png

远程调用中,网上商城和支付服务部署在不同的机器上, 中间隔着网络。

RPC需要解决的问题:

  1. 函数映射:网上商城怎么告诉支付服务,我们要调用付款这个函数,而不是退款或者充值?本地调用中,函数体可以通过函数指针来指定的,我们调用哪个方法,编译器就自动帮我们调用它相应的指针。但是在远程调用中,函数指针是肯定不行的,因为两个进程的的地址空间是完全不一样的。所以函数都有自己的一个ID,在做RPC的时候要附上这个ID,还得有个ID和函数的对照关系表,通过ID找到对应的函数并执行。
  2. 数据转换成字节流:客户端怎么把参数值传给远程的函数呢?本地调用的时候,只需要把参数压到栈里,然后再从栈里弹出来。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读的格式。
  3. 网络传输:远程调用往往发生在网络上,如何保证网络上高效稳定的传输数据。

1.3 一次RPC的完整过程

IDL(Interface description language)文件:相比于本地函数调用,远程调用的话我们不知道对方有哪些方法,以及参数长什么样子,所以需要有一种方式来描述或者说声明有哪些方法,方法的参数是什么样子,这样的话大家就能按照这个来调用,这个描述文件就是IDL文件。IDL通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信。

生成代码:通过编译器工具把IDL文件转换成语言对应的静态库。

编解码:从内存中表示到字节序列的转换称为编码,反之称为解码,也常叫做序列化和反序列化。

通信协议:规范了数据在网络中的传输内容和格式。除必须的请求、响应数据外,通常还会包含额外的元数据。

网络传输:通常基于成熟的网路库走TCP、UDP传输。

image.png

1.4 RPC的好处

  1. 单一职责,开发(采用不同的语言)、部署以及运维都是独立的。
  2. 可扩展性强,例如压力过大的时候可以独立扩充资源,底层基础服务可以复用,节省资源。
  3. 故障隔离,服务的整体可靠性更高。某个模块发生故障,不会影响整体的可靠性。

1.5 RPC带来的问题

  1. 服务宕机,对方应该如何处理?
  2. 在调用过程中发生网络异常,如何保证消息的可达性?
  3. 请求量突增导致服务无法及时处理,有哪些应对措施?

二、分层设计

RPC框架的主要核心有三层:编解码层、协议层和网络通信层。

2.1 分层设计 - 以Apache Thrift为例

image.png

2.2 编解码层

image.png

客户端和服务端依赖同一份IDL文件去生成不同语言对应的生成代码。

IDL文件中的数据格式:

  • 语言特定的格式:许多编程语言都内建了将内存对象编码为字节序列的支持,例如Java有java.io.Serializable。这种编码形式的好处是非常方便,可以用很少的额外代码实现内存对象的保护与恢复,这类编码通常与特定的编程语言深度绑定,其他语言很难读取这种数据。安全和兼容性也是问题。
  • 文本格式:JSON、XML、CSV等文本格式,具有人类可读性,数字的编码多有歧义之处,比如XML和CSV不能区分数字和字符串,JSON虽然区分字符串和数字,但是不区分整数和浮点数,而且不能指定精度,处理大量数据时,这个问题就更严重了。由于JSON在一些语言中的序列化和反序列化需要采用反射机制,所以性能比较差。
  • 二进制编码:具备跨语言和高性能等优点,常见有Thrift的BinaryProtocol,Protobuf等。

二进制编码 - TLV编码:

  • Tag:标签,可以理解为类型
  • Length:长度
  • Value:值,Value也可以是个TLV结构

image.png

image.png

选型:

  • 兼容性:支持自动增加新的字段,而不影响老的服务,这将提高系统的灵活度。
  • 通用性:
    • 第一、技术层面,序列化协议是否支持跨平台、跨语言。如果不支持,在技术层面上的通用性大大降低。
    • 第二、流行程度,序列化和反序列化需要多方参与,很少人使用的协议往往意味着昂贵的学习成本;另一方面,流行程度低的协议,往往缺乏稳定而成熟的跨语言、跨平台的公共包。
  • 性能:
    • 空间开销:序列化需要在原有的数据上加上描述字段,为反序列化解析之用。如果序列化过程引入的额外开销过高,可能会导致过大的网络、磁盘等高方面的压力。对于海量分布式存储系统,数据量往往以TB为单位,巨大的额外空间开销意味着高昂的成本。
    • 时间开销:复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈。

2.3 协议层

协议是双方确定的交流语义。

  • 特殊结束符:一个特殊字符作为每个协议单元结束的标识。过于简单,对于一个协议单元必须要全部读入才能够进行处理,除此之外必须防止用户传输的数据不能同结束符相同,否则就会出现紊乱。

image.png

  • 变长协议:一般都是自定义协议,由header和payload组成,以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度。使用比较广泛。

image.png

协议构造

image.png

2.4 网络通信层

一般采用封装好的网络库来作为rpc框架的网络通信层。衡量指标如下:

  • 提供易用API
    • 封装底层Socket API
    • 连接管理和事件分发
  • 功能
    • 协议支持:tcp、udp等
    • 优雅退出、异常处理等
  • 性能
    • 应用层buffer减少copy
    • 高性能定时器、对象池等

三、关键指标

3.1 稳定性

保障策略:

  • 熔断:保护调用方,防止被调用的服务出现问题而影响到整个链路。一个服务A调用服务B时,服务B的业务逻辑又调用了服务C,而这时服务C响应超时了,由于服务B依赖服务C,C超时直接导致B的业务逻辑一直等待,而这个时候服务A继续频繁的调用服务B,服务B就可能会因为堆积大量的请求而导致服务宕机,由此就导致了服务雪崩的问题。

  • 限流:保护调用方,防止被调用的服务出现问题而影响到整个链路。当调用端发送请求过来时,服务端在执行业务逻辑之前先执行检查限流逻辑,如果发现访问量过大并且超出了限流条件,就让服务端直接降级处理或者返回给调用方一个限流异常。

  • 超时控制:避免浪费资源在不可用节点上。当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,避免浪费资源。

请求成功率:

负载均衡

image.png

重试

image.png

重试有放大故障的风险。首先重试会加大直接下游的负载。如上图,假设A服务调用B服务,重试次数设置为r(包括首次请求),当B高负载时很可能调用不成功,这时A调用失败重试B,B服务的被调用量快速增大,最坏情况下可能放大到r倍,不仅不能请求成功,还可能导致B的负载继续升高,甚至直接打挂。

防止重试风暴,限制单点重试和限制链路重试。

长尾请求:

image.png

长尾请求一般是指明显高于均值的那部分占比较小的请求。界面关于延迟有一个常用的P99标准,P99单个请求响应耗时从小到大排列,顺序处于99%位置的值即为P99值,那后面的这1%就可以认为是长尾请求。在较复杂的系统中,长尾延时总是会存在。造成这个的原因非常多,常见的有网络抖动,GC,系统调度。

我们预先设定一个阈值t3,当Req1发出去后超过t3时间都没有返回,那我们直接发起重试请求Req2,这样相当于同时有两个请求运行。然后等待请求返回,只要Resp1或者Resp2任意一个返回成功的结果,就可以立即结束这次请求,这样整体的耗时就是t4,它表示从第一个请求发出到第一个成功结果,返回之间的时间,相比于等待超时后再发送请求,这种机制能大大减少整体延时。

3.2 易用性

  • 开箱即用:合理的默认参数选项,丰富的文档。
  • 周边工具:生成代码工具,脚手架工具。

3.3 扩展性

  • Middleware
  • Option
  • 编解码层
  • 协议层
  • 网络传输层
  • 代码生成工具插件扩展

image.png

上图是一个典型的中间件执行流程。一次请求发起首先会经过治理层面,治理相关的逻辑被封装在middleware中,这些middleware会被构造成一个有序调用链逐个执行,比如服务发现、路由、负载均衡、超时控制等,middlewware执行后就会进入到remote模块,完成与远端的通信。

3.4 观测性

image.png

  • Log、Metric、Tracing
  • 内置观测性服务

3.5 高性能

场景:

  • 单机多机
  • 单连接多连接
  • 单、多client,单、多server
  • 不同大小的请求包
  • 不同请求类型:例如pingpong、streaming等

目标:

  • 高吞吐
  • 低延迟

手段:

  • 连接池
  • 多路复用
  • 高性能编解码协议
  • 高性能网络库

四、总结

通过本次青训营课程,学习到了RPC框架完成的概念以及分层结构,掌握了RPC框架的核心指标的衡量标准。