详细谈谈SpringCloud的负载均衡实现与@LoadBala
在本文的第一章当中,我们将会大概去介绍SpringCloud如何去实现负载均衡?
在本文的第二章当中,我们将会去介绍@LoadBalanced
注解的真正的生效的原理是什么?我们也将详细介绍@Qualifier
注解的实现原理。
1. SpringCloud负载均衡与@LoadBalanced
注解
也许很多人刚学SpringCloud的时候,跟着视频进行学习时,老师会说@LoadBalanced
注解标在RestTemplate
上就能实现负载均衡,至于为什么呢?本文的第二章将会进行介绍。
如下面的代码所示(代码为Kotlin代码)
@Bean
@LoadBalanced
fun restTemplate(): RestTemplate {
return RestTemplate()
}
我们打开@LoadBalanced
的源码
我们可以看到@LoadBalanced
的源码上标注了一个@Qualifier
注解。并且对这个注解的描述信息为,它标识RestTemplate
和WebClient
的Bean,被配置成为了一个LoadBalancerClient
。
1.1 LoadBalancerClient
是什么?
LoadBalancerClient
是SpringCloud当中对于负载均衡的客户端的一层抽象,它的实现类有两个RibbonLoadBalancerClient
和BlockingLoadBalancerClient
。
首先我们应该知道的是LoadBalancerClient
是来自于spring-cloud-common
包下,而
RibbonLoadBalancerClient
是来自于spring-cloud-ribbon
包下,BlockingLoadBalancerClient
是来自于SpringCloud自家做的spring-cloud-loadbalancer
包下。
也就是说,SpringCloud当中提供负载均衡的统一接口是LoadBalancerClient
,而Ribbon
、LoadBalancer
则需要针对于SpringCloud当中提供的规范去进行提供自己的实现。
我们以Ribbon
为例
我们可以看到,RibbonLoadBalancerClient
当中execute
的实现方式为,首先要获取一个ILoadBalancer
,接着根据ILoadBalancer
去使用特定的负载均衡策略,选择出来一个合适的Server,来完成本次请求的执行。
上面我们展示了chooseServer
的逻辑,我们发现,它是组合了Rule
(规则),来完成的Server的选择。具体的Rule
的实现如下,如果你了解过Ribbon,肯定对这些策略并不陌生(起码也会有一定的了解,从我第一次看SpringCloud时,就听说过这个概念)。
下面是ILoadBalancer
的接口规范:
需要注意的是,不管是ILoadBalancer
还是IRule
,这些组件所在的包,可都是com.netflix
,与SpringCloud可以说是毫无关系。而RibbonLoadBalancerClient
是什么?它就是针对SpringCloud的规范,将Netflix的负载均衡策略,全部桥接到SpringCloud当中,让SpringCloud可以使用到Ribbon的负载均衡算法。
BlockingLoadBalancerClient
的实现肯定也类似,只不过它肯定用到的是SpringCloudLoadBalancer的LoadBalancer罢了,我们这里就不进行展开讲了,感兴趣的小伙伴可以自行查阅相关的源码。
1.2 为什么加上@LoadBalanced
注解就能让RestTemplate拥有负载均衡的能力?
是不是觉得很神奇呢?从注解上似乎并未告诉我们为什么以及怎么实现,只是说这是一个标识负载均衡的注解罢了。我们来找到SpringCloudCommon包的源码,并找到自动配置类LoadBalancerAutoConfiguration
。
首先,我们可以看到,它直接Autowired注入了容器当中配置的所有的RestTemplate。还给容器中放入了一个SmartInitializingSingleton
这样的一个Bean。
在了解后续之前,我们先来了解SmartInitializingSingleton
和RestTemplateCustomizer
是什么?
SmartInitializingSingleton
是什么?
相信各位熟悉Spring的朋友,应该都有用过Spring的InitializingBean
吧,是一个Bean的初始化方法的回调方法。
但是也许你会遇到,你想在这里去容器当中去进行各种的getBean的情况。但是实际上在这里完成初始化是有可能产生问题的,因为获取的时机还比较早,这时候Spring容器的有些配置有可能还没完成呢,这时就去getBean就有可能产生一些问题(其实一般情况也遇不到)。
与该接口对应的,Spring还有一个接口是SmartInitializingSingleton
,它其实也是作为一个Bean的初始化回调方法,它会在Spring当中的所有的Bean都完成实例化和初始化之后再去进行回调。
它的具体回调时机如下,第一部分是实例化所有的单实例Bean,第二部分则是回调所有的SmartInitializingSingleton
,它也完全可以用来初始化Bean,是没有任何问题的。
RestTemplateCustomizer
是什么?
首先,我们知道RestTemplate,是一个HttpClient的客户端,可以用于完成HTTP请求的发送和处理,并且它还是一个InterceptingHttpAccessor
,支持去添加请求的拦截器,对请求去进行处理。(注意RestTemplate
是来自于Spring的web
包)
SpringCloud当中,针对于RestTemplate去提供了RestTemplateCustomizer
,支持去对RestTemplate去进行自定义吗,我们可以实现这个接口,在customize
方法当中去添加,我们自定义的逻辑。
Spring当中,针对于XXXCustomizer
的实现,其实非常非常多,我们从名字也可以知道,是对XXX的一个自定义化器,支持去对XXX去进行自定义操作。
在了解了上面的知识之后,我们继续来看。
image.png我们已经知道了Spring容器启动过程当中,它会回调所有的RestTemplateCustomizer
,那么这些组件从哪来?我们往下翻,就会发现,它给容器当中配置了LoadBalancerInterceptor
,
我们来看LoadBalancerInterceptor
的实现:
我们可以看到,它从request当中获取到uri,并且以uri作为serviceName,把它交给了LoadBalancerClient
去进行执行。然后,不就到了刚刚我们说的SpringCloud抽象了LoadBalancerClient
了吗?也就是说SpringCloud使用自定义的LoadBalancerInterceptor
去拦截了RestTemplate
,将请求转交给了LoadBalancerClient
去进行处理。
如果我们引入了Ribbon的jar包,那么这个LoadBalancerClient
就会是Ribbon的实现;如果我们引入了SpringCloudLoadBalancer
的jar包,那么这个LoadBalancerClient
就会是SpringCloudLoadBalancer
,只要导入一方的相关的实现配置,它就会被SpringBoot所整合,这也就是我们所说的SpringBoot的自动配置。
整体的流程大概如下:
image.png1.3 自定义RPC远程调用的协议
类似地,如果我们想要自定义相关的负载均衡的功能(并且不想使用SpringCloud提供的RestTemplate或者是SpringCloudOpenFeign)。比如说,我们想不使用HTTP请求去完成RPC的远程调用(RestTemplate/SpringCloudOpenFeign/WebClient都是基于HTTP的),我们只需要从Spring容器当中去注入LoadBalancerClient,并且自定义LoadBalancerRequest
回调函数去指定处理请求的逻辑即可实现负载均衡。
至于在回调函数当中,我们想要使用何种方式去发送网络请求获取数据,那就是我们完全可以去自定义的部分了。
2. @LoadBalanced
与@Qualifier
之间的关系?
在上面的代码当中,我们会见到下面这样的代码,在@Autowired
上打了@LoadBalanced
注解。
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
这是什么操作?字段上标注@LoadBalanced
?干啥的?我这时候有以下两个想法:
- 1.SpringCloud单独使用BeanPostProcessor单独处理了这个字段?
- 2.难道Spring还支持
Autowired
时,还支持打上个注解,去进行注解的匹配吗?
按照我对Spring的源码的理解,使用第一种的概率比较大,于是我找遍了整个SpringCloud的Common包下的源码,没有发现。但是第二种,完全没听说过这种用法,应该是不太可能,我看过的源码里似乎也没有这个插曲。
但是这时候,我想起来了Qualifier注解,这个注解引起了我的单独注意。
2.1 AutowireCandidateResolver
是什么
Spring在处理自动注入(不只是针对@Autowired
注解,也针对@Resource
注解,以及@Inject
等注解)时,会使用DefaultListableBeanFactory.resolveDepenency
方法去完成依赖的注入。
在处理自动注入时,会用到AutowireCandidateResolver
,去判断容器当中的所有的Bean,挨个去匹配是否可以作为当前依赖的注入对象。从名字,我们也可以知道些什么,AutowireCandidateResolver
,自动装配的候选的解析器。至于如何判断?具体逻辑就在isAutowireCandidate
方法当中了。
我们找到它的子类QualifierAnnotationAutowireCandidateResolver
,这个类是我们需要重点研究的类。
这里它会从字段上、方法参数上、方法上、构造器上,去匹配Qualifier注解,需要注意的是,这里(以及后续)提到的Qualifier注解,并不只是Spring家的Qualifier,也包含javax.inject
当中的Qualifier,甚至是自定义的Qualifier。
有一个点是我们要关注的,向下传递的参数descriptor.getAnnotations()
是要进行注入的元素的注解列表。
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
比如上面的代码当中,获取到的注解,就是Autowired
和LoadBalanced
这两个注解。
接着,就是要注入的元素上的注解,和Qualfier去进行匹配,如果匹配了(也就是说要注入的元素上有@Qualifier注解的话),那么走进去checkQualifier
方法,检查Qualifier注解的情况。这个方法很长,我们分成两段去进行介绍:
它会从各个地方去检查Qualifier注解,我们主要关注下面三种情况:
-
bd.getQualifier
从BeanDefinition当中去获取,这个主要是XML当中配置的Qualfier属性,就会解析封装到里面。
-
-
getQualifiedElementAnnotation
,这种情况主要是扫描的时候,是通过注解的方式扫描Class(比如@ComponentScan扫描到了某个类)进来的那种情况。
-
- 如果还找不到?
getFactoryMethodAnnotation
,从FactoryMethod(工厂方法,也就是@Bean方法)上找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的源码就发现了,根本不可能,你想放进去?还没放进去就给你抛异常了。
到这里,我们就彻底明白了。下面的代码当中@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),去完成本次请求的处理工作。