微服务系列--深入理解RPC底层原理与设计实践(上)

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 微服务系列--深入理解RPC底层原理与设计实践(上)

在微服务系统当中,各个服务之间进行远程调用的时候需要考虑各种各样的场景,例如以下几种异常情况:


  • 超时调用
  • 失败重试
  • 服务下线通知
  • 服务上线通知
  • 服务分组
  • 请求队列


等等…

国内也有一些有先见之明的技术专家们对于这些技术有了较早的认知,因此很早便开始了关于远程服务调用中间件的开发。慢慢地,一些国内大厂自研的RPC调用框架开始变做了一款产品向市面上去进行推广。


今年年初的时候,我花了大概一个半月的业余时间自己打磨了一套RPC框架,通过实践尝试后发现,要想真正地落地一款给公司内部使用的RPC框架难度真的超乎想象。本文不会过多地去介绍市面上某一款中间件的底层源代码是如何执行和编写的,更多是通过结合一些中间件底层设计的原理来阐述 我自己是如何设计一款RPC框架的。


准备工作



为了写一款可用的RPC框架,我大概准备了这些技术工作:


  • 阅读了Dubbo内部的大量源码设计。
  • 了解关于RPC框架设计的难点和痛点。(这里主要感谢nx的东哥,上了他的课程之后很多技术点都有了新的理解和感悟)
  • 不断地实践和测试。
  • 自己编写的中间件如何能够优雅地接入Spring容器。


RPC的整体设计思想



起初在设计RPC远程调用框架的时候,主要的设计思路是采用了经典的生产者-消费者思想。客户端发送请求,服务端接收之后匹配本地已有的服务方法进行处理执行。


但是在实际到落地过程中却发现,其中的技术复杂性远远超出预期~~

最终结果如下图所示:


整个项目的包结构整理


网络异常,图片无法展示
|


客户端调用:


网络异常,图片无法展示
|


服务端使用:


网络异常,图片无法展示
|


ps:这里面的每个api和设计思路大部分都是模仿了Dubbo框架内部的源代码设计以及部分自己的改编。


本地代理的设计



为了能够保证远程方法的调用使用起来和本地方法调用一样简单,通常可以使用代理模式去实现。场景的代理模式有好2大类:静态代理和动态代理,静态代理需要通过硬编码的方式实现,不现实,这里直接不合适。


动态代理主要有以下两种:


  • JDK代理
  • CGLIB代理


Java给出了动态代理,动态代理具有如下特点:


1.Proxy对象不需要implements接口;


2.Proxy对象的生成利用JDK的Api,在JVM内存中动态的构建Proxy对象。需要使用java.lang.reflect.Proxy类的newProxyInstance接口


public static <T> T getProxy(final Class interfaceClass, ReferenceConfig referenceConfig) {
        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //每次执行目标方法的时候都会回调到这个invoke方法处
                return null;
            }
        });
    }
复制代码


JDK动态代理要求target对象是一个接口的实现对象,假如target对象只是一个单独的对象,并没有实现任何接口,这时候就会用到Cglib代理(Code Generation Library),即通过构建一个子类对象,从而实现对target对象的代理,因此目标对象不能是final类(报错),且目标对象的方法不能是final或static(不执行代理功能)。


//给目标对象创建一个代理对象
    public Object getProxyInstance() {
        //工具类
        Enhancer en = new Enhancer();
        //设置父类
        en.setSuperclass(target.getClass());
        //设置回调函数
        en.setCallback(this);
        //创建子类代理对象
        return en.create();
    }
    public Object intercept(Object object, Method method, Object[] arg2, MethodProxy proxy) throws Throwable {
        System.out.println("before");
        Object obj = method.invoke(target);
        System.out.println("after");
        return obj;
    }
复制代码


我最终选择了JDK作为基本的动态代理实现方案,一开始的技术选型并没有选择更加完美的方案,而是采用了最为简单熟悉的技术。


如果读者感兴趣的话,可以阅读我之前介绍aop原理的文章,内部有详细讲解cglib底层原理的细节。点击这跳转


远程调用的数据传输



本地代理设计好了之后,需要考虑如何将数据发送给到服务端的问题了。底层采用的是netty框架,为了避免粘包和拆包的问题,我尝试使用了ObjectEncoder和ObjectDecoder两个netty内置的组件。


关于netty内部出现粘包,拆包现象的解决手段,可以细看这篇文章:

www.cnblogs.com/rickiyang/p…


协议体的内部需要设计哪些字段?


大概整理了一下代码,基本结构如下所示:


public class IettyProtocol implements Serializable {
    private static final long serialVersionUID = -7523782352702351753L;
    /**
     * 魔数
     */
    protected long MAGIC = 0;
    /**
     * 客户端的请求id
     */
    private String requestId;
    /**
     * netty专属
     */
    private ChannelHandlerContext channelHandlerContext;
    /**
     * 0请求 1响应
     * @see CommonConstants.ReqOrRespTypeEnum
     */
    protected byte reqOrResp = 0;
    /**
     * 0需要从服务端返回数据 1不需要从服务端响应数据
     */
    protected final byte way = 0;
    /**
     * 0是心跳时间,1不是心跳事件
     */
    private byte event = 0;
    /**
     * 序列化类型
     */
    private String serializationType;
    /**
     * 状态
     */
    private short status;
    /**
     * 返回的数据类型格式
     */
    private Type type;
    /**
     * 消息体 请求方发送的函数类型,参数信息都存在这里, 接收方响应的信息也都存在这里
     */
    private byte[] body;
}
复制代码


稍微解释几个字段:


requestId 客户端的请求id(用于请求响应做必配使用,下文中会介绍到)


reqOrResp 协议数据包的类型(标示该数据包是请求类型还是响应类型)


type 是指调用该方法的返回数据格式类型 (例如int,String,返回类型在做数据的序列化转换的时候会非常有用)


body 这里面是核心重点,主要的调用服务名称,参数,方法等详细信息都会先转换为字节数组,然后再通过网络将其发送出去。


如何将不同格式的数据转换为字节数组


数字类型

将数字类型转换为二进制,在之前的一篇文章中我有写过详细的底层实现机制,核心是通过将数字的二进制数右移8位,然后存入byte数组当中。核心代码为:


/**
     * 字节转成数字 int 大小是4个字节
     *
     * @param bytes
     * @return
     */
    public static int byteToInt(byte[] bytes) {
        if (bytes.length != 4) {
            return 0;
        }
        return (bytes[0]) & 0xff | (bytes[1] << 8) & 0xff00 | (bytes[2] << 16) & 0xff0000 | (bytes[3] << 24) & 0xff000000;
    }
    /**
     * 数字转成字节 int 大小是4个字节
     *
     * @param n
     * @return
     */
    public static byte[] intToByte(int n) {
        byte[] buf = new byte[4];
        for (int i = 0; i < buf.length; i++) {
            buf[i] = (byte) (n >> (8 * i));
        }
        return buf;
    }
复制代码


字符串类型


将字符串转换为对应的字符数组,然后每个数组的char类型使用asc码映射为数字,接下来又是回归到数字转换的思路上了。


集合,复杂对象类型


这些类型可以尝试先通过json转换为字符串,然后再将字符串转换为char数组,再转换为数字数组类型,后还是要回归到数字转换的思路上。


数据接收与响应设计



早期在做RPC通讯设计的时候,采用的是简单的生产者消费者模型。下边给出早期自己在进行实现过程中所思考的一些点:


同步发送数据


网络异常,图片无法展示
|


这类同步发送设计案例来看,consumer端发送数据之后,consumer会一直处于等待状态,只有等到数据抵达到provider端并且处理完毕之后,consumer端才会继续进行下去。


这样设计的弊端很明显:


consumer和provider的吞吐量都不高,而且一旦某个接口出现了超时还会影响其他接口的调用堵塞。


consumer端异步发送,provider端异步接收处理


这里需要引入两个新的概念,io线程和业务线程。整体设计如下图所示:


网络异常,图片无法展示
|


客户端发送数据的时候,不再是处于等待的状态,它会只需要将数据放入到一个本地的请求队列中即可。客户端的io线程会不断地尝试从队列中取出数据,然后进行网络发送。


服务端也会专门有一个io线程负责接收这类数据,接着将数据放入到服务端的一个队列缓冲中,然后再交给服务端的业务线程池去慢慢消费掉服务端的缓冲队列内部的数据。

服务端的核心设计如下:


网络异常,图片无法展示
|


provider端的数据处理完毕之后该如何正确返回?


为了解决这个问题,我尝试阅读了一下Dubbo的底层源代码,然后借鉴了其中的设计思路进行了一波实现。


客户端如何接收响应


其核心的本质是客户端在发送请求到时候会生成一个唯一的requestId,然后客户端在发送数据之后,会有一个Map集合(key是requestId,value是接口响应值)管理接口响应到数据,客户端的调用线程在执行了写入数据到发送队列之后需要不断监听Map集合中对应requestId的value是否有值,如果超过指定时间都没有数据,那么就抛出超时异常,如果收到了响应数据则正常返回即可。


服务端返回响应


服务端的本地代码正常处理完数据之后要将数据写入一个Map集合中,服务端的io线程会不断轮训这份Map集合(key是客户端发送过来的requestId,value是本地代码处理完之后写入的数据),如果发现对应的requestId有写好的返回数据,就会将其发送给客户端。


整体设计大概如下图所示:


网络异常,图片无法展示
|

目录
相关文章
|
15天前
|
Kubernetes 安全 Java
构建高效微服务架构:从理论到实践
【4月更文挑战第9天】 在当今快速迭代与竞争激烈的软件市场中,微服务架构以其灵活性、可扩展性及容错性,成为众多企业转型的首选。本文将深入探讨如何从零开始构建一个高效的微服务系统,覆盖从概念理解、设计原则、技术选型到部署维护的各个阶段。通过实际案例分析与最佳实践分享,旨在为后端工程师提供一套全面的微服务构建指南,帮助读者在面对复杂系统设计时能够做出明智的决策,并提升系统的可靠性与维护效率。
|
8天前
|
消息中间件 运维 监控
现代化软件开发中的微服务架构设计与实践
本文将深入探讨现代化软件开发中微服务架构的设计原则和实践经验。通过分析微服务架构的优势、挑战以及常见的设计模式,结合实际案例,帮助开发者更好地理解如何构建可靠、可扩展、高效的微服务系统。
|
8天前
|
负载均衡 Java 开发者
细解微服务架构实践:如何使用Spring Cloud进行Java微服务治理
【4月更文挑战第17天】Spring Cloud是Java微服务治理的首选框架,整合了Eureka(服务发现)、Ribbon(客户端负载均衡)、Hystrix(熔断器)、Zuul(API网关)和Config Server(配置中心)。通过Eureka实现服务注册与发现,Ribbon提供负载均衡,Hystrix实现熔断保护,Zuul作为API网关,Config Server集中管理配置。理解并运用Spring Cloud进行微服务治理是现代Java开发者的关键技能。
|
12天前
|
Kubernetes 监控 Cloud Native
构建高效云原生应用:基于Kubernetes的微服务治理实践
【4月更文挑战第13天】 在当今数字化转型的浪潮中,企业纷纷将目光投向了云原生技术以支持其业务敏捷性和可扩展性。本文深入探讨了利用Kubernetes作为容器编排平台,实现微服务架构的有效治理,旨在为开发者和运维团队提供一套优化策略,以确保云原生应用的高性能和稳定性。通过分析微服务设计原则、Kubernetes的核心组件以及实际案例,本文揭示了在多变的业务需求下,如何确保系统的高可用性、弹性和安全性。
16 4
|
21天前
|
消息中间件 监控 API
构建高性能微服务架构:从理论到实践
【4月更文挑战第4天】 在当今互联网应用的快速迭代和高并发需求下,传统的单体应用架构已不足以满足市场的灵活性与扩展性要求。微服务架构以其独立部署、弹性伸缩、技术多样性等优势,成为众多企业转型升级的首选方案。本文将深入探讨如何构建一个高性能的微服务系统,涵盖关键组件的选择、系统设计的考量以及性能优化的策略,旨在为开发者和架构师提供一套实用的指导思路和具体实践步骤。
|
23天前
|
消息中间件 安全 API
构建高效微服务架构:策略与实践
【4月更文挑战第1天】在数字化转型的浪潮中,微服务架构已成为企业追求敏捷、可扩展和灵活部署的重要技术手段。本文将深入探讨如何通过合理的设计原则和先进的技术栈,构建一个高效的微服务系统。我们将剖析微服务设计的核心要点,包括服务的划分、通信机制、数据一致性以及安全性问题,并结合案例分析,展示如何在现实世界中应用这些策略以提升系统的可靠性和性能。
|
24天前
|
设计模式 API 持续交付
构建高效微服务架构:从理论到实践
在当今快速迭代和部署的软件开发环境中,微服务架构已成为一种流行的设计模式,它允许开发团队以模块化的方式构建、维护和扩展应用程序。本文将深入探讨微服务的核心概念,包括其定义、优势、挑战以及如何在实际项目中实施。我们将通过一个实际案例来展示如何将传统的单体应用拆分成一系列独立、松耦合的服务,并通过容器化、服务发现、API网关和持续集成/持续部署(CI/CD)等技术手段来管理这些服务。
|
27天前
|
Java fastjson 数据安全/隐私保护
【Dubbo3技术专题】「云原生微服务开发实战」 一同探索和分析研究RPC服务的底层原理和实现
【Dubbo3技术专题】「云原生微服务开发实战」 一同探索和分析研究RPC服务的底层原理和实现
39 0
|
5天前
|
敏捷开发 监控 数据管理
构建高效微服务架构的五大关键策略
【4月更文挑战第20天】在当今软件开发领域,微服务架构已经成为一种流行的设计模式,它允许开发团队以灵活、可扩展的方式构建应用程序。本文将探讨构建高效微服务架构的五大关键策略,包括服务划分、通信机制、数据管理、安全性考虑以及监控与日志。这些策略对于确保系统的可靠性、可维护性和性能至关重要。
|
17天前
|
API 数据库 开发者
构建高效可靠的微服务架构:后端开发的新范式
【4月更文挑战第8天】 随着现代软件开发的复杂性日益增加,传统的单体应用架构面临着可扩展性、维护性和敏捷性的挑战。为了解决这些问题,微服务架构应运而生,并迅速成为后端开发领域的一股清流。本文将深入探讨微服务架构的设计原则、实施策略及其带来的优势与挑战,为后端开发者提供一种全新视角,以实现更加灵活、高效和稳定的系统构建。
23 0