RPC原理(2)之深入RPC原理

本文详细介绍了RPC的原理,包括动态代理的内部实现,如JDK动态代理,以及服务注册中心的作用和工作流程,如Zookeeper的服务发现机制。此外,还探讨了网络IO模型,特别是IO多路复用和零拷贝技术在RPC中的应用,以及时间轮算法在超时检测和心跳机制中的高效实现。通过对这些核心技术的理解,可以提升RPC框架的性能和稳定性。
摘要由CSDN通过智能技术生成

一、简介

        在上一篇博客中我们介绍到了深入RPC原理的序列化技术,接下来我们来继续介绍深入RPC原理对应的其他技术。

二、深入RPC原理

2.1动态代理

             

2.1.1内部接口如何调用实现

RPC的调用对用户来讲是透明的,那内部是如何实现呢?内部核心技术采用的就是动态代理,RPC会自动给接口生成一个代理类,当我们在项目中注入接口的时候,运行过程实际绑定的是这个接口生成的代理类。在接口方法被调用的时候,它实际上是被生成代理类拦截到了,这样就可以在生成的代理类里面,加入其它调用处理逻辑,比如连接负载均衡,日志记录等。

2.1.2JDK动态代理实现

        实现代码:

public class JDKDynamicProxy {

    /**
     * 定义用户的接口
     */
    public interface User {
        String job();
    }


    /**
     * 实际的调用对象
     */
    public static class Teacher  {

        public String invoke(){
            return "i'm a Teacher";
        }
    }


    /**
     * 创建JDK动态代理类
     */
    public static class JDKProxy implements InvocationHandler {
        private Object target;

        JDKProxy(Object target) {
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] paramValues) {
            return ((Teacher)target).invoke();
        }
    }


    public static void main(String[] args){
        // 构建代理器
        JDKProxy proxy = new JDKProxy(new Teacher());
        ClassLoader classLoader = JDKDynamicProxy.class.getClassLoader();

        // 生成代理类
        User user = (User) Proxy.newProxyInstance(classLoader, new Class[]{User.class}, proxy);

        // 接口调用
        System.out.println(user.job());
    }
}

        对于JDK的原理大家可以去看之前写的一篇博客,Spring源码解析(24)之JDK动态代理与cglib动态代理源码解析_技术路上的苦行僧的博客-CSDN博客一、前言 我们知道AOP生成代理对象底层是根据JDK动态代理或者是Cglib动态代理,如果不熟悉代理模式的可以先去看下:设计模式(20)之代理模式_技术路上的苦行僧的博客-CSDN博客,下面直接来分析对应的源码。二、JDK动态代理源码分析下面先来看下我们一般是如何使用JDK动态代理的。/** * @author maoqichuan * @date 2022年03月22日 18:42 */public class CalculatorProxy {public s...https://blog.csdn.net/jokeMqc/article/details/123677831?spm=1001.2014.3001.5501

2.2服务注册中心

    2.2.1 服务注册发现的作用

        在高可用的生产环境中,一般都是以集群方式提供服务,集群里面的IP可能随时变化,也可能会随着维护扩充或减少节点,客户端需要及时感知服务端的变化,获取集群最新服务节点的连接信息。

    2.2.2 服务注册发现功能

服务注册:在服务提供方启动的时候,将对外暴露的接口注册到注册中心内,注册中心将这个服务节点的IP和接口等连接信息保存下来,为了检测服务的服务端的有效状态,一般会建立双向心跳机制。

服务订阅:在服务调用方启动的时候,客户端去注册中心查找并订阅服务提供方的IP,然后缓存到本地,并用于后续的远程调用。如果注册中心信息发生变化,一般采用推送的方式做更新。

2.2.3 服务注册发现的具体流程

        主流服务注册工具有Nacos、Consul、Zookeeper等。

        基于Zookeeper的服务发现:Zookeeper集群作为注册中心集群,服务注册的时候只需要服务节点像Zookeeper节点写入注册信息即可,利用Zookeeper的Watcher机制完成服务订阅与服务下发功能。

        

  •  A. 先在 ZooKeeper 中创建一个服务根路径,可以根据接口名命名(例 如:/micro/service/com.itcast.xxService),在这个路径再创建服务提供方与调用方目(serverclient),分别用来存储服务提供方和调用方的节点信息。
  • B. 服务端发起注册时,会在服务提供方目录中创建一个临时节点,节点中存储注册信 息,比如 IP,端口,服务名称等等。
  • C. 客户端发起订阅时,会在服务调用方目录中创建一个临时节点,节点中存储调用方的信息,同时 watch 服务提供方的目录(/service/com.demo.xxService/server)中所有的服务节点数据。当服 务端产生变化时,比如下线或宕机等,ZooKeeper 就会通知给订阅的客户端。

        Zookeeper方案的特点:Zookeeper的一大特点就是强一致性,Zookeeper集群的每个节点的数据每次发生更新操作,都会通知其他Zookeeper节点同时执行更新,它要求每个节点的数据能够实时的完全一致,这样也就会导致Zookeeper集群性能上的下降,ZK采用的CP模式(保持强一致性),如果要注重性能,可以考虑采用AP模式(保证最终一致性)的注册中心组建,比如Nacos等。

2.3网络IO模型

        网络IO模型分为5种:

  • 同步阻塞IO(BIO)
  • 同步非阻塞IO(NIO)
  • IO多路复用
  • 信号驱动IO
  • 异步非阻塞IO(AIO)

        通常的是同步阻塞IO和IO多路复用模型。

2.3.1 什么是阻塞IO模型

         通常由一个Acceptor线程负责监听客户端的连接。一般通过while(true)循环中服务端会调用accept()方法等待接收客户端的连接的方式监听请求,请求一旦接收到连接请求,就可以建立通信套接字,在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,知道客户端的操作执行完成。

        系统内核IP操作分为两个阶段:等待数据和拷贝数据,而在这两个阶段中,应用进程IO操作的线程会一直都处于阻塞状态,如果基于Java多线程开发,那么每一个IO操作都要占用线程,直至IO操作完成。

2.3.2 IO多路复用

        概念:服务端采用单线程过select/epoll机制,获取fd列表,遍历fd的所有时间,可以关注多个文件描述符,使其能够支持更多的并发连接。

        IO多路复用的实现主要有select、poll和epoll模式。

        文件描述符:在Linux系统中一切皆可以看成是文件,文件又可以分为:普通文件、目录文件、链接文件和设备文件。

        文件描述符(file descriptor)是内核为了高效管理已经被打开的文件所创建的索引,用来指向被打开的文件。文件描述符的值是一个非负整数。

        下图说明(左边是进程、中间是内核、右边是文件系统):

  1. A的文件描述符130都指向了同一个打开的文件句柄, 代表进程多次执行打开操作。
  2. A 的文件描述符 2 B 的文件描述符 2 都指向文件句柄( #73 ),代表 A 和程 B 可能是父子进程或
    A 和进程 B 打开了同一个文件(低概率)。
    1. (时间紧张可不讲) A 的描述符 0 B 的描述符 3 分别指向不同的打开文件句柄,但这些句柄均指 向i-node 表的相同条目( #1936 ),这种情况是因为每个进程各自对同一个文件发起了打开请求。
       

         程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文 件,它的文件描述符会是3。

        三者的区别:

select/poll处理流程:

 

epoll的处理流程:

 

         

        

        当连接有I/O 流事件产生的时候, epoll 就会去告诉进程哪个连接有 I/O流事件产生,然后进程就去处理这 个进程。这样性能相比要高效很多.epoll 可以说是 I/O 多路复用最新的一个实现, epoll 修复了 poll select 绝大部分问题. 

2.4 零拷贝

        系统内核处理 IO 操作分为两个阶段:等待数据和拷贝数据。

  • 等待数据,就是系统内核在等待网卡接收到数据后,把数据写到内核中。
  • 拷贝数据,就是系统内核在获取到数据后,将数据拷贝到用户进程的空间中。
    具体流程:

     

        应用进程的每一次操作,都会把数据写到用户空间的缓冲区中,再由CPU将数据拷贝到系统内核的缓冲区中 ,之后再由DMA将这份数据拷贝到网卡中,最后由网卡发送出去,这里我们可以看到,一次写操作数据拷贝两次才能通过网卡发送出去,而用户进程的读操作则是将整个流程反过来,数据同样会拷贝两次才能让应用程序读取到程序。

        所谓的零拷贝,就是取消用户空间与内核空间之间的数据拷贝操作,应用进程每一次的读写操作都可以通过一种方式,让应用进程向用户控件写入或者读取数据,就如同直接向内核控件写入或者读取数据一样,在通过DMA将内核总的数据拷贝到网卡,或将网卡的数据拷贝到内核中。

2.4.1 RPC框架中的零拷贝应用

        Netty框架是否也有零拷贝机制,其实Netty零拷贝机制有些不一样,他完全站在用户空间之上,也就是基于JVM之上,Netty当中的零拷贝是如何实现零拷贝的。

        RPC 并不会把请求参数作为一个整体数据包发送到对端机器上,中间可能会拆分,也可能会合并其 他请求,所以消息都需要有边界。接收到消息之后,需要对数据包进行处理,根据边界对数据包进 行分割和合并,最终获得完整的消息。

        Netty零拷贝主要体现在三个方面:

  1. Netty 的接收和发送 ByteBuffer 是采用 DIRECT BUFFERS ,使用 堆外的直接内存 (内存对象分 配在JVM 中堆以外的内存)进行 Socket 读写,不需 要进行字节缓冲区的二次拷贝 。如果采用传 统堆内存(HEAP BUFFERS )进行 Socket 读写, JVM 会将 堆内存 Buffer 拷贝一份到直接内存 ,然后写入 Socket 中。
  2. Netty 提供了 组合 Buffer 对象 ,也就是 CompositeByteBuf 类,可以将 ByteBuf 分解为多个共
    享同一个存储区域的 ByteBuf,避免了内存的拷贝。
  3. Netty的文件传输采用了 FileRegion 中包装 NIO FileChannel.transferTo() 方法,它可以
    直接将文件缓冲区的数据发送到目标 Channel ,避免了传统通过循环 write 方式导致的内存拷
    贝问题。

​​​        零拷贝带来的作用就是避免没必要的 CPU 拷贝,减少了 CPU 在用户空间与内核空间之间的上下文 切换,从而提升了网络通信效率与应用程序的整体性能。 

        Netty的零拷贝与操作系统的零拷贝是有一些区别的,Netty的零拷贝实际上是对用户控件中数据操作的优化,这对处理TCP传输中的拆包粘包问题有着重要意义,通过CompositeByteBuf可以有效解决这些问题。

        在RPC框架的开发和应用过程中,我们要深入了解网络通信相关的原理知识,尽量做到零拷贝,比如采用Netty框架作为RPC通信;我们要合理使用ByteBuff子类,做到完全零拷贝,提升RPC框架的整体性能。

2.5时间轮

2.5.1为什么需要时间轮

        在Dubbo中,为增强系统的容错能力,会有相应的监听判断处理机制,比如RPC调用的超时机制的实现,消费者判断RPC调用是否超时,如果超时结果返回给应用层。在Dubbo最开始的实现中,是将所有的返回结果(DefaultFuture都放入一个集合中,并且通过一个定时任务,每隔 一定时间间隔就扫描所有的future,逐个判断是否超时。

        这样子的实现方式虽然简单,但是存在一个问题就是会有很多无意义的遍历操作,比如一个RPC超时时间是10s,而设置超时判定的定时任务是2s执行一次,那么可能会有4次左右的无意义的循环遍历操作。

        为了解决上述场景的类似问题,Dubbo借鉴Netty,引入了时间轮算法,减少无意义的轮训判断操作。

2.5.2 时间轮原理

     对于以上问题, 目的是要减少额外的扫描操作就可以了。比如说一个定时任务是在5 秒之后执行,那么在 4.9 秒之后才扫描这个定时任务,这样就可以极大减少 CPU开销。这时我们就可以利用时钟轮的机制了。

        时钟轮的实质上是参考了生活中的时钟跳动的原理,那么具体是如何实现呢?

         在时钟轮机制中,有时间槽和时钟轮的概念,时间槽就相当于时钟的刻度;而时钟轮就相当于指针跳动的一个周期,我们可以将每个任务放到对应的时间槽位上。

        如果时钟轮有 10 个槽位,而时钟轮一轮的周期是 10 ,那么我们每个槽位的单位时间就 1 ,而下一层时间轮的周期就是 100 秒,每个槽位的单位时间也就是 10 秒,这就好比秒针与分 针, 在秒针周期下, 刻度单位为秒, 在分针周期下, 刻度为分。

        

        假设现在我们有 3 个任务,分别是任务 A0.9秒之后执行)、任务 B2.1秒后执行)与任务 C(12.1秒之后执行),我们将这 3 个任务添加到时钟轮中,任务 A 被放到第 0 槽位,任务 B 被放 到第 2槽位,任务 C 被放到下一层时间轮的第2个槽位,如下图所示:

 

      通过这个场景我们可以了解到,时钟轮的扫描周期仍是最小单位1秒,但是放置其中的任务并没有 反复扫描,每个任务会按要求只扫描执行一次, 这样就能够很好的解决CPU 浪费的问题。 叠加时钟轮, 无限增长, 效率会不断下降,该如何解决?设定三个时钟轮, 小时轮, 分钟轮, 秒 级轮。

2.5.2 Dubbo的时间轮原理是如何实现

        主要是通过TimerTimeoutTimerTask几个接口定义了一个定时器的模型,再通过 HashedWheelTimer这个类实现了一个时间轮定时器(默认的时间槽的数量是512,可以自定义这 个值)。它对外提供了简单易用的接口,只需要调用newTimeout接口,就可以实现对只需执行一 次任务的调度。通过该定时器,Dubbo在响应的场景中实现了高效的任务调度。

        时间轮核心类HashedWheelTimer结构:

 

2.5.3 时间轮在RPC的应用

  • 调用超时与重试机制:上面所讲的客户端超时的处理,就可以应用到时间轮,我们每发一次请求,都创建一个处理请求超时的定时任务放到时间轮里,在高并发、高访问量的情况下,时钟轮每次只轮询一个时间槽中的任务,这样子会节省大量的CPU。
  • 定时心跳检测机制:RPC 框架调用端定时向服务端发送的心跳检测,来维护连接状态,我们可以将心跳的逻辑封装为一个心跳任务,放到时钟轮里。心跳是要定时重复执行的,而时钟轮中的任务执行一遍就被移除了,对于这种需要重复执行的定时任务我们该如何处理呢?我们在定时任务逻辑结束的最后,再加上一段逻辑, 重设这个任务的执行时间,把它重新丢回到时钟轮。这样就可以实现循环执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值