添加了@LoadBalanced注解,即可实现负载均衡功能,这是什么原理呢?
1、负载均衡原理
SpringCloud底层其实是利用了一个名为Ribbon的组件,来实现负载均衡功能的。
思考:发出的请求明明是http://userservice/user/2,怎么变成了http://localhost:8081/user/2的呢?
2、源码分析
为什么我们只输入了service名称就可以访问了呢?之前还要获取ip和端口。
显然是因为有什么组件帮我们根据service名称,获取到了服务实例的ip和端口。
先说结论:它就是LoadBalancerInterceptor
。
这个类会在对RestTemplate的请求进行拦截,然后从Eureka根据服务id获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务id。
下面进行源码分析。
2.1、@LoadBalanced
按住CTRL键,点进@LoadBalanced源码,选择加载完整,可以看到如下:
看起来非常的平平无奇,但是请留意上面的英文说明:
Annotation to mark a RestTemplate or WebClient bean to be configured to use a LoadBalancerClient.
简单来说,就是这个@LoadBalanced注解是用来标记的,标记要使用的是LoadBalancerClient。
所以真正起作用的其实是LoadBalancerClient这个Java类。
下面找到LoadBalancerClient这个类。
2.2、LoadBalancerClient
可以看到,这个类也是一个接口。
看一下类里面有什么方法:
只有这三个方法,三个都打断点尝试一下,看看拦截的是哪一个。
经过尝试,拦截的是:
<T> T execute(String serviceId, ServiceInstance serviceInstance,LoadBalancerRequest<T> request) throws IOException;
并且,拦截到实现类是RibbonLoadBalancerClient
:
但是这时候,我再往下调试断点,发现有点没有头绪了。
后面的内容并没有像前文这样显而易见的内容,都是一些判断之类的业务逻辑。
此时想起Ribbon组件,应该就是一整套东西。
那么查一下是否还有别的类?
让我们接着往下看
2.3、RibbonAutoConfiguration
查看ribbon包下其他的类,发现以ribbon开头的Java文件如下:
其中有一个很引人注目的类,即是配置类RibbonAutoConfiguration
!
点进去看一下:
英文提示信息是:Auto configuration for Ribbon (client side load balancing).
顾名思义,这个类果然是自动配置ribbon相关内容的。
看一下这个类实现了什么方法:
此时看见了LoadBalancerClient()
方法,那么这个LoadBalancerClient()
和LoadBalancerClient.java
会不会有什么关系呢?点过去看一下:
原来这个方法是返回了一个新的RibbonLoadBalancerClient
对象,并且限制了注入LoadBalancerClient
对象只允许注册一个实例。
其实,返回了一个新的RibbonLoadBalancerClient
对象,本质上也是返回了一个LoadBalancerClient
对象,因为RibbonLoadBalancerClient
是实现LoadBalancerClient
的:
2.4、LoadBalancerAutoConfiguration
进入LoadBalancerClient
,查看哪些类实现了这个接口:
又出现一个自动配置类,点进去查看:
英文提示:Auto-configuration for blocking client-side load balancing.
顾名思义,自动配置阻塞客户端负载平衡。
看来,关键点找到了!
看一下这个类实现了什么方法:
可以看到这里有一个LoadBalancerInterceptorConfig
拦截器配置。
那么猜想里面应该是配置了拦截器。
这符合之前分析的流程,Ribbon组件接收到请求,然后去找Eureka拉去实例列表,那这个过程的实现,不就是需要一个拦截器么?请接着往下看:
原来这是一个静态内部类。这个内部类的内容如下:
@Configuration(proxyBeanMethods = false) @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate") static class LoadBalancerInterceptorConfig { @Bean public LoadBalancerInterceptor loadBalancerInterceptor( LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) { return new LoadBalancerInterceptor(loadBalancerClient, requestFactory); } @Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return restTemplate -> { List<ClientHttpRequestInterceptor> list = new ArrayList<>( restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); }; } }
这部分内容,我们让通义灵码解释一下:
由此,重点就在LoadBalancerIntercepor了。
2.5、LoadBalancerIntercepor⭐
进入LoadBalancerIntercepor
:
LoadBalancerIntercepor
实现了ClientHttpRequestInterceptor
(翻译过来是:客户端Http请求拦截器)。
到这里,基本上就没错了,接下来就是打断点接着拦截!
实现的方法如下:
对intercept方法打断点:
可以看到这里的intercept方法,拦截了用户的HttpRequest请求,然后做了几件事:
①request.getURI()
:获取请求uri,本例中就是 http://userservice:8081/user/2
②originalUri.getHost()
:获取uri路径的主机名,其实就是服务id,userservice
③this.loadBalancer.execute()
:处理服务id,和用户请求。
这里的this.loadBalancer
是LoadBalancerClient
类型:
继续跟入末尾的execute方法。
2.6、再回LoadBalancerClient
可以注意到文章目录其实有两个LoadBalancerClient
。
第一次是断点定位到execute方法之后没有头绪,
第二次则是带着答案的合理猜测来的。
继续跟入this.loadBalancer.execute
方法:
此时来到了RibbonLoadBalancerClient.java
。下面进入这个类。
2.7、RibbonLoadBalancerClient
继续进入RibbonLoadBalancerClient
的execute方法:
(进入发现其实还是RibbonLoadBalancerClient
类里面的方法,只不过是别的execute方法):
2.7.1、DynamicServerListLoadBalancer
注意上图中的DynamicServerListLoadBalancer
,在这里已经拉取到实例了。
上图是DynamicServerListLoadBalancer
类的说明,意思翻译过来就是:
具有使用动态源获取候选服务器列表的能力的LoadBalancer
。例如,服务器列表可能会在运行时更改。它还包含一些工具,其中服务器列表可以通过筛选条件传递,以过滤掉不符合所需条件的服务器。
2.7.2、getServer
注意到这里有一个getServer方法,点进去一探究竟:
再进chooseServer方法:
来到了ZoneAwareLoadBalancer.java
的chooseServer方法
再进super.chooseServer(key),来到了ZoneAwareLoadBalancer
的父类BaseLoadBalancer
。
2.8、BaseLoadBalancer
进入BaseLoadBalancer
的chooseServer方法:
断点来到了rule.choose(key)。
rule则是规则的意思,查看一下rule是什么:
根据命名规范,I是指interface接口的意思,所以IRule是一个接口:
英文提示信息:
Interface that defines a "Rule" for a LoadBalancer.
A Rule can be thought of as a Strategy for loadbalacing.
Well known loadbalancing strategies include Round Robin, Response Time based etc.
信息主要内容:规则被认为是一种负载平衡策略。众所周知的负载平衡策略包括轮询、基于响应时间等。
所以到这一步,就已经很明显了。
SpringCloud底层利用Ribbon实现负载均衡,而Ribbon则通过IRule实现负载均衡。
也就是说,Ribbon实现负载均衡的组件,一定是IRule的实现类。
在IDEA中CTRL+H查看一下IRule的实现类:
RoundRobinRule.java
就是实现了轮询规则。
2.9、RoundRobinRule
进入RoundRobinRule
,查看实现方法:
打上断点,看看Ribbon组件有没有使用RoundRobinRule
,有则说明Ribbon的原理就是轮询策略实现负载均衡。
我们的验证是没有错的,Ribbon组件实现负载均衡就是轮询规则。
2.10、总结
SpringCloudRibbon的底层采用了一个拦截器,拦截了RestTemplate发出的请求,对地址做了修改。用一幅图来总结一下:
基本流程如下:
- 拦截
RestTemplate
请求http://userservice/user/2 RibbonLoadBalancerClient
会从请求url中获取服务名称,也就是userserviceDynamicServerListLoadBalancer
根据userservice到eureka拉取服务列表eureka
返回列表,localhost:8081、localhost:8082IRule
利用内置负载均衡的轮询规则,从列表中选择一个,例如localhost:8081RibbonLoadBalancerClient
修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/2,发起真实请求
3、负载均衡策略
3.1、负载均衡策略
负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:
不同规则的含义如下:
内置负载均衡规则类 |
规则描述 |
RoundRobinRule |
简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule |
对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。 |
WeightedResponseTimeRule |
为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule |
以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule |
忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule |
随机选择一个可用的服务器。 |
RetryRule |
重试机制的选择逻辑 |
默认的实现就是ZoneAvoidanceRule,是一种轮询方案
3.2、自定义负载均衡策略
通过定义IRule实现可以修改负载均衡规则,有两种方式:
- 代码方式:在order-service中的OrderApplication类中,定义一个新的IRule:
@Bean public IRule randomRule(){ return new RandomRule(); }
- 配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则:
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务 ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
注意,一般用默认的负载均衡规则,不做修改。
3.3、饥饿加载⭐
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
ribbon: eager-load: enabled: true clients: userservice