自定义你的spring-cloud-loadbalancer负载均衡策略

1,962 阅读9分钟

在文章 openFeign让http调用更简单 中介绍了基于openfeign的http客户端调用,但是其实还有一个地方没有介绍,就是openfeign从注册中心获取了服务的实例列表后,如何选择哪一个具体的实例进行调用的,也就是如何实现客户端负载均衡的。spring boot3默认使用的客户端负载均衡组件为spring cloud loadbalancer,这篇文章就来介绍它的应用。

1 spring cloud loadbalancer默认是如何进行负载均衡的

使用spring cloud loadbalancer进行客户端负载均衡时,最终的http请求调用委托给了LoadBalancerClient。查看配置类BlockingLoadBalancerClientAutoConfiguration可知,LoadBalancerClient在非反应式web服务环境下的实现类是BlockingLoadBalancerClient

@LoadBalancerClients
@AutoConfigureAfter({LoadBalancerAutoConfiguration.class})
@AutoConfigureBefore({org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.class})
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnProperty(
    value = {"spring.cloud.loadbalancer.enabled"},
    havingValue = "true",
    matchIfMissing = true
)
public class BlockingLoadBalancerClientAutoConfiguration {

    @Bean
    @ConditionalOnBean({LoadBalancerClientFactory.class})
    @ConditionalOnMissingBean
    public LoadBalancerClient blockingLoadBalancerClient(LoadBalancerClientFactory loadBalancerClientFactory) {
        return new BlockingLoadBalancerClient(loadBalancerClientFactory);
    }

}

BlockingLoadBalancerClient类的choose()方法用来选择一个服务实例(即实现负载均衡),代码如下:

public class BlockingLoadBalancerClient implements LoadBalancerClient {

	@Override
	public ServiceInstance choose(String serviceId) {
		return choose(serviceId, REQUEST);
	}

	@Override
	public <T> ServiceInstance choose(String serviceId, Request<T> request) {
	    // 返回一个负载均衡器,从后面的代码分析可知,这个地方默认返回的是RoundRobinLoadBalancer类的实例
		ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId);
		if (loadBalancer == null) {
			return null;
		}
		
		// 负载均衡器执行具体的负载均衡功能,选择一个具体的服务实例
		Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
		if (loadBalancerResponse == null) {
			return null;
		}
		return loadBalancerResponse.getServer();
	}

}

从配置类LoadBalancerAutoConfiguration可以看到loadBalancerClientFactory类实例是如何创建的:

@Configuration(proxyBeanMethods = false)
@LoadBalancerClients
@EnableConfigurationProperties({ LoadBalancerClientsProperties.class, LoadBalancerEagerLoadProperties.class })
@AutoConfigureBefore({ ReactorLoadBalancerClientAutoConfiguration.class,
		LoadBalancerBeanPostProcessorAutoConfiguration.class })
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.enabled", havingValue = "true", matchIfMissing = true)
public class LoadBalancerAutoConfiguration {

	@ConditionalOnMissingBean
	@Bean
	public LoadBalancerClientFactory loadBalancerClientFactory(LoadBalancerClientsProperties properties,
			ObjectProvider<List<LoadBalancerClientSpecification>> configurations) {
		LoadBalancerClientFactory clientFactory = new LoadBalancerClientFactory(properties);
		clientFactory.setConfigurations(configurations.getIfAvailable(Collections::emptyList));
		return clientFactory;
	}
}

继续查看LoadBalancerClientFactory类的getInstance()方法:

public class LoadBalancerClientFactory extends NamedContextFactory<LoadBalancerClientSpecification>
		implements ReactiveLoadBalancer.Factory<ServiceInstance> {
		
	@Override
	public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
		return getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class);
	}
	
	public <T> T getInstance(String name, Class<T> type) {
        GenericApplicationContext context = this.getContext(name);

        try {
            return context.getBean(type);
        } catch (NoSuchBeanDefinitionException var5) {
            return null;
        }
    }

}

从上面代码可知,LoadBalancerClientFactory.getInstance()会从子容器中获取ReactorServiceInstanceLoadBalancer类实例作为具体的负载均衡器。这里注意,因为LoadBalancerClientFactory继承了NamedContextFactory,所以ReactorServiceInstanceLoadBalancer bean不是从spring ApplicationContext根容器中获取,而是从服务的子容器(每一个服务有一个自己独立的子容器)中获取。

继续查看配置类LoadBalancerClientConfiguration可知,默认的负载均衡器是RoundRobinLoadBalancer

@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
public class LoadBalancerClientConfiguration {

	@Bean
	@ConditionalOnMissingBean
	public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
			LoadBalancerClientFactory loadBalancerClientFactory) {
		
		// 这里的environment是服务的子容器的environment,不是全局的environment,获取的名字就是服务名
		String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
		return new RoundRobinLoadBalancer(
				loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
	}

}

继续查看RoundRobinLoadBalancerchoose()方法实现:

public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
	public Mono<Response<ServiceInstance>> choose(Request request) {
	
	    // ServiceInstanceListSupplier提供备选的服务实例列表
		ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
				.getIfAvailable(NoopServiceInstanceListSupplier::new);
		
		return supplier.get(request).next()
				.map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
	}

	private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
			List<ServiceInstance> serviceInstances) {
		
		// 从备选的服务列表中选择一个具体的服务实例
		Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances);
		if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
			((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
		}
		return serviceInstanceResponse;
	}
	
	private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
		if (instances.isEmpty()) {
			if (log.isWarnEnabled()) {
				log.warn("No servers available for service: " + serviceId);
			}
			return new EmptyResponse();
		}

		// Do not move position when there is only 1 instance, especially some suppliers
		// have already filtered instances
		if (instances.size() == 1) {
			return new DefaultResponse(instances.get(0));
		}

		// Ignore the sign bit, this allows pos to loop sequentially from 0 to
		// Integer.MAX_VALUE
		int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;

		ServiceInstance instance = instances.get(pos % instances.size());

		return new DefaultResponse(instance);
	}

}

RoundRobinLoadBalancer的实现可知,它先通过ServiceInstanceListSupplier获得一个备选的服务实例列表,然后从服务列表中通过一定的策略(对于RoundRobinLoadBalancer来说,就是轮询策略)选择一个具体的服务实例。

还是通过查看配置类LoadBalancerClientConfiguration,可以知道ServiceInstanceListSupplier的默认实现类:

@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
public class LoadBalancerClientConfiguration {

	static class DefaultConfigurationCondition implements Condition {

		@Override
		public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
			return LoadBalancerEnvironmentPropertyUtils.equalToOrMissingForClientOrDefault(context.getEnvironment(),
					"configurations", "default");
		}

	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnBlockingDiscoveryEnabled
	@Order(REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER + 1)
	public static class BlockingSupportConfiguration {

		@Bean
		@ConditionalOnBean(DiscoveryClient.class)
		@ConditionalOnMissingBean
		@Conditional(DefaultConfigurationCondition.class)
		public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
				ConfigurableApplicationContext context) {
			return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withCaching().build(context);
		}
		
	}

}

1.1 总结

spring cloud loadbalancer默认的负载均衡器是RoundRobinLoadBalancer,负载均衡策略是轮询,它先通过ServiceInstanceListSupplier获取一个备选的服务实例列表,然后采用轮询策略从备选服务列表中选择一个具体的实例。而ServiceInstanceListSupplier默认是通过DiscoveryClient从注册中心获取服务实例列表。从默认实现我们也可以知道,如果采用与RoundRobinLoadBalancer相同的负载均衡实现架构,我们有两个地方影响最终选择的具体服务实例:1)ServiceInstanceListSupplier获取备选服务实例时可以根据一定的条件获取备选列表;2)从备选列表采用一定的策略最终选择一个具体的服务实例。

2 spring cloud loadbalancer已经实现了的负载均衡策略

2.1 spring cloud loadbalancer提供的ServiceInstanceListSupplier

在配置类LoadBalancerClientConfiguration中,我们可以看到spring cloud loadbalancer已经提供了一些ServiceInstanceListSupplier的实现。

2.1.1 discoveryClientServiceInstanceListSupplier

默认的ServiceInstanceListSupplier实现,它从DiscoveryClient获取状态为UP的服务实例列表

2.1.2 zonePreferenceDiscoveryClientServiceInstanceListSupplier

ServiceInstanceListSupplierDiscoveryClient中优先获取与自身zone相同的服务实例列表,没有与自己在同一个zone的服务实例时,才返回DiscoveryClient中所有的服务实例列表。

zone信息从服务实例的metadata-map.zone字段获取,如果是注册到eureka的服务,就是服务实例的配置项eureka.instance.metadata-map.zone的值。客户端自身的zone值优先从配置项spring.cloud.loadbalancer.zone获取,该配置项没有配置的话,则从配置项eureka.instance.metadata-map.zone中获取(假设注册中心是eureka)

2.1.3 healthCheckDiscoveryClientServiceInstanceListSupplier

ServiceInstanceListSupplier会对DiscoveryClient中的服务实例进行健康探测,返回健康的服务实例列表。这种ServiceInstanceListSupplier一般用在使用类似SimpleDiscoveryClient这种本身不提供服务实例健康探测功能的DiscoveryClient场合。如果注册中心是eureka、consul等,则没有必要使用它。

2.1.4 requestBasedStickySessionDiscoveryClientServiceInstanceListSupplier

相当于通过http请求中的指定cookie的值选择服务实例,该cookie的值就是服务实例的instanceId。需要通过配置

spring.cloud.loadbalancer.sticky-session.add-service-instance-cookie: true

来开启该功能,同时,cookie的名字默认为sc-lb-instance-id。也可以通过配置spring.cloud.loadbalancer.instance-id-cookie-name来指定cookie的名字

2.1.5 sameInstancePreferenceServiceInstanceListSupplier

总是尝试选择上次选择的服务实例。它需要一个委托(delegate)的ServiceInstanceListSupplier,如果委托的ServiceInstanceListSupplier中包含上次选择的服务实例,则只返回上次选择的实例。否则返回所有的服务实例。

2.1.6 weightedServiceInstanceListSupplier

ServiceInstanceListSupplier支持为每一个实例分配一个权重(weight),根据权重确定实例被选择的概率。

权重值从服务实例的metadata-map.weight字段获取,如果是注册到eureka的服务,就是服务实例的配置项eureka.instance.metadata-map.weight的值。

2.1.7 HintBasedServiceInstanceListSupplier

在spring boot3的新版本中,配置类LoadBalancerClientConfiguration已经没有创建基于HintBasedServiceInstanceListSupplierServiceInstanceListSupplier了。如果需要继续使用,需要自己创建基于HintBasedServiceInstanceListSupplierServiceInstanceListSupplier

HintBasedServiceInstanceListSupplier支持为每一个实例指定一个hint值,同时在http请求中包含一个指定hint值的header,HintBasedServiceInstanceListSupplier会优先选择具有与hint header值相同的hint的服务实例。

服务实例的hint值从服务实例的metadata-map.hint字段获取,如果是注册到eureka的服务,就是服务实例的配置项eureka.instance.metadata-map.hint的值。http请求的hint header的名字默认为X-SC-LB-Hint,也可以通过配置项spring.cloud.loadbalancer.hintHeaderName重新指定。

2.2 负载均衡器

spring cloud loadbalancer提供的负载均衡器除了RoundRobinLoadBalancer目前就只有RandomLoadBalancer。loaderbalancer主要通过为RoundRobinLoadBalancer设计不同过滤策略的ServiceInstanceListSupplier来实现不同的负载均衡策略

3 修改默认的负载均衡策略

3.1 修改全局的负载均衡策略

3.1.1 修改默认的ServiceInstanceListSupplier为系统提供的ServiceInstanceListSupplier

上一节我们介绍了好几个系统已经提供的可以直接使用的ServiceInstanceListSupplier,我们只要配置一下即可

spring:
  cloud:
    loadbalancer:
      # 配置默认的`ServiceInstanceListSupplier`为zonePreferenceDiscoveryClientServiceInstanceListSupplier,默认值为default,即默认使用discoveryClientServiceInstanceListSupplier
      configurations: zone-preference

3.1.2 自己创建ServiceInstanceListSupplier bean或discoveryClientServiceInstanceListSupplier bean

如下所示,自己注入一个hintBasedServiceInstanceListSupplier bean,替换默认的discoveryClientServiceInstanceListSupplier bean。你甚至可以自己注入一个完整的discoveryClientServiceInstanceListSupplier bean,用来替换默认的RoundRobinLoadBalancer

@Configuration
public class AppLoadBalancerClientConfiguration {

    @Bean
	public ServiceInstanceListSupplier hintBasedServiceInstanceListSupplier(ConfigurableApplicationContext context) {
		return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withHints().withCaching()
				.build(context);
	}
	
}

3.1.3 自己创建一个configuration类,通过@LoadBalancerClients注解添加

// 这个配置类不要注册到spring容器中
public class AppLoadBalancerClientConfiguration {

    @Bean
	public ServiceInstanceListSupplier hintBasedServiceInstanceListSupplier(ConfigurableApplicationContext context) {
		return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withHints().withCaching()
				.build(context);
	}
	
}


@LoadBalancerClients(defaultConfiguration = AppLoadBalancerClientConfiguration.class)
@SpringBootApplication
public class AppClient1Application {

    public static void main(String[] args) {
        SpringApplication.run(AppClient1Application.class, args);
    }

}

3.2 基于服务配置负载均衡策略

spring cloud loadbalancer支持为每个服务配置独立的负载均衡策略

3.2.1 通过@LoadBalancerClient注解添加

@LoadBalancerClient(name = "app-server", configuration = AppLoadBalancerClientConfiguration.class)
@FeignClient(name = "app-server", configuration = AppServerFeignConfig.class)
public interface AppServerFeign {

    @PostMapping(value = "/hello/{who}", headers = {"Content-Type: application/json"})
    HelloResponse hello(@PathVariable String who, @RequestParam String type, @RequestBody HelloRequest request);
}

3.2.2 在@LoadBalancerClients注解中统一添加所有服务的@LoadBalancerClient注解


@LoadBalancerClients(value = {
        @LoadBalancerClient(name = "app-server", configuration = AppLoadBalancerClientConfiguration.class)
})
public class AppClient1Application {

    public static void main(String[] args) {
        SpringApplication.run(AppClient1Application.class, args);
    }

}

4 loadbalancer cache

从上面ServiceInstanceListSupplier的创建过程来看,系统创建的ServiceInstanceListSupplier都包括了一个cache。我们可以通过以下配置控制cache的容量和过期时间。

spring:
  cloud:
    loadbalancer:
      cache:
        # 缓存条目过期时间,默认是35秒
        ttl: 35s
        # cache容量,默认256条缓存条目
        capacity: 1024

如果不想使用cache(如注册中心是eureka,就不需要在loadbalancer中维护一个cache了),可以通过自己创建一个不包含cache的ServiceInstanceListSupplier,或者通过配置禁用cache

spring:
  cloud:
    loadbalancer:
      cache:
        # 禁用loadbalancer cache,默认使能
        enable: false

5 loadbalancer retry

失败重试操作在应用中是如此常见,以致到处可以看到它的身影。毫不意外地,spring cloud loadbalancer层也支持重试功能。在微服务环境中,一个服务往往存在多个节点,重试时往往希望重试的是另一个服务节点,以便提高重试成功的概率。在其他地方进行重试,默认情况下,重试时可能请求的还是同一个节点。因为,假设负载均衡器采用常用的RoundRobinLoadBalancer,被调用的服务有两个实例A、B,这时请求实例A失败了,然后进行重试,但是在进行重试前,有其他的业务处理流程也调用该服务,这样重试时,还是请求的实例A。而loadbalancer实现的重试,可以避免重试时调用相同的实例。

@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
public class LoadBalancerClientConfiguration {

	static final class BlockingOnAvoidPreviousInstanceAndRetryEnabledCondition extends AllNestedConditions {

		private BlockingOnAvoidPreviousInstanceAndRetryEnabledCondition() {
			super(ConfigurationPhase.REGISTER_BEAN);
		}

		@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
				matchIfMissing = true)
		static class LoadBalancerRetryEnabled {

		}

        // 避免选择前一个实例,默认开启
		@Conditional(AvoidPreviousInstanceEnabledCondition.class)
		static class AvoidPreviousInstanceEnabled {

		}

	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnBlockingDiscoveryEnabled
	@ConditionalOnClass(RetryTemplate.class)
	@Conditional(BlockingOnAvoidPreviousInstanceAndRetryEnabledCondition.class)
	@AutoConfigureAfter(BlockingSupportConfiguration.class)
	@ConditionalOnBean(ServiceInstanceListSupplier.class)
	public static class BlockingRetryConfiguration {

		@Bean
		@ConditionalOnBean(DiscoveryClient.class)
		@Primary
		public ServiceInstanceListSupplier retryAwareDiscoveryClientServiceInstanceListSupplier(
				ServiceInstanceListSupplier delegate) {
			return new RetryAwareServiceInstanceListSupplier(delegate);
		}

	}

}

retry配置示例

spring:
  cloud:
    loadbalancer:
      # 全局
      retry:
        enabled: true
        avoid-previous-instance: true
        retry-on-all-exceptions: false
        retryable-exceptions: java.io.IOException
        retryable-status-codes: 500
        retry-on-all-operations: false
        backoff:
          enabled: true
          jitter: 10
          min-backoff: 5
          max-backoff: 100
        max-retries-on-next-service-instance: 1
        max-retries-on-same-service-instance: 0
      clients: 
        # 特定服务
        app-server:
          retry:
            enabled: true
            avoid-previous-instance: true
            retry-on-all-exceptions: false
            retryable-exceptions: java.io.IOException
            retryable-status-codes: 500
            retry-on-all-operations: false
            backoff:
              enabled: true
              jitter: 10
              min-backoff: 5
              max-backoff: 100
            max-retries-on-next-service-instance: 1
            max-retries-on-same-service-instance: 0
      

6 loadbalancer metric

spring cloud loadbalancer已经支持了一些metric,但是metric统计功能默认是关闭的,可以通过配置打开

spring:
  cloud:
    loadbalancer:
      stats:
        # 开启统计,默认关闭
        micrometer: true