一文详解RPC框架核心原理与手写实战
Posted 京东零售技术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文详解RPC框架核心原理与手写实战相关的知识,希望对你有一定的参考价值。
首先来回顾下经典的RPC架构图:
简单介绍就是:
步骤1:服务提供方启动时将服务注册到注册中心。
步骤5:调用双方与监控平台建立定时发送机制,用来统计调用次数与耗时等相关数据。
本demo同样遵循此架构图,但是没有实现步骤5的Monitor,因为它不是架构必须的,因此并不影响效果。本文意图是通过文章讲解结合代码调试,熟悉整个RPC框架发布和调用流程。对其技术细节,这里不做深究。
已经实现内容:
1.SPI机制。
2.服务发布与调用流程。(支持netty或http两种协议,通过SPI选型)
3.服务注册中心。(本地文件形式)
4.服务集群容错。(安全失败 或 失败重试 策略)
5.负载均衡。(随机 或 轮询 策略 且支持权重)
未实现内容:
1.没有实现 SPI的增强(DI和AOP)。
2.为了方便本地调试,注册中心采用了本地文件形式,未实现通知机制。
3. 未实现 异步、泛化、隐式参数,mock降级等功能。
4. 监控平台。
整个demo一共有 Provider 、Consumer 和 Sdk三个模块,其中Provider模块包括RPC接口的具体实现类、服务的发布流程、处理请求等逻辑的实现。Consumer包括消费端的启动与调用,通过SPI机制动态选择负载均衡和集群容错等逻辑。Sdk模块包含例如 注册中心、协议实现 等RPC相关组件的类。如下图所示:
1. 声明一个测试接口,并给出实现:
2. 通过修改端口,本地开启两个服务,如图所示:
3.客户端调用通过 轮询机制 调用10次:
4.结果展示:
由于框架每层都是组件化的,所以每层都用到了SPI机制,因此我们先从SPI原理讲起。SPI (Service Provider Interface) 是服务提供的发现机制,它类似策略模式,能将改动与原来的代码隔离开来,从而避免新增代码对原有代码的影响,通过 SPI 机制提供的灵活性,让框架拥有良好的插件特性,便于扩展。具体可以参考dubbo官网介绍:http://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html。
我们结合服务提供者核心类 ServerConfig 来讲解如何使用SPI加载扩展接口Protocal的实现类的,在ServerConfig 中有如下代码:
方法1、getExtensionLoader方法,及获取当前扩展接口对应的ExtensionLoader 对象,在整个模块中每个扩展接口对应着自己的ExtensionLoader 对象,内部通过并发Map 来缓存扩展接口与对应的ExtensionLoader 的映射,其中key 为扩展接口的Class 对象, value为对应的ExtensionLoader 实例:
方法2、getAdaptiveExtension方法,是采用JDK的动态代理方式,对本SPILoader中扩展接口生成一个对应的适配器类,适配器类里会根据特定export方法,选出合适的实现类,执行其方法。如下图所示:
方法3、getExtension方法根据接口路径找到相应的配置文件,然后通过配置文件的实现类的名称找到对应的扩展实现类,实际底层是用了反射把class对象放入了map缓存中。如下图所示:
上图loadResource方法是读取此接口相应的配置信息,key起任意名,value为实现类的全路径,然后通过反射把相应的class放入缓存中。
上图的createExtension 方法是把上一步的class通过newInstance创建实例保存到缓存中。
下面画出了 SPIloader的方法时序图,可以根据时序图再结合代码,可以清楚它调用关系:
通过时序图,看下RPC服务提供方启动流程,如下图所示:
步骤1、2、3、4启动spring环境,通过spring后置处理器对ServiceConfig对象进行初始化,然后执行其export方法,代码具体为:
步骤5为doExport方法,先把配置信息,转换为URL对象,然后把URL转换为Invoker对象,最后通过动态代理类和入参动态挑选一个 Protocol 实现类(SPI策略)执行export方法具体为:
步骤7、8、9的export()方法实际完成了 启动NettyServer监听服务链接,然后将服务注册到文件中。具体实现为:
上文可知,在服务发布过程中指定了NettyServerHandler处理业务请求,当请求过来时,首先把消息转为Invocation 对象,根据Invocation的接口名获取服务发布时候缓存中的具体的实例,最后根据Invocation的方法名和参数类型,反射执行具体的RPC方法代码如下:
通过时序图,看下RPC服务调用方启动流程:
步骤1、2:是启动spring环境,从ApplicationContext
里获取rpc接口,spring环境初始化时,会对ReferenceConfig
对象进行初始化,然后执行其get方法,代码具体为:
步骤3,get() 方法生成远程调用代理类,首先从配置中心获取此接口已经注册好的urls,然后循环urls调用protocol.refer方法(启动客户端链接)生成invokers list,再通过cluster.join方法,生成一个带有集群容错逻辑的invoker,最后在为此invoker做一层动态代理返回代理对象,具体代码为:
步骤5,PROTOCOL同样也是通过SPI机制拿到实现类,调用refer方法,作用是开启netty客户端返回一个客户单端调动对象invoker代码如下:
步骤6, CLUSTER 通过SPI机制获取实际类,调用join方法通过构造方法初始化一个带有集群容错机制的Invoker。
步骤7,最终通过JDK动态代理,返回一个代理类。
步骤2 调用rpc方法,实际调用的代理的invoke方法,首先通过方法对象创建一个Invocation对象,然后通过SPI生成对应的LoadBalance(负载均衡策略),最后执行invoker.doInvoke方法;
步骤4 doInvoke方法实现了集群容错逻辑,同时也执行了负载均衡的select方法:
步骤5 通过SPI筛选的负载均衡策略执行select方法返回具体的invoker对象例如:
步骤6,7调用选好的Invoker执行其invoke方法,底层是nettyclient的send方法:
集群容错机制实现了当服务消费方调用服务提供者的服务,出现错误时的容错方案,目前实现了下面几种,其中Failfast Cluster为当消费者调用服务提供者失败后,立即报错,也就是只调用一次;Failsafe Cluster当出现调用异常时,直接忽略异常;Failover Cluster当调用失败后,自动切换到其他服务提供者进行重新调用。当然也可以通过SPI机制进行定制化的容错逻辑。本demo已经实现的有:
下面介绍一下比较常用的 失败重试 策略的实现:
我们看到失败重试实际通过对重试次数进行循环便可实现,如果第一次调用成功,则直接跳出循环返回,否则进行循环重试。如果第一次调用出现了异常,仍然会继续循环调用,直到服务提供者都调用一遍,记录最后一个的异常,再返回。
当服务提供方有多台时,为了解决某台负载过高问题,需要负载均衡策略。本例中实现了下面两种:1、random随机策略。可以支持设置权重来影响服务提供者的概率。2、roudrobin轮询策略,通过服务提供者设置的权重,设置轮询的比率。如果有定制化需求,可以基于loadBalance进行定制。本demo已经实现的有:
下面介绍一下默认的random随机策略:
随机策略不是简单的random一个值就可以了,而是与服务提供方的权重有关。通过步骤7可以知道如果权重不一样,会根据总权重选择随机选择一个数,再去确定要用那个invoker。权重越大,选到的概率越大。
本文首先讲解了SPI原理,通过SPI 增强了框架对 协议层,集群容错,负载均衡 等相关组件的扩展性。然后对服务提供方启动 以及 接收的请求进行处理,和 服务消费方启动流程以及如何发起一次远程过程调用的过程,进行介绍。最后分别列举了一种集群容错,和负载均衡策略的具体实现,看完本文,相信大家对RPC的原理将会有直观的理解。
以上是关于一文详解RPC框架核心原理与手写实战的主要内容,如果未能解决你的问题,请参考以下文章