系列文章:
Spring Cloud LoadBalancer之负载均衡简介
Spring Cloud NamedContextFactory 原理分析
Ribbon 的替代品 Spring Cloud Loadbalancer 使用与原理分析


本文的 spring-cloud-loadbalancer 版本为:3.1.1

由于 Ribbon 已经停更,Spring Cloud 在 Hoxton.M2 Released 版本将 Ribbon 剔除,并使用 Spring Cloud Loadbalancer 作为其替代品;

image-20220421162540630

二者区别:

组件组件提供的负载策略支持负载的客户端
Ribbon轮询、随机、重试策略、权重优先策略、
BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
AvailabiliyFilteringRule:先过滤掉故障实例,再选择并发较小的实例
ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server的可用性选择服务器
feign或openfeign、RestTemplate等 Web 调用工具
Spring Cloud Loadbalancer轮询、随机Ribbon 所支持的、WebClient

LoadBalancer 的优势主要是,支持响应式编程的方式异步访问客户端,依赖 Spring Web Flux 实现客户端负载均衡调用。

一、 使用方法

使用方法很简单,只需要给需要负载均衡的 bean 添加 @LoadBalanced 注解即可:

// 以 RestTemplate 为例,在 bean 上面添加 @LoadBalanced 即可

@LoadBalanced
@Bean
public RestTemplate restTemplate() {
    return new RestTemplate();
}

如果使用了 feign 默认会执行负载均衡策略,feign 会在自动配置的时候加入负载均衡的功能,从而实现在调用时负载均衡。

二、 @LoadBalancerClients 与 @LoadBalancerClient

在某些情况下,你定义的负载均衡策略并不想作用于全局,那么可以使用这两个注解对特定的服务使用负载均衡策略。

@LoadBalancerClient 的作用是给特定的服务添加特定的负载均衡策;@LoadBalancerClients 的作用是将多个 @LoadBalancerClient 组合在一起使用,因为 @LoadBalancerClient 不能多个写在同一个地方

@LoadBalancerClients(
        value = {
                @LoadBalancerClient(value = "loadbalancer-provider", configuration = CustomRandomConfig.class),
                @LoadBalancerClient(value = "loadbalancer-log", configuration = CustomRoundRobinConfig.class)
        }, defaultConfiguration = LoadBalancerClientConfiguration.class
)
public class RestTemplateConfig {}

上面代码的意思是,给 loadbalancer-provider 服务使用的负载均衡策略是 RandomLoadBalancer 随机负载均衡,给 loadbalancer-log 使用的是 RoundRobinLoadBalancer 轮训策略,其他没有标识的则使用默认的配置 LoadBalancerClientConfiguration(轮询);
CustomRandomConfig是自定义的配置,对应的是随机负载均衡的配置,CustomRoundRobinConfig也是自定义的配置,为轮询负载均衡。

三、 自定义负载均衡

如果需要自定义策略,只需要实现 ReactiveLoadBalancer 接口并编写 choose(Request) 负载策略,可以参考已有的实现去做,例如默认的轮询 RoundRobinLoadBalancer 是怎么编写的

image-20220422095615395

由于负载均衡在容器(父容器与子容器)中只有一个,因此如果你注册为 Bean 则会覆盖掉原先默认的轮训策略,因为默认的负载均衡加了 @ConditionalOnMissingBean 注解。

/**
 * 自定义负载均衡 bean 配置
 *
 * @author zxb
 * @date 2022-04-12 17:13
 */
@Configuration
// 表示全局使用同一个配置
@LoadBalancerClients(defaultConfiguration = {SpringBeanConfiguration.class})
public class SpringBeanConfiguration {

    @Bean
    ReactorLoadBalancer<ServiceInstance> nacosTransferRuleLoadBalancer(Environment environment,
                                                            LoadBalancerClientFactory loadBalancerClientFactory,
                                                            NacosDiscoveryProperties nacosDiscoveryProperties) {
        String name = environment
            .getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        ObjectProvider<ServiceInstanceListSupplier> lazyProvider =
                loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class);
        return new NacosTransferRule(lazyProvider, name, nacosDiscoveryProperties);
    }
}
四、 重试机制

Spring Cloud 提供了重试机制,直接在配置文件进行配置即可,但如果加了重试,请务必保证有更新操作接口的幂等性

spring:
  application:
    name: loadbalancer-consumer
  cloud:
    loadbalancer:
      # 以下配置为 LoadBalancerProperties 配置类
      clients:
        # default 表示全局配置,如要针对某个服务,则填写对应的服务名即可
        default:
          retry:
            enbled: true
            # 是否所有的请求都重试,false 表示只有 GET 请求才重试
            retryOnAllOperations: true
            # 同一个实例的重试次数,不包括第一次调用;比如填了 3,实际会调用 4 次
            maxRetriesOnSameServiceInstance: 3
            # 其他实例的重试次数,多节点的情况下使用
            maxRetriesOnNextServiceInstance: 0

以上的配置对 OpenFeign 不生效,如要配置 OpenFeign 的重试,则需要使用编码的方式实现

@Bean
public Retryer feignRetryer() {
    Retryer retryer = new Retryer.Default(100, 1000, 5);
    return retryer;
}
五、 原理分析

5.1 加了 @LoadBalanced 注解为何会使 RestTemplate 负载均衡生效?

LoadBalancerAutoConfiguration 自动配置类中会给标有 @LoadBalanced 注解的 RestTemplate 添加一个负载均衡拦截器,这样就能通过 LoadBalancerInterceptor 去添加负载均衡策略:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerClientsProperties.class)
public class LoadBalancerAutoConfiguration {

    /**
     * 会注入标有 @LoadBalanced 注解的 RestTemplate
     */
	@LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();

	@Bean
	public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
			final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
		return () -> restTemplateCustomizers.ifAvailable(customizers -> {
			for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
				for (RestTemplateCustomizer customizer : customizers) {
                    // 给 restTemplate 添加 LoadBalancerInterceptor 负载均衡拦截器
					customizer.customize(restTemplate);
				}
			}
		});
	}
    
    // 静态内部类,负载均衡拦截器配置
    @Configuration(proxyBeanMethods = false)
	@Conditional(RetryMissingOrDisabledCondition.class)
	static class LoadBalancerInterceptorConfig {
		@Bean
		public LoadBalancerInterceptor loadBalancerInterceptor(LoadBalancerClient loadBalancerClient,
				LoadBalancerRequestFactory requestFactory) {
            // 负载均衡拦截器
			return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
		}
		@Bean
		@ConditionalOnMissingBean
		public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
            // 给 restTemplate 添加负载均衡拦截器
			return restTemplate -> {
				List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
				list.add(loadBalancerInterceptor);
				restTemplate.setInterceptors(list);
			};
		}
	}
    
	// 略...
}

5.2 加了 @LoadBalancerClient 如何实现不同服务隔离?

@LoadBalancerClient上面会有一个 @Import(LoadBalancerClientConfigurationRegistrar.class)注解,@Import 的作用简单来说会去调用LoadBalancerClientConfigurationRegistrar#registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry) 方法,可以在方法中自定义需要添加到容器的 bean。

image-20220421155343899

LoadBalancerClientConfigurationRegistrar的作用就是注册 LoadBalancerClientSpecification 实例

private static void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
                                                Object configuration) {
    BeanDefinitionBuilder builder = BeanDefinitionBuilder
        .genericBeanDefinition(LoadBalancerClientSpecification.class);
    // @LoadBalancerClient 注解的 value 值
    builder.addConstructorArgValue(name);
    // @LoadBalancerClient 注解的 configuration 值
    builder.addConstructorArgValue(configuration);
    registry.registerBeanDefinition(name + ".LoadBalancerClientSpecification", builder.getBeanDefinition());
}

LoadBalancerClientSpecification NamedContextFactory.Specification 的一个实现类,结合第二部分内容可以知道他是用来区分不同实例使用不同配置的子容器,容器名即为 @LoadBalancerClient 注解的 value 值,容器的配置即为 @LoadBalancerClient 注解的 configuration 值。


既然在 Spring Cloud Loadbalancer 中存在 NamedContextFactory.Specification 的实现类LoadBalancerClientSpecification ,那么肯定也有对应的 NamedContextFactory 的实现类用来管理这些子容器跟配置项,这个实现类就是 LoadBalancerClientFactory,有了这两个类 loadbalancer 就能够实现服务之间的配置隔离了,具体是怎么用呢?下面从执行流程进行分析。

如不了解 NamedContextFactoryNamedContextFactory.Specification 的作用,建议先阅读 Spring Cloud NamedContextFactory 原理分析,这样才能清楚服务隔离的实现。

六、 执行流程分析

Spring Cloud Loadbalancer 的工作流程主要分为 3 步

  • 从注册中心获取服务列表
  • 根据负载策略找到目标服务,重新构造请求地址
  • 使用 Web 请求工具对目标服务进行远程调用

6.1 如何获取服务列表?

LoadBalancerClientConfiguration 配置类中定义了很多获取 ServiceInstanceListSupplier Bean的方法,ServiceInstanceListSupplier 的作用,见名知意它就是服务实例集合的提供者。

image-20220421180326463

获取ServiceInstanceListSupplier 的提供者有很多,阻塞式请求类型、阻塞式重试类型、反应式类型、反应式重试类型等,阻塞的对应传统的 Web 请求,而反应式的就对应 Web Fulx。这些提供 ServiceInstanceListSupplier 实例的配置类并不是全部生效的,会根据系统的环境生效,例如你要支持重试,那就必须在类路径下有 Spring-Retry 的依赖,默认取的是阻塞式的。

@Bean
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnMissingBean
@Conditional(DefaultConfigurationCondition.class)
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
    ConfigurableApplicationContext context) {
    // 从服务发现与缓存中获取服务实例
    return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withCaching().build(context);
}

只要注册中心有实现 DiscoveryClient接口,并且将其注册到 Bean 容器中,负载均衡会拿到对应实现类的服务实例;withCaching() 表示会将服务列表进行缓存,不用一直请求注册中心拿到服务实例列表(nacos 也会作相应的缓存)。

扩展:根据之前讲的 NamedContextFactory 特性,在子容器中是可以获取到父容器上下文的 Bean 实例,因此所有子容器都能够拿到注册中心实例

6.2 请求过来时,在什么时机使用负载均衡策略?

对于阻塞式 RestTemplate、异步非阻塞式请求 WebClient、OpenFeign,使用的时机不同,但最终都是去调用 LoadBalancerClient 的实现类 BlockingLoadBalancerClient(以loadbalancer 为例,Ribbon 不同),在实现类的 execute 方法中会执行负载均衡策略:

public class BlockingLoadBalancerClient implements LoadBalancerClient {

	private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory;
    
    @Override
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        // 略...
        // 在 choose 方法获取服务实例
		ServiceInstance serviceInstance = choose(serviceId, lbRequest);
		// 略...
		return execute(serviceId, serviceInstance, lbRequest);
	}
    
    @Override
	public <T> ServiceInstance choose(String serviceId, Request<T> request) {
        // loadBalancerClientFactory 是 NamedContextFactory 的实现类,根据服务名获取子容器 ReactorServiceInstanceLoadBalancer 实例
		ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId);
		// 调用负载均衡策略获取目标服务实例
        Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
        // 略...
		return loadBalancerResponse.getServer();
	}
}

对 RestTemplate 请求调用时经过的类流程图如下:

img

总结:

​ 本文介绍了负载均衡的基本概念以及与 Ribbon 的对比有何优势以及Spring Cloud LoadBalancer 的使用;如何去自定义负载均衡策略并让其生;@LoadBalanced 通过添加 RestTemplate 拦截器的方式,让 RestTemplate 实现负载均衡;@LoadBalancerClient 注解如何实现不同服务负载均衡配置隔离;请求的流程中在何时会使用负载均衡策略。

参考链接:Spring cloud之LoadBalancer篇_负载均衡_邱学喆_InfoQ写作社区

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐