Spring Cloud LoadBalancer

发布时间 2023-04-01 17:58:45作者: 青子Aozaki

ReactiveLoadBalancer与ServiceInstanceListSupplier

Spring Cloud提供了client的load-balance抽象和实现。在load-balance机制中添加了ReactiveLoadBalancer接口,并且为其提供了Round-Robin-based和Random实现。

为了从反应式服务中选择服务实例,使用了ServiceInstanceListSupplier接口。
Spring Cloud使用ServiceInstanceListSupplier的service-discovery-based的实现来从使用类路径中可用的DiscoveryClient的ServiceDiscovery中检索可用服务实例。

RoundRobinLoadBalancer

RoundRobinLoadBalancer是默认情况下使用的ReactiveLoadBalancer的实现类。
还可以自定义LoadBalancer的配置来切换不同的ReactiveLoadBalancer实现。

切换自己的Spring Cloud LoadBalancer配置

可以使用@LoadBalancerClient来切换自己的load-balancer client配置,如下:

@Configuration
//value的值指定了在给定的load-balancer client配置下,要发送请求到哪个服务(服务id)
@LoadBalancerClient(value = "stores", configuration = CustomLoadBalancerConfiguration.class)
public class MyConfiguration {
​
    @Bean
    @LoadBalanced
    public WebClient.Builder loadBalancedWebClientBuilder() {
        return WebClient.builder();
    }
}

或者通过@LoadBalancerClients来定义多个配置:

@LoadBalancerClients({
    @LoadBalancerClient(value = "stores", configuration = StoresLoadBalancerClientConfiguration.class),
    @LoadBalancerClient(value = "customers", configuration = CustomersLoadBalancerClientConfiguration.class)
})

作为@LoadBalancerClient和@LoadBalancerClients注释的参数的类不应该使用@Configuration进行注释,也不应在组件扫描之外。
为了更容易地使用自己的LoadBalancer配置,可以使用ServiceInstanceListSupplier类的builder()方法。
也可以使用Spring Cloud预定义的其他配置方案,来替换默认配置,例如设置spring.cloud.loadbalancer.configurations属性为zone-preference来使用带有缓存的ZonePreferenceServiceInstanceListSupplier,或设置为health-check来使用带有缓存的HealthCheckServiceInstanceListSupplier。
可以利用这个特性来实例化不同的ServiceInstanceListSupplier或ReactorLoadBalancer的实现来覆盖默认的配置。

Zone-Based Load-Balancing

为了实现zone-based的load-balancing,Spring Cloud提供了ZonePreferenceServiceInstanceListSupplier,使用特定DiscoveryClient的zone配置(例如,eureka.instance.metadata-map.zone)来选择客户端尝试筛选可用服务实例的区域。

可以通过设置spring.cloud.loadbalancer.zone属性,覆盖特定DiscoveryClient的区域(zone)配置。

为了确定获取到的服务实例(ServiceInstance)的区域(zone),会检测它元数据(metadata)中"zone"的值。
ZonePreferenceServiceInstanceListSupplier会筛选获取到的服务实例,只返回在同一区域(zone)内的实例。如果zone的值为null,或者没有在区域内的服务实例,就会返回所有获取到的实例。

使用Zone-based Load-Balancing:
Spring Cloud使用委托来处理ServiceInstanceListSupplierbeans。
建议在ZonePreferenceServiceInstanceListSupplier的构造器中传入DiscoveryClientServiceInstanceListSupplier的委托,然后用CachingServiceInstanceListSupplier包装ZonePreferenceServiceInstanceListSupplier以利用LoadBalancer Cache。

//将该类作为@LoadBalancerClient或@LoadBalancerClients的configuration值,以配置LoadBalancer
public class CustomLoadBalancerConfiguration {
    @Bean
    public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
            ConfigurableApplicationContext context) {
        return ServiceInstanceListSupplier.builder()
                    .withDiscoveryClient()
                    .withZonePreference()
                    .withCaching()
                    .build(context);
    }
}
Health-Check Load-Balancing

为了让能够为LoadBalancer提供定期的健康检查成为可能,Spring Cloud提供了HealthCheckServiceInstanceListSupplier,它会通过委托ServiceInstanceListSupplier定期的验证这些服务实例是否存活,然后只返回健康的服务实例,除非一个健康的服务实例都没有,那么它就会返回所有获取到的服务实例。

这个机制在使用SimpleDiscoveryClient时尤其有用。对于由实际Service Registry支持的客户端没有必要使用,因为在查询外部的Service Discovery后,我们已经获得了健康的服务实例。
当每个服务只有很少数量的服务实例时,也推荐使用该机制,以避免对失效的服务实例不停地调用。
HealthCheckServiceInstanceListSupplier依赖于委托(代理)流提供的更新的服务实例。在少数情况下,当我们想要使用不刷新服务实例的委托操作时,你可以设置spring.cloud.loadbalancer.health-check.refetch-instances为true,来让HealthCheckServiceInstanceListSupplier刷新服务实例列表,尽管服务实例的列表可能会改变(例如DiscoveryClientServiceInstanceListSupplier)。
也可以通过修改spring.cloud.loadbalancer.health-check.refetch-instances-interval的值来调整刷新的时间间隔。
修改spring.cloud.loadbalancer.health-check.repeat-health-check为false来选择取消额外的重复的健康检查,因为每个服务实例的刷新也会触发一次健康检查。
HealthCheckServiceInstanceListSupplier使用前缀为spring.cloud.loadbalancer.health-check的属性。可以用其设置定期检查的initialDelay和interval。
可以通过设置spring.cloud.loadbalancer.health-check.path.default的值设置健康检查的默认URL。也可以用服务的ID作为spring.cloud.loadbalancer.health-check.path.[SERVICE_ID]中[SERVICE_ID]的值([SERVICE_ID]的值为服务的正确ID)。如果[SERVICE_ID]的值没有被指定,则默认为/actuator/health。如果[SERVICE_ID]的值为null或是一个空值,则健康检查不会被执行。
如果依赖于默认路径/actuator/health,确保添加了spring-boot-starter-actuator到你的依赖中,除非打算自己添加一个/actuator/health路径的服务。
也可以通过设置spring.cloud.loadbalancer.health-check.port来自定义健康检查请求的端口。如果没有设置,则被健康检查请求的端口为服务实例上可用的端口。
使用Health-Check Load-Balancing:
Spring Cloud使用委托来处理ServiceInstanceListSupplierbeans。建议在HealthCheckServiceInstanceListSupplier的构造器中传入一个DiscoveryClientServiceInstanceListSupplier的委托。
public class CustomLoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withHealthChecks()
.build(context);
}
}
对于non-reactive的模式,使用withBlockingHealthChecks()来创建HealthCheckServiceInstanceListSupplier。也可以传入自定义的WebClient和RestTemplate用于健康检查。
HealthCheckServiceInstanceListSupplier有它自己的基于Reactor Fluxreplay()的缓存机制。所以,如果使用该模式的Load-Balancing,可以跳过将其使用CachingServiceInstanceListSupplier的步骤。
Same-Instance Load-Balancing
在这种模式中,可以设置LoadBalancer优先选择先前选择过的服务实例(如果该实例可用的话)。
使用Same-Instance Load-Balancing:
需要使用SameInstancePreferenceServiceInstanceListSupplier。可以通过spring.cloud.loadbalancer.configurations为same-instance-preference或者通过提供自定义的ServiceInstanceListSupplierbean:
public class CustomLoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withSameInstancePreference()
.build(context);
}
}
这也是Zookeeper的StickyRule的一个替代方案。
Request-based Sticky Session Load-Balancing
在这种模式中,LoadBalancer优先选择与请求携带的cookie中提供的instanceId对应的服务实例。
如果请求通过Spring Cloud的LoadBalancer交换过滤器方法和过滤器时使用的ClientRequestContext或ServerHttpRequestContext传递给LoadBalancer,Spring Cloud才会支持这种模式。
该模式目前只被WebClient-backed load-balancing支持。
使用Request-based Sticky Session Load-Balancing:
需要使用RequestBasedStickySessionServiceInstanceListSupplier。可以通过设置spring.cloud.loadbalancer.configurations为request-based-sticky-session或提供自定义的ServiceInstanceListSupplierbean:
public class CustomLoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withRequestBasedStickySession()
.build(context);
}
}
本模式中,在向前发送请求之前更新所选服务实例(如果原始请求cookie中的服务实例不可用,则该服务实例可能与原始请求cookie中的服务实例不同)非常有用。可以通过设置spring.cloud.loadbalancer.sticky-session.add-service-instance-cookie为true来启用这一功能。
默认情况下,cookie的名字为sc-lb-instance-id,通过设置spring.cloud.loadbalancer.instance-id-cookie-name修改它的名字。
Hint-based Load-Balancing
Spring Cloud LoadBalancer可以在Request中设置传入LoadBalancer的String Hints,之后会被用于ReactiveLoadBalancer来处理这些Hints。
可以通过spring.cloud.loadbalancer.hint.default设置所有服务默认的hint,也可以通过设置spring.cloud.loadbalancer.hint.SERVICE_ID来为特定的服务设置特定的hint。
Spring Cloud提供了HintBasedServiceInstanceListSupplier,这是一个ServiceInstanceListSupplier的hint-based实现。该类会检查hint请求头(默认的请求头名称为X-SC-LB-Hint,可以通过spring.cloud.loadbalancer.hint-header-name来修改该名称),如果找到一个hint请求头,就会用该请求头的hint值来筛选服务实例。如果没有hint请求头,HintBasedServiceInstanceListSupplier使用上面提到的spring.cloud.loadbalancer.hint.[SERVICE_ID]设置的hint值来筛选请求实例。如果们没有设置hint值,也没有hint请求头,就会返回所有服务实例。
当筛选服务实例时,HintBasedServiceInstanceListSupplier查找请求实例中的metadataMap中的hint值,并返回匹配的服务实例。如果没有匹配的实例,就会返回所有的服务实例。
使用Hint-based Load-Balancing:
public class CustomLoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withHints()
.withCaching()
.build(context);
}
}
ReactorLoadBalancerExchangeFilterFunction
为了更容易的使用Spring Cloud的Load Balancer,可以使用ReactorLoadBalancerExchangeFilterFunction结合WebClient与BlockingLoadBalancerClient(配合RestTemplate)。
Spring RestTemplate作为Load Balancer Client
使用@LoadBalanced来创建一个load-balanced RestTemplate:
@Configuration
public class MyConfiguration {

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

public class MyClass {
@Autowired
private RestTemplate restTemplate;

public String doOtherStuff() {
//URI需要使用一个虚拟的host name(即service name),BlockingLoadBalancerClient会用其来创建一个完整的物理地址
String results = restTemplate.getForObject("http://stores/stores", String.class);
return results;
}
}
要使用load-balanced RestTemplate,类路径中必须要有load-balancer的实现类。(添加Spring Cloud LoadBalancer stater依赖)
Spring WebClient作为Load Balancer Client
使用@LoadBalanced来自动创建一个load-balanced client:
@Configuration
public class MyConfiguration {

@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}

public class MyClass {
@Autowired
private WebClient.Builder webClientBuilder;

public Mono doOtherStuff() {
//URI需要使用一个虚拟的host name(即service name),BlockingLoadBalancerClient会用其来创建一个完整的物理地址
return webClientBuilder.build().get().uri("http://stores/stores")
.retrieve().bodyToMono(String.class);
}
}
要使用load-balanced RestTemplate,类路径中必须要有load-balancer的实现类。(添加Spring Cloud LoadBalancer stater依赖)
在底层ReactiveLoadBalancer会起作用。
Spring WebFlux WebClient结合ReactorLoadBalancerExchangeFilterFunction**
如果spring-webflux在类路径中,ReactorLoadBalancerExchangeFilterFunction会被自动配置。
public class MyClass {
@Autowired
private ReactorLoadBalancerExchangeFilterFunction lbFunction;

public Mono doOtherStuff() {
//URI需要使用一个虚拟的host name(即service name),BlockingLoadBalancerClient会用其来创建一个完整的物理地址
return WebClient.builder().baseUrl("http://stores")
.filter(lbFunction)
.build()
.get()
.uri("/stores")
.retrieve()
.bodyToMono(String.class);
}
}
Spring WebFlux WebClient结合Non-reactive Load Balancer Client
如果spring-webflux在类路径中,LoadBalancerExchangeFilterFunction会被自动配置。
public class MyClass {
@Autowired
private LoadBalancerExchangeFilterFunction lbFunction;

public Mono doOtherStuff() {
//URI需要使用一个虚拟的host name(即service name),BlockingLoadBalancerClient会用其来创建一个完整的物理地址
return WebClient.builder().baseUrl("http://stores")
.filter(lbFunction)
.build()
.get()
.uri("/stores")
.retrieve()
.bodyToMono(String.class);
}
}
该方法已经被废弃了,建议使用Spring WebFlux WebClient结合ReactorLoadBalancerExchangeFilterFunction
Spring Cloud LoadBalancer Caching
基本上,ServiceInstanceListSupplier实现类每次要选择服务实例时都要通过DiscoveryClient类来获取,但在此基础之上我们还提供了两层缓存实现。
Caffeine-backed LoadBalancer Cache实现
如果com.github.ben-manes.caffeine:caffeine在类路径中,Caffeine-based实现就会被使用。
可以通过设置spring.cloud.loadbalancer.cache.caffeine.spec属性为自己定义的CaffeineSpec类来覆盖Caffeine Cache对LoadBalancer的默认配置。
传入自定义的CaffeineSpec类会覆盖所有其他的LoadBalancerCache设置,包括ttl和capacity。
默认的LoadBalancer Cache实现
如果类路径中没有Caffeine Cache,则DefaultLoadBalancerCache就会被使用。该类由spring-cloud-starter-loadbalancer自动配置。
配置LoadBalancer Cache
你可以自定义ttl(entries生存时间)值,设置spring.cloud.loadbalancer.cache.ttl为Duration类的String格式。同理可以设置spring.cloud.loadbalancer.cache.capacity属性来自定义LoadBalancer Cache的初始容量。
ttl和initialCapacity的默认值分别为35秒和256。
可以设置spring.cloud.loadbalancer.cache.enabled为false来禁用缓存。
虽然基本的、非缓存的实现对于原型设计和测试很有用,但它的效率远远低于缓存版本,因此建议在生产中始终使用缓存版本。如果DiscoveryClient实现(例如EurekaDiscoveryClient)已经完成了缓存,则应禁用配置LoadBalancer Cache以防止双重缓存。
Transform the load-balanced HTTP request
可以使用被筛选出来的服务实例来转换load-balanced HTTP request。
要用RestTemplate的话,需要实现并定义LoadBalancerRequsetTransfomerbean:
@Bean
public LoadBalancerRequestTransformer transformer() {
return new LoadBalancerRequestTransformer() {
@Override
public HttpRequest transformRequest(HttpRequest request, ServiceInstance instance) {
return new HttpRequestWrapper(request) {
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.putAll(super.getHeaders());
headers.add("X-InstanceId", instance.getInstanceId());
return headers;
}
};
}
};
}
要用WebClient的话,需要实现并定义LoadBalancerClientRequestTransformerbean:
@Bean
public LoadBalancerClientRequestTransformer transformer() {
return new LoadBalancerClientRequestTransformer() {
@Override
public ClientRequest transformRequest(ClientRequest request, ServiceInstance instance) {
return ClientRequest.from(request)
.header("X-InstanceId", instance.getInstanceId())
.build();
}
};
}
如果定义了多个transformer,它们会按照bean的定义顺序来作用。或者,可以使用LocalBalancerRequestTransformer.DEFAULT_ORDER或者LoadBalancerClientRequestTransformer.DEFAULT_ORDER来定义它们的作用顺序。
Spring Cloud LoadBalancer Lifecycle
LoadBalancerLifecyclebeans提供了一些回调方法:onStart(Request request)、onStartRequest(Request request, Response lbResponse)和onComplete(CompletionContext<RES, T, RC> completionContext),需要实现这些方法来规定load-balancing前后发生的逻辑。
onStart(Request request)方法需要传入一个Request对象参数,其中包含着用来筛选合适服务实例的信息(包括downstream client request和hint)。onStartRequest(Request request, Response lbResponse)方法需要传入一个Request对象和一个Response对象作为参数。onComplete(CompletionContext<RES, T, RC> completionContext)方法需要传入一个CompletionContext对象,其中包含LoadBalancer Response(包括选择的服务实例),还有在服务实例上执行的请求的Status以及(如果有的话)返回给downstream client的Response,如果有异常发生的话还会包含相应的Throwable对象。
supports(Class requestContextClass, Class responseClass, Class serverTypeClass)方法返回的boolean值决定了请求过程中的处理器(processor in question)是否处理给定类型的对象,默认情况下(方法没有被重写)返回true。
RC:RequestContext type; RES:client的response type;T:返回的server type
Spring Cloud LoadBalancer Statistics
Spring Cloud提供了一个LoadBalancerLifecyclebean叫做MicrometerStatsLoadBalancerLifecycle,它使用MicroMeter来提供load-balancer的调用统计。
可以通过设置spring.cloud.loadbalancer.stats.micrometer.enabled为true,并且有一个可用的MeterRegistry对象(Spring Boot Actuator会将其添加到项目中)。
MicrometerStatsLoadBalancerLifecycle将下列的meters注册到MeterRegistry中:
loadbalancer.requests.active: 一个仪表,让你能够监测任何服务实例当前激活的请求数(或其他通过tags附加的服务实例信息)。
loadbalancer.requests.success:一个计时器,测量任何load-balanced请求的执行时间,该请求以将响应传递给underlying client而结束。
loadbalancer.requests.failed:一个计时器,测量任何load-balanced请求的执行时间,该请求因异常而结束。
loadbalancer.requests.discard:一个计数器,计量被丢弃的load-balanced请求数,即LoadBalancer尚未检索到运行请求的服务实例的请求数。
关于服务实例、请求数据和响应数据的附加信息在可用时通过tags添加到度量(metrics)中。
对于有些实现类,如BlockingLoadBalancerClient,request和response data可能不可用,因为我们根据参数建立泛型类型,所以可能无法确定类型并读取数据。
当为给定Meter添加至少一条记录时,Meters将在MeterRegistry中注册。
Configuring Individual LoadBalancerClients
单独的LoadBalancer clients可以通过一个不同的前缀spring.cloud.loadbalancer.clients.来单独配置,为loadbalancer的名称。默认配置值可以被spring.cloud.loadbalancer.命名空间设置,并且会被优先合成为client的特定值。
spring:
cloud:
loadbalancer:
health-check:
initial-delay: 1s
clients:
myclient:
health-check:
interval: 30s
上面的例子将合成一个@ConfigurationProperties的对象并且设置了initial-dalay=1s和interval=30s。
大多数client属性可以单独配置,除了以下的全局配置:
spring.cloud.loadbalancer.enabled:全局启用/关闭load-balancing
spring.cloud.loadbalancer.retry.enabled:全局启用/关闭load-balancing重试机制,如果全局启用了它,仍然可以在单独的client属性中配置来关闭它。
spring.cloud.loadbalancer.cache.enabled:全局启用/关闭load-balancing缓存,如果全局启用了它,仍然可以通过创建自定义配置(在ServiceInstanceListSupplier委托层级中不包含CachingServiceInstanceListSupplier)来关闭它。
spring.cloud.loadbalancer.stats.micrometer.enabled:全局启用/关闭LoadBalancer Micrometer metrics。