SpringCloud Ribbon 客户端负载均衡原理
1. Ribbon实现负载均衡
相比于nginx, nginx是在服务端进行赋值均衡,由nginx服务器通过负载均衡算法获取到某个服务提供者的节点后,再将请求转发到该节点,而ribbon是在客户端进行负载均衡,也就是服务的调用方,获取到服务的提供者的地址列表,通过某种负载均衡算法,选择其中一个服务提供者节点进行服务调用, 下面通过两个项目来学习Ribbon 负载均衡的实现以及原理;
- Ribbon实现负载均衡的两种方式
通过LoadBalancerClient 实现负载均衡
通过@LoadBalanced注解实现负载均衡
- 创建两个项目order-service user-service,一个作为服务提供者,一个作为服务消费者
服务提供者order-service:
配置文件application.properties文件中配置端口号: server.port=8080 ,启动服务,然后修改端口号为8082,再启动一个服务,这样就得到了一个服务提供者的集群
服务消费者user-service:
package com.gupaoedu.springcloud.example.springclouduserservice;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class UserController {
@Autowired
RestTemplate restTemplate;
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){
return restTemplateBuilder.build();
}
@Autowired
LoadBalancerClient loadBalancerClient;
@GetMapping("/user/{id}")
public String findById(@PathVariable("id")int id){
//TODO
// 调用订单的服务获得订单信息
// HttpClient RestTemplate OkHttp JDK HttpUrlConnection
ServiceInstance serviceInstance=loadBalancerClient.choose("spring-cloud-order-service");
String url=String.format("http://%s:%s",serviceInstance.getHost(),serviceInstance.getPort()+"/orders");
return restTemplate.getForObject(url,String.class);
}
}
配置文件: 指定服务消费者端口,以及服务提供者的地址列表,ribbon会读取spring-cloud-order-service.ribbon.listOfServers 这个配置项获取服务提供者的集群地址列表
server.port=8088
# 配置指定服务的提供者的地址列表(服务集群列表)
spring-cloud-order-service.ribbon.listOfServers=\
localhost:8080,localhost:8082
然后启动项目,不断访问http://localhost:8088/user/1 , 会看到order-service项目的两个控制台轮流打印端口号(ribbon默认的负载均衡算法是轮询)
上面是通过LoadBalancerClient 实现负载均衡,LoadBalancerClient 可以使用@LoadBalanced注解来代替,只需要在RestTemplate Bean的定义上面加上 @LoadBalanced注解即可:
package com.gupaoedu.springcloud.example.springclouduserservice;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class UserController {
@Autowired
RestTemplate restTemplate;
@Bean
@LoadBalanced
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){
return restTemplateBuilder.build();
}
// @Autowired
// LoadBalancerClient loadBalancerClient;
@GetMapping("/user/{id}")
public String findById(@PathVariable("id")int id){
return restTemplate.getForObject("http://spring-cloud-order-service/orders",String.class);
}
}
有个细节需要注意到,就是上面的请求地址是改成了http://spring-cloud-order-service/orders ,即 http://服务名/接口名的形式。
那么这个注解是如何实现负载均衡的,我们暂时不得而知,但是可以肯定的是,它肯定实现了以下功能:
- 通过服务名来解析服务地址列表
- 在通过restTemplate 进行http请求调用前,修改请求url
- 通过restTemplate 进行http请求调用,返回结果
2. @LoadBalanced注解原理
查看LoadBalanced 注解,可以看到它本质上就是一个@Qualifier注解,这个注解的作用就是给Bean打上一个标记(详见https://blog.csdn.net/weixin_41300437/article/details/109192723)
@Target({
ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
并不能看出什么端倪,从springboot自动装配原理,我们猜想应该有一个负载均衡的配置类,在IDEA中全局搜索LoadBalance, 找到LoadBalancerAutoConfiguration 这个自动配置类
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({
RestTemplate.class})
@ConditionalOnBean({
LoadBalancerClient.class})
@EnableConfigurationProperties({
LoadBalancerRetryProperties.class})
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Autowired(equired = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
public LoadBalancerAutoConfiguration() {
}
可以看到restTemplates 上面加了 @Autowired注解和一个@LoadBalanced注解,也就等同于加了一个@Qualifier注解,那么我们就能得出一个结论,只要是加了@LoadBalanced 的restTemplate bean, 那么都会被添加到restTemplates 这个list中。不加@LoadBalanced注解的,就不能被添加进去。LoadBalancerAutoConfiguration 这个自动装配类,会在spring容器启动时,完成ribbon所需要的一些bean的自动装配,完整代码如下:
package org.springframework.cloud.client.loadbalancer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.RestTemplate;
/**
* Auto-configuration for Ribbon (client-side load balancing).
*
* @author Spencer Gibb
* @author Dave Syer
* @author Will Tran
* @author Gang Li
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
// 将所有加了@LoadBalanced注解的restTemplate bean放到restTemplates这个list里面
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
// 3. 遍历restTemplates,用RestTemplateCustomizer对每一个加了@LoadBalanced注解的restTemplate进行包装
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
});
}
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(
LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
// 1.定义LoadBalancerInterceptor 拦截器Bean
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
// 2.对restTemplate进行包装,依赖注入loadBalancerInterceptor 这个bean,对加了@LoadBalanced注解的Restemplate实例添加LoadBalancerInterceptor拦截器
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors()); // 获取restTemplate默认的拦截器
list.add(loadBalancerInterceptor); // 对restTemplate添加新的拦截器
restTemplate.setInterceptors(list); // 将新的拦截器链设置到restTemplate里
};
}
}
/**
* Auto configuration for retry mechanism.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RetryTemplate.class)
public static class RetryAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public LoadBalancedRetryFactory loadBalancedRetryFactory() {
return new LoadBalancedRetryFactory() {
};
}
}
/**
* Auto configuration for retry intercepting mechanism.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RetryTemplate.class)
public static class RetryInterceptorAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public RetryLoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRetryProperties properties,
LoadBalancerRequestFactory requestFactory,
LoadBalancedRetryFactory loadBalancedRetryFactory) {
return new RetryLoadBalancerInterceptor(loadBalancerClient, properties,
requestFactory, loadBalancedRetryFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
}
}