SpringBoot

详细谈谈SpringCloud的负载均衡实现与@LoadBala

2022-05-21  本文已影响0人  Wannay

在本文的第一章当中,我们将会大概去介绍SpringCloud如何去实现负载均衡?

在本文的第二章当中,我们将会去介绍@LoadBalanced注解的真正的生效的原理是什么?我们也将详细介绍@Qualifier注解的实现原理。

1. SpringCloud负载均衡与@LoadBalanced注解

也许很多人刚学SpringCloud的时候,跟着视频进行学习时,老师会说@LoadBalanced注解标在RestTemplate上就能实现负载均衡,至于为什么呢?本文的第二章将会进行介绍。

如下面的代码所示(代码为Kotlin代码)

    @Bean
    @LoadBalanced
    fun restTemplate(): RestTemplate {
        return RestTemplate()
    }

我们打开@LoadBalanced的源码

image.png

我们可以看到@LoadBalanced的源码上标注了一个@Qualifier注解。并且对这个注解的描述信息为,它标识RestTemplateWebClient的Bean,被配置成为了一个LoadBalancerClient

1.1 LoadBalancerClient是什么?

LoadBalancerClient是SpringCloud当中对于负载均衡的客户端的一层抽象,它的实现类有两个RibbonLoadBalancerClientBlockingLoadBalancerClient

image.png

首先我们应该知道的是LoadBalancerClient是来自于spring-cloud-common包下,而
RibbonLoadBalancerClient是来自于spring-cloud-ribbon包下,BlockingLoadBalancerClient是来自于SpringCloud自家做的spring-cloud-loadbalancer包下。

也就是说,SpringCloud当中提供负载均衡的统一接口是LoadBalancerClient,而RibbonLoadBalancer则需要针对于SpringCloud当中提供的规范去进行提供自己的实现。

我们以Ribbon为例

image.png

我们可以看到,RibbonLoadBalancerClient当中execute的实现方式为,首先要获取一个ILoadBalancer,接着根据ILoadBalancer去使用特定的负载均衡策略,选择出来一个合适的Server,来完成本次请求的执行。

image.png

上面我们展示了chooseServer的逻辑,我们发现,它是组合了Rule(规则),来完成的Server的选择。具体的Rule的实现如下,如果你了解过Ribbon,肯定对这些策略并不陌生(起码也会有一定的了解,从我第一次看SpringCloud时,就听说过这个概念)。

image.png

下面是ILoadBalancer的接口规范:

image.png

需要注意的是,不管是ILoadBalancer还是IRule,这些组件所在的包,可都是com.netflix,与SpringCloud可以说是毫无关系。而RibbonLoadBalancerClient是什么?它就是针对SpringCloud的规范,将Netflix的负载均衡策略,全部桥接到SpringCloud当中,让SpringCloud可以使用到Ribbon的负载均衡算法。

BlockingLoadBalancerClient的实现肯定也类似,只不过它肯定用到的是SpringCloudLoadBalancer的LoadBalancer罢了,我们这里就不进行展开讲了,感兴趣的小伙伴可以自行查阅相关的源码。

1.2 为什么加上@LoadBalanced注解就能让RestTemplate拥有负载均衡的能力?

是不是觉得很神奇呢?从注解上似乎并未告诉我们为什么以及怎么实现,只是说这是一个标识负载均衡的注解罢了。我们来找到SpringCloudCommon包的源码,并找到自动配置类LoadBalancerAutoConfiguration

image.png

首先,我们可以看到,它直接Autowired注入了容器当中配置的所有的RestTemplate。还给容器中放入了一个SmartInitializingSingleton这样的一个Bean。

在了解后续之前,我们先来了解SmartInitializingSingletonRestTemplateCustomizer是什么?

SmartInitializingSingleton是什么?

相信各位熟悉Spring的朋友,应该都有用过Spring的InitializingBean吧,是一个Bean的初始化方法的回调方法。

image.png

但是也许你会遇到,你想在这里去容器当中去进行各种的getBean的情况。但是实际上在这里完成初始化是有可能产生问题的,因为获取的时机还比较早,这时候Spring容器的有些配置有可能还没完成呢,这时就去getBean就有可能产生一些问题(其实一般情况也遇不到)。

与该接口对应的,Spring还有一个接口是SmartInitializingSingleton,它其实也是作为一个Bean的初始化回调方法,它会在Spring当中的所有的Bean都完成实例化和初始化之后再去进行回调。

image.png

它的具体回调时机如下,第一部分是实例化所有的单实例Bean,第二部分则是回调所有的SmartInitializingSingleton,它也完全可以用来初始化Bean,是没有任何问题的。

image.png

RestTemplateCustomizer是什么?

image.png

首先,我们知道RestTemplate,是一个HttpClient的客户端,可以用于完成HTTP请求的发送和处理,并且它还是一个InterceptingHttpAccessor,支持去添加请求的拦截器,对请求去进行处理。(注意RestTemplate是来自于Spring的web包)

SpringCloud当中,针对于RestTemplate去提供了RestTemplateCustomizer,支持去对RestTemplate去进行自定义吗,我们可以实现这个接口,在customize方法当中去添加,我们自定义的逻辑。

Spring当中,针对于XXXCustomizer的实现,其实非常非常多,我们从名字也可以知道,是对XXX的一个自定义化器,支持去对XXX去进行自定义操作。

image.png

在了解了上面的知识之后,我们继续来看。

image.png

我们已经知道了Spring容器启动过程当中,它会回调所有的RestTemplateCustomizer,那么这些组件从哪来?我们往下翻,就会发现,它给容器当中配置了LoadBalancerInterceptor

image.png image.png

我们来看LoadBalancerInterceptor的实现:

image.png

我们可以看到,它从request当中获取到uri,并且以uri作为serviceName,把它交给了LoadBalancerClient去进行执行。然后,不就到了刚刚我们说的SpringCloud抽象了LoadBalancerClient了吗?也就是说SpringCloud使用自定义的LoadBalancerInterceptor去拦截了RestTemplate,将请求转交给了LoadBalancerClient去进行处理。

如果我们引入了Ribbon的jar包,那么这个LoadBalancerClient就会是Ribbon的实现;如果我们引入了SpringCloudLoadBalancer的jar包,那么这个LoadBalancerClient就会是SpringCloudLoadBalancer,只要导入一方的相关的实现配置,它就会被SpringBoot所整合,这也就是我们所说的SpringBoot的自动配置。

整体的流程大概如下:

image.png

1.3 自定义RPC远程调用的协议

类似地,如果我们想要自定义相关的负载均衡的功能(并且不想使用SpringCloud提供的RestTemplate或者是SpringCloudOpenFeign)。比如说,我们想不使用HTTP请求去完成RPC的远程调用(RestTemplate/SpringCloudOpenFeign/WebClient都是基于HTTP的),我们只需要从Spring容器当中去注入LoadBalancerClient,并且自定义LoadBalancerRequest回调函数去指定处理请求的逻辑即可实现负载均衡。

image.png

至于在回调函数当中,我们想要使用何种方式去发送网络请求获取数据,那就是我们完全可以去自定义的部分了。

2. @LoadBalanced@Qualifier之间的关系?

在上面的代码当中,我们会见到下面这样的代码,在@Autowired上打了@LoadBalanced注解。

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

这是什么操作?字段上标注@LoadBalanced?干啥的?我这时候有以下两个想法:

按照我对Spring的源码的理解,使用第一种的概率比较大,于是我找遍了整个SpringCloud的Common包下的源码,没有发现。但是第二种,完全没听说过这种用法,应该是不太可能,我看过的源码里似乎也没有这个插曲。

但是这时候,我想起来了Qualifier注解,这个注解引起了我的单独注意。

2.1 AutowireCandidateResolver是什么

Spring在处理自动注入(不只是针对@Autowired注解,也针对@Resource注解,以及@Inject等注解)时,会使用DefaultListableBeanFactory.resolveDepenency方法去完成依赖的注入。

image.png

在处理自动注入时,会用到AutowireCandidateResolver,去判断容器当中的所有的Bean,挨个去匹配是否可以作为当前依赖的注入对象。从名字,我们也可以知道些什么,AutowireCandidateResolver,自动装配的候选的解析器。至于如何判断?具体逻辑就在isAutowireCandidate方法当中了。

我们找到它的子类QualifierAnnotationAutowireCandidateResolver,这个类是我们需要重点研究的类。

image.png

这里它会从字段上、方法参数上、方法上、构造器上,去匹配Qualifier注解,需要注意的是,这里(以及后续)提到的Qualifier注解,并不只是Spring家的Qualifier,也包含javax.inject当中的Qualifier,甚至是自定义的Qualifier。

有一个点是我们要关注的,向下传递的参数descriptor.getAnnotations()是要进行注入的元素的注解列表。

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

比如上面的代码当中,获取到的注解,就是AutowiredLoadBalanced这两个注解。

image.png

接着,就是要注入的元素上的注解,和Qualfier去进行匹配,如果匹配了(也就是说要注入的元素上有@Qualifier注解的话),那么走进去checkQualifier方法,检查Qualifier注解的情况。这个方法很长,我们分成两段去进行介绍:

image.png

它会从各个地方去检查Qualifier注解,我们主要关注下面三种情况:

如果找到了?就会在下面触发这样的代码:

targetAnnotation != null && targetAnnotation.equals(annotation)

也就是Bean(类上,@Bean方法上,或者更多地方)上的Qualifier,和要注入的Qualfier完全匹配的话(也就是说是一对一模一样的注解),那么return true。。如果这里没有检查到?那么就是下一段代码了

image.png

这里,它会从annotation(要注入的元素上的Qualifier注解)上去获取到value属性,和bdHolder当中的name是否匹配?如果匹配的话,那么return true。也就是我们常说的,Qualifier去指定beanName去进行注入的情况。

这里其实有一个点,我们值得注意,那就是,如果没有Qualifier注解,那么是根本不会去进行匹配Qualifier注解的,只需要类型匹配,就return true(匹配类型这部分代码在QualifierAnnotationAutowireCandidateResolver的父类当中)。如果有Qualifier注解的话,那么去匹配Qualifier注解的情况。

2.2 我们继续谈谈为什么@LoadBalanced能实现负载均衡?

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

上面这段代码,经过我们上面的分析,它会去匹配RestTemplate上是否有Qualifier注解?并且如果有Qualifier注解的话,必须完全匹配才行。

    @Bean
    @LoadBalanced
    fun restTemplate(): RestTemplate {
        return RestTemplate()
    }

而我们通过上面的代码,因为两边都加了@LoadBalance注解,因此两边都有@Qualifier注解,并且两边的@Qualifier注解的value属性也确实都是空字符串,因此,那不是就恰好命中了这段代码,去return true了?

targetAnnotation != null && targetAnnotation.equals(annotation)

那么,如果我们给@Bean方法上打个@Qualifier注解,是不是就行了?当然可以,我们可是看了源码并进行追根究底的!

另外一个角度说,SpringCloud当中对于@LoadBalanced注解当中的注释对这个注解的定位是mark(标识)其实是没有问题的,其实就正好符合我们的定位。为什么要有@Service@Repositry这种注解?其实也只是标识作用,为了让人一眼就看到就知道这个组件的作用是什么。

    @Bean
    @Qualifier
    fun restTemplate(): RestTemplate {
        return RestTemplate()
    }

到这里,爱搞事情的小朋友(我)就在想了,@Qualifier不是还能匹配beanName吗,那么我们能不能想办法造个beanName为空串的情况到容器当中呢?可惜,打开注册BeanDefinition的源码就发现了,根本不可能,你想放进去?还没放进去就给你抛异常了。

image.png

到这里,我们就彻底明白了。下面的代码当中@LoadBalanced,只是为了匹配所有的加了Qualifier(并且value属性为空字符串)的所有的RestTemplate,而我们自己加的@LoadBalanced,也只是为了给它加一个@Qualifier注解罢了。

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

我们简单做个分析:SpringCloud会自动注入容器当中的所有的@Qualifier的RestTemplate,并使用RestTemplateCustomizer,去给RestTemplate添加LoadBalancerInterceptor,让这个拦截器,将你真正的HTTP请求,直接转交给SpringCloud的LoadBalanceClient,在交给负载均衡组件,比如Ribbon或者是SpringCloudLoadBalancer使用负载均衡策略,选择出来一个合适的实例(Server/ServcieInstance),去完成本次请求的处理工作。

上一篇下一篇

猜你喜欢

热点阅读