在文章 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);
}
}
继续查看RoundRobinLoadBalancer
的choose()
方法实现:
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
该ServiceInstanceListSupplier
从DiscoveryClient
中优先获取与自身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
已经没有创建基于HintBasedServiceInstanceListSupplier
的ServiceInstanceListSupplier
了。如果需要继续使用,需要自己创建基于HintBasedServiceInstanceListSupplier
的ServiceInstanceListSupplier
。
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