RPC 详解

匿名 2021-06-09 10:22:48

RPC 的全称是 Remote Procedure Call,即远程过程调用。RPC 是“微服务”的基础,只要涉及到网络通信,我们就可能用到 RPC,RPC 是解决分布式系统通信问题的一大利器。RPC的两大使命:屏蔽远程调用跟本地调用的区别,让我们感觉就是调用项目内的方法;隐藏底层网络通信的复杂性,让我们更专注于业务逻辑。

RPC 的调用流程:

调用方 -> 数据序列化 -> 网络传输 -> 服务提供方 -> 数据反序列化 -> 执行对应的方法调用

服务提供方 -> 数据序列化 -> 网络传输 -> 调用方 -> 数据反序列化 -> 获取调用结果

RPC 协议:

通俗的讲,协议就是为了让通信的双方能理解通信数据。

为了避免语义不一致的事情发生,我们需要在发送请求的时候设定一个边界,然后在收到请求的时候按照这个设定的边界进行数据解析。这个边界语义的表达,就是我们所说的协议。

一般设计一个协议,包含协议头和协议体两部分。在协议头里面,我们除了会放协议长度、序列化方式,还会放一些像协议标示、消息 ID、消息类型这样的参数,而协议体一般只放请求接口方法、请求的业务参数值和一些扩展属性。这样一个完整的 RPC 协议大概就出来了,协议头是由一堆固定的长度参数组成,而协议体是根据请求接口和参数构造的,长度属于可变的。

数据序列化:

网络传输的数据必须是二进制数据,但调用方请求的出入参数都是对象。对象是不能直接在网络中传输的,所以我们需要提前把它转成可传输的二进制,并且要求转换算法是可逆的,这个过程我们一般叫做“序列化”。 服务提供方根据请求类型和序列化类型,把二进制的消息体逆向还原成请求对象,这个过程我们称之为“反序列化”。

常用的序列化方式:JSON、Protobuf、Hessian

RPC 框架中如何去选择序列化协议,有这样几个很重要的参考因素,优先级从高到低依次是安全性、通用性和兼容性,之后会再考虑序列化框架的性能、效率和空间开销。
归根结底还是因为服务调用的稳定性与可靠性,要比服务的性能与响应耗时更加重要。

在使用 RPC 框架的过程中,我们构造入参、返回值对象要注意以下几点:

1. 对象要尽量简单,没有太多的依赖关系,属性不要太多,尽量高内聚;
2. 入参对象与返回值对象体积不要太大,更不要传太大的集合;
3. 尽量使用简单的、常用的、开发语言原生的对象,尤其是集合类;
4. 对象不要有复杂的继承关系,最好不要有父子类的情况

RPC 网络通信:

考虑到系统内核的支持、编程语言的支持以及 IO 模型本身的特点,RPC 框架在网络通信的处理上,我们更倾向选择 IO 多路复用的方式。

关于零拷贝(Zero-copy):

所谓的零拷贝,就是取消用户空间与内核空间之间的数据拷贝操作,应用进程每一次的读写操作,可以通过一种方式,直接将数据写入内核或从内核中读取数据,再通过 DMA 将内核中的数据拷贝到网卡,或将网卡中的数据 copy 到内核。

零拷贝有两种解决方式,分别是 mmap+write 方式和 sendfile 方式,其核心原理都是通过虚拟内存来解决的。

动态代理技术:

自动给接口生成一个代理类,当我们在项目中注入接口的时候,运行过程中实际绑定的是这个接口生成的代理类。这样在接口方法被调用的时候,它实际上是被生成代理类拦截到了,这样我们就可以在生成的代理类里面,加入远程调用逻辑。通过这种的手法,就可以帮用户屏蔽远程调用的细节,实现像调用本地一样地调用远程的体验。

AOP 中实现统一拦截效果也是基于动态代理技术。

关于gRPC框架:

gRPC 就是采用 HTTP/2 协议,并且默认采用 PB 序列化方式的一种 RPC,它充分利用了 HTTP/2 的多路复用特性,使得我们可以在同一条链路上双向发送不同的 Stream 数据。

在 gRPC里面定义接口是通过写 Protocol Buffer 代码,从而把接口的定义信息通过 ProtocolBuffer 语义表达出来。然后利用 Protocol Buffer 的编译器 protoc,再配合 gRPC 针对不同语言提供的插件,就可以生成消息对象和 gRPC 通信所需要的基础代码。

RPC 框架之服务发现:

原理:通过引入注册中心,服务提供者启动时注册自身到注册中心,调用方从注册中心订阅服务接口,启动时查找并订阅服务提供方IP,缓存到本地供后续使用。

DNS 或者 VIP 方案:DNS由于有多级缓存机制,难以满足实时性要求;VIP由于多引入了一层负载均衡设施,性能有一定影响,并且节点的上下线不够高效,负载均衡算法也不一定能满足所有要求。

服务发现方案:基于 Zookeeper、etcd 实现服务注册与发现。

上述方案有一定的局限性,一方面服务大批量上下线时Zookeeper集群负载会很高,稳定性得不到保证。另外其自身性能问题,当连接的节点数特别多、读写非常频繁时,性能会下降。针对此问题的解决方案是通过消息系统异步下发服务注册,达到最终一致性(AP)即可。

RPC 框架之健康检测:

让调用方实时感知到节点的状态变化,这样他们才能做出正确的选择。

通过心跳机制,以指定的时间间隔,轮询服务节点是否可用,当不可用次数达到一定的阈值时,标记该服务节点不可用。反之,不可用节点也可以恢复至正常状态。也可以检测某个接口的可用率来标记服务是否可用,所谓可用率是指某个时间窗口内请求成功数与请求总数的比值。

RPC 框架之路由策略:

在 RPC 里面,不管是哪种路由策略,其核心思想都是一样的,就是让请求按照我们设定的规则发送到目标节点上,从而实现流量隔离的效果。

可以在选择节点(负载均衡)前加上“筛选逻辑”,把符合我们要求的节点筛选出来。这个所谓的“筛选逻辑”就是我们说的路由策略。

IP 路由策略:用于限制可以调用服务提供方的 IP。

参数路由:我们可以给所有的服务提供方节点都打上标签,用来区分新老应用节点。在服务调用方发生请求的时候,我们可以很容易地拿到请求参数,可以根据注册中心下发的规则来判断某个请求是过滤掉新应用还是老应用的节点。

RPC 框架之负载均衡:

通常 Web 服务中的负载均衡主要分为软负载和硬负载,软负载就是在一台或多台服务器上安装负载均衡的软件,如 LVS、Nginx 等,硬负载就是通过硬件设备来实现的负载均衡,如 F5 服务器等。负载均衡的算法主要有随机法、轮询法、最小连接法等。

RPC 负载均衡策略一般包括随机权重、Hash、轮询。

RPC自适应负载均衡:通过服务节点的综合打分与节点的权重,最终计算出节点的最终权重,之后服务调用者会根据随机权重的策略,来选择服务节点。

RPC 框架之异常重试:

在使用 RPC 框架的时候,我们要确保被调用的服务的业务逻辑是幂等的,这样我们才能考虑根据事件情况开启 RPC 框架的异常重试功能。

为了能够在约定的时间内进行安全可靠地重试,在每次触发重试之前,我们需要先判定下这个请求是否已经超时,如果超时了会直接返回超时异常,否则我们需要重置下这个请求的超时时间,防止因多次重试导致这个请求的处理时间超过用户配置的超时时间,从而影响到业务处理的耗时。

在所有发起重试、负载均衡选择节点的时候,去掉重试之前出现过问题的那个节点,以保证重试的成功率。

RPC 框架之优雅关闭:

当服务提供方正在关闭,如果这之后还收到了新的业务请求,服务提供方直接返回一个特定的异常给调用方(比如 ShutdownException)。这个异常就是告诉调用方“我已经收到这个请求了,但是我正在关闭,并没有处理这个请求”,然后调用方收到这个异常响应后,RPC 框架把这个节点从健康列表挪出,并把请求自动重试到其他节点,因为这个请求是没有被服务提供方处理过,所以可以安全地重试到其他节点,这样就可以实现对业务无损。

我们可以在服务对象加上引用计数器,每开始处理请求之前加一,完成请求处理减一,通过该计数器我们就可以快速判断是否有正在处理的请求。服务对象在关闭过程中,会拒绝新的请求,同时根据引用计数器等待正在处理的请求全部结束之后才会真正关闭。但考虑到有些业务请求可能处理时间长,或者存在被挂住的情况,为了避免一直等待造成应用无法正常退出,我们可以在整个 ShutdownHook 里面,加上超时时间控制,当超过了指定时间没有结束,则强制退出应用。

RPC 框架之优雅启动:

启动预热:就是让刚启动的服务提供方应用不承担全部的流量,而是让它被调用的次数随着时间的移动慢慢增加,最终让流量缓和地增加到跟已经运行一段时间后的水平一样。

延迟暴露:服务提供方应用在没有启动完成的时候,调用方的请求就过来了,而调用方请求过来的原因是,服务提供方应用在启动过程中把解析到的 RPC 服务注册到了注册中心,这就导致在后续加载没有完成的情况下服务提供方的地址就被服务调用方感知到了。解决方案:我们可以把接口注册到注册中心的时间挪到应用启动完成后。

RPC 框架之熔断限流:

服务是如何实现自我保护?

限流是一个比较通用的功能,我们可以在 RPC 框架中集成限流的功能,让使用方自己去配置限流阈值;我们还可以在服务端添加限流逻辑,当调用端发送请求过来时,服务端在执行业务逻辑之前先执行限流逻辑,如果发现访问量过大并且超出了限流的阈值,就让服务端直接抛回给调用端一个限流异常,否则就执行正常的业务逻辑。

在一个服务作为调用端调用另外一个服务时,为了防止被调用的服务出现问题而影响到作为调用端的这个服务,这个服务也需要进行自我保护。而最有效的自我保护方式就是熔断。

熔断器的工作机制主要是关闭、打开和半打开这三个状态之间的切换。在正常情况下,熔断器是关闭的;当调用端调用下游服务出现异常时,熔断器会收集异常指标信息进行计算,当达到熔断条件时熔断器打开,这时调用端再发起请求是会直接被熔断器拦截,并快速地执行失败逻辑;当熔断器打开一段时间后,会转为半打开状态,这时熔断器允许调用端发送一个请求给服务端,如果这次请求能够正常地得到服务端的响应,则将状态置为关闭状态,否则设置为打开。

业务分组,流量隔离:

非核心应用不要跟核心应用分在同一个组,核心应用之间应该做好隔离,一个重要的原则就是保障核心应用不受影响。

在 RPC 里面我们可以通过分组的方式人为地给不同的调用方划分出不同的小集群,从而实现调用方流量隔离的效果,保障我们的核心业务不受非核心业务的干扰。

本文来源于 匿名

免责声明:
1. 本文版权归属原作所有,仅代表作者本人观点,不代表币推儿的观点或立场。
2. 如发现文章、图片等侵权行为,侵权责任将由作者本人承担。