rpc实战与核心原理

  • Post author:
  • Post category:其他


网络通信一般会采用四层Tcp协议或者七层Http协议。

RPC的作用:

1.屏蔽远程调用和本地调用的区别,让你感觉就是调用项目内的方法。

2.隐藏底层网络通信的复杂性,让我们更专注于业务逻辑。


RPC的流程

在这里插入图片描述


RPC协议介绍


HTTP和RPC都是应用层协议。相比于HTTP的用处,RPC更多的是负责应用间的通信,所以性能要求相对更高。但HTTP协议的数据包大小相对于数据本身要大的多,又需要加入许多无用的内容,比如换行符、回车符等;还有一个更重要的原因是HTTP协议属于无状态协议,无法对请求和响应进行关联,每次请求都需要重新进行连接,响应完成后再关闭连接。因此,对于要求高性能的RPC来说,HTTP基本很难满足需求,所以RPC选择更紧密的私有协议。

在这里插入图片描述

在这里插入图片描述


有哪些常用的序列化?


1.JDK原生序列化

JDK 自带的序列化机制对使用者而言是非常简单的。序列化具体的实现是由 ObjectOutputStream 完成的,而反序列化的具体实现是由 ObjectInputStream 完成的。

jdk的序列化过程:

在这里插入图片描述

2.JSON

json进行序列化有这样两个问题:

1.json进行序列化的额外空间开销比较大,对于大数据量服务这意味着需要巨大的内存和磁盘开销。

2.JSON没有类型,所以像java这种强类型语言,需要通过反射统一解决,所以性能不会太好。

3.Hessian

性能上要比 JDK、JSON 序列化高效很多,而且生成的字节数也更小。

但是Hessian本身也有问题,官方版本对java里的一些常用对象的类型不支持。

4.Protobuf

优点:

1.序列化后的体积相比于JSON、Hessian小很多。

2.IDL能清楚的描述语言,所以足以保证应用程序之间的类型不会丢失,无需类似xml解析器。

3.序列化反序列化快,不需要通过反射获取类型。

4.消息格式升级和兼容性不错,可以做到向后兼容。

缺点:对于具有反射和动态能力的语言,这样用起来很费劲,这一点不如Hessian


RPC框架如何选择序列化呢?


序列化需要考虑的因素:

在这里插入图片描述


RPC框架在使用时需要注意哪些问题?


1.对象构造过于复杂

2.对象过于庞大

3.使用序列化框架不支持的类作为入参类

4.对象有复杂的继承关系



常见的网络io模型

io的四种模型:同步阻塞io(BIO)、同步非阻塞io(NIO)、io多路复用和异步非阻塞io(AIO)。


阻塞IO(blocking IO)


首先应用程序发起io调用后,应用程序被阻塞,转到内核空间进行处理。之后内核开始等待数据,等待到数据后,再将内核中的数据拷贝到用户内存中,整个IO处理完毕后返回进程。最后应用进程解除阻塞状态,进行业务逻辑。

阻塞io是一个io对应一个进程或线程。



什么是零拷贝?

应用进程每一次写操作,都会把数据写入到用户空间的缓冲区中,再由CPU将数据拷贝到系统内核的缓冲区,之后再由DMA将这份数据拷贝到网卡中最后再由网卡发送出去。这里我们看到一个写操作要拷贝两次才能将数据通过网卡发送出去。

在这里插入图片描述

所谓零拷贝技术就是取消用户空间和内核空间之间的数据拷贝操作
在这里插入图片描述

其实rpc就是把拦截到的方法参数,转变为可以在网络中可以传输的二进制,并保证在服务提供发能够正确的还原出语义,最终实现像调用本地一样调用远程的目的。



RPC实战:剖析RPC源码,动手完成一个完整的RPC

我们先以最简单的Hello Word为例开始了解。在这个例子里我们定义一个say方法,调用方通过gRPC调用服务提供方,然后服务提供方会返回一个字符串给调用方。

为了保证调用方和服务提供方能够正常通信,我们先约定通信过程中的契约,也就是java里面说的接口,这个接口只包含一个say方法。在grpc里面定义接口是通过Protocol Buffer代码,从而把接口的定义信息通过Protocol Buffer语义表达出来。

在这里插入图片描述

通过上面的Protocol Buffer可以为客户端和服务端生成消息对象和RPC基础代码。
在这里插入图片描述

在这里插入图片描述

调用端代码大致分为三个步骤:

1.首先用host和port生成channel连接

2.然后用前面生成的HelloService gRPC创建Stub类

3.最后我们可以用生成的stub调用say方法发起真正的RPC调用。

在这里插入图片描述



服务发现到底是CP还是AP?

为了高可用,服务提供方都是以集群的方式对外提供服务,集群里面这些IP随时可能发生变化,我们也需要一本“通讯录”及时获取到对应服务节点,这个获取过程我们一般叫做“服务发现”。
在这里插入图片描述

为什么不使用DNS呢?

没有使用DNS的两个原因:

1.这个IP端口下线了,服务调用者能否及时摘除服务节点?

2.如果在之前已经上线的一部分服务节点,这是我突然对这个服务进行扩容,新上线的服务是否能及时接收到流量?

答案是不能,因为DNS存在多级缓存机制,一般配置的缓存时间较长,特别是jvm配置的缓存是永久有效的,所以服务调用者不能及时的感知到服务节点的变化。

这是你可能会想加一个负载均衡设备,将域名绑定到这个负载均衡设备上,通过DNS拿到这个负载均衡设备的IP。这样服务调用的时候,服务调用方就可以直接根据VIP建立连接,然后由VIP机器完成转发

,

这个方案确实能解决DNS遇到的一些问题,但在RPC场景里也并不是很合适,因为:

1.搭建负载均衡设备或者TCP/IP四层代理,需要额外的成本。

2.请求流量都经过负载均衡设备带来的性能问题

3.负载均衡添加、删除节点都需要手动执行

4.服务治理需要更灵活的负载均衡策略,目前的负载均衡算法并不能满足需求。


基于ZooKeeper的服务发现机制


在这里插入图片描述

1.服务平台管理端会在Zookeeper中创建一个服务根路径,可以根据接口名命名,在这个路径下再创建服务提供方目录和服务调用方目录,分别用来存储服务提供方节点信息和服务调用方节点信息。

2.当服务提供方发起注册时,会在服务提供方目录中创建一个临时节点,节点中存储服务提供方的注册信息

3.当服务调用方发起注册时,会在服务调用方目录中创建一个临时节点,节点中存储服务调用方的注册信息,同时服务调用方会watch服务提供方目录中所有服务节点数据


基于消息总线的最终一致性的注册中心


Zookeeper的一大特点就是强一致性,Zookeeper集群每一个节点的数据每一次发生更新操作,都会通知其他Zookeeper节点同时执行更新,他要保证每个节点的数据都能够实时的完全一致。

而RPC框架的服务发现,在服务刚上线时服务调用方可以容忍在一段时间之后发现这个新上线的节点的。毕竟服务节点刚上线之后的几秒内,甚至更长的一段时间内没有接收到请求流量,对整个服务集群没有什么影响的,所以牺牲掉CP(强一致性),而选择AP(最终一致性)来换取整个注册中心集群的性能和稳定性。

那么是否有那种简单、高效、并且最终一致的更新机制,能代替Zookeeper那种数据强一致的数据更新机制?

可以考虑采用消息总线机制。注册数据可以全量缓存在每个注册中心内存中,通过消息总线(消息队列)来同步数据。
在这里插入图片描述



健康检查机制

问题:线上的某个接口可用性不高,基本上10次调用总会有几次失败的。

实际的原因:服务已经处于“病危”状态,但是探活心跳包取一直可以发送正常,导致没有把它移出健康检查列表。

如果只是服务下线,正常情况我们肯定会收到连接断开的通知,在这个事件里直接加处理逻辑不就好了吗,为什么还需要健康检查机制?

因为健康检查不仅包括TCP连接状态,还包括应用本身是否存活,很多情况下tcp没有断开连接,但是应用已经处于僵死状态。

在这里插入图片描述


具体的解决方案:


一个节点从健康转变为亚健康状态的前提是“连续”心跳失败达到阀值,比如3次。

而我们的场景里,节点的心跳日志只是间歇性失败,达不到阀值,那我们应该如何解决呢?

调低阀值么?它明显治标不治本,因为如果出现网络波动的情况下,就会导致误判发生。第二在高负载的情况下,服务器来不及处理心跳请求由于心跳时间很短会导致调用方连续出现心跳失败而造成断开连接。

那我们是不是可以不单单将失败次数作为阀值呢?

我们可以将可用率作为阀值(成功次数/总请求数)。



异步RPC:压榨单机吞吐量

我们了解到一次RPC调用的本质就是调用端向服务器端发送一条请求消息,服务器端收到消息后进行处理,处理之后给调用端一条响应消息,调用端收到响应消息后进行处理,最后将最终返回值返回给动态代理。

对于调用端来说,向服务器端发送请求消息和接收服务器端发送过来的响应消息,这两个过程是完全独立的过程。这两个过程在大多数情况下都不在一个线程中进行,那是不是说RPC框架的调用端,对于RPC调用的处理逻辑,内部实现就是异步的呢?

我们知道,调用端发送的每一条消息都有一个唯一的消息标识,实际上调用端向服务器端发送请求消息之前会先创建一个Feature,并会存储这个消息标识和与这个Future的映射,动态代理所获得的返回值最终就是从这个Feature中获取的;当收到服务器端的响应消息时,调用端会根据响应消息的唯一标识,通过之前存储的映射找到对应的Future,将结果注入到Future,再经过一系列的处理逻辑,最后动态代理会从Future中获取到正确的返回值。

所谓的同步调用是指RPC在调用端处理逻辑中主动执行了这个future的get方法,让动态代理等待返回值;

而异步调用则是RPC框架没有主动执行这个future的get方法,用户可以通过上下文获取到future,自己决定什么时候执行这个future的get方法。

在这里插入图片描述


如何做到RPC调用全异步的?


通过java8的CompletableFuture,来实现RPC调用在调用端和服务端的完全异步:

1.调用方发起RPC调用,直接拿到返回值CompletableFuture对象之后,就完全不用管任何额外与RPC框架相关的操作了(如我刚才讲的Future方式需要通过请求上下文获取Future的操作),直接就可以进行异步处理;

2.在服务端的业务逻辑中创建一个返回值CompletableFuture对象,之后服务端真正的业务逻辑完全在一个线程池中异步处理,业务逻辑完成之后再调用CompletableFuture对象的complete方法完成异步通知。

3.调用端在收到服务器端的响应之后,RPC框架再自动地调用调用端拿到饿那个返回值CompletableFuture对象的complete方法,这样一次异步调用就完成了。



安全体系:如何建立可靠的安全体系?



时钟轮在RPC中的应用

如果上文中,异步调用服务端没有及时响应消息给调用端呢?调用端如何处理超时请求?

我们可以利用定时任务,每次创建一个Future,我们都记录这个future的创建时间和超时时间,摒弃有一个定时任务进行检测,当这个future达到超时时间并且没有被处理的时候,我们就对这个future执行超时逻辑。

我们可以通过一个线程处理所有定时任务,假设我们要启动一个线程,这个线程每隔100ms扫描一遍所有处理Future超时任务,当发现一个future超时就执行这个任务,对这个future执行超时逻辑。

如果是高并发的请求,那么扫描任务的线程每隔100ms要扫描多少定时任务呢?如果调用端刚好在1s内发送1万次请求,这1万次请求在5s后超时,那么这个扫描线程就需要在5s内这1万个请求进行遍历,要额外扫描40多次,很浪费cpu。


上述问题可以采用时钟轮机制解决


时钟轮算法


时间轮用来解决什么问题?

如果一个系统存在着大量的调度任务,而大量的调度任务每一个都使用自己的调度器来管理任务的生命周期的话,浪费cpu资源并且很低效。

时间轮是一种高效来利用线程资源批量化调度的一种调度模型。把大批量的调度任务全部都绑定到同一个调度器上面,使用这一个调度器来进行所有任务的管理,触发以及运行。

时钟轮算法的时间复杂度:O(1)



版权声明:本文为Drftyytf原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。