SpringCloud LoadBalancer 负载均衡
SpringCloud负载均衡组件一直使用的是
Netflix-Ribbon
组件,但是在 SpringCloud 2020版本以后 SpringCloud剔除掉了 出eureka-server
与eureka-client
除外的所有Netflix
组件,但是官方也提供了一些替代品如下图,由此也能从中看到Ribbon
的负载均衡组件被SpringCloud-Loadbalancer
组件进行代替,这次我们就来讲一下SpringCloud-Loadbalancer
的使用流程使用的技术栈
SpringBoot
2.4.6
SpringCloud
2020.0.3
SpringCloud-Netflix-Eureka-Server
SpringCloud-Netflix-Eureka-Client
小弟有一个开源项目,希望大家可以多多一键三连,谢谢大家
后续的源码解析也都会进行同步更新上去
1、什么是负载均衡?
因为现在都在说微服务,分布式的整体思路,当我们在多集群环境下即有多个生产者,而消费者在请求生产者的服务实例时,这时候我们怎么样在多个服务实例中去选择正确的一个服务实例呢?这时候就需要负载均衡组件来帮助我们能够正确并且合理的把流量分发到对应的生产者服务上。
2、怎么去使用 LoadBalancer 负载均衡?
我们在项目中使用的组件全部都是SpringCloud官方提供的组件,所以我们注册中心使用 Eureka
的注册中心,Eureke
的源码解析在我上篇博客中有所介绍,这里就不多细说了。
/**
* 创建一个Eureka-Server 注册中心
*
**/
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
我们在SpringBoot启动类上面添加 @EnableEurekaServer
注解,会自动去装配 EurekaServer
的相关组件,由此我们的 EurekaServer
注册中心就已经启动成功了
当我们的注册中心启动好了之后,我们就需要将我们的生产者服务注册到我们的 EurekaServer
中,我们的 Eureka-Client
端的代码如下
/**
* 创建一个Eureka-Client ServiceA
*
**/
@EnableEurekaClient
@SpringBootApplication
public class EurekaServiceAApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServiceAApplication.class, args);
}
/**
* 暴露出一个请求接口
*
**/
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping("/sayHello/{test}")
public String test(@PathVariable("test") String test) {
System.out.println("sayHello:" + test);
return "sayHello:" + test;
}
}
}
我们在服务A
的启动类上面标注 @EnableEurekaClient
注解会将当前的服务注册到 EurekaServer
中并且会拉取对应的服务注册表,并且我们在当前的服务A
实例中暴露了一个 test/sayHello/{test}
接口,那么我们的消费者就可以根据 ServiceA
的服务实例进行请求到这个接口,而且我们在注册中心的控制台上面也可以看到对应的服务实例信息,而我们需要集群模式,所以我们先启动两个服务实例。
当我们生产者有了之后,就需要消费者来进行数据请求,我们再来创建一个消费者
/**
* 创建一个Eureka-Client ServiceB
* 用于服务调用
**/
@EnableEurekaClient
@SpringBootApplication
public class EurekaServiceBApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServiceBApplication.class, args);
}
@RestController
@Configuration
public class ServiceBController {
/**
* 创建一个 基于负载均衡的 RestTemplate http请求组件
*
*/
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
/**
* 创建一个接口,使用 RestTemplate 进行调用 ServiceA的服务实例+接口
*
*/
@RequestMapping(value = "/greeting/{name}", method = RequestMethod.GET)
public String greeting(@PathVariable("name") String name) {
RestTemplate restTemplate = getRestTemplate();
return restTemplate.getForObject("http://ServiceA/test/sayHello/" + name, String.class);
}
}
}
我们创建了一个消费者 ServiceB
,并且通过 RestTemplate
组件进行Http
请求 http://ServiceA/test/sayHello/
地址,但是我们的 RestTemplate
上面有我们今天说的主题 @LoadBalanced
注解标记然后我们进行请求接口就可以发现,从 ServiceB
调用 ServiceA
的服务实例请求,分发到了两个 ServiceA
生产者服务实例中。
3、@LoadBalanced 详解
从上面的请求来看,我们知道了 @LoadBalanced
能够使 RestTemplate
自动进行负载均衡请求 ServiceA
的服务,具体的流程是怎么样的?我们接下来继续来看源码。
首先我们从 @LoadBalanced
注解类上面的注释看到了
Annotation to mark a RestTemplate or WebClient bean to be configured to use a LoadBalancerClient
这句话让我们知道,被这个注解标注的 RestTemplate
或者 WebClient
都会被 LoadBalancerClient
进行配置,从这句话来看的话,被标注后的请求会被拦截,然后使用 LoadBalancerClient
进行请求。具体是不是这样的呢?我们继续往下看,因为 SpringCloud
是基于 SpringBoot
来进行做的框架,那么 SpringBoot
的自动装配特性也会在 SpringCloud
中进行体现,那么我们直接去找一下有没有 LoadBalancerAutoConfiguration
,我们发现还确实有,并且有两个,一个是 springcloud-common 包下面的 org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration
,还有一个
springcloud-loadbalancer 包下面的 org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration
,有两个装配 loadbalancer
组件的类,我们从 common包下面的 LoadBalancerAutoConfiguration
能够看到,我们看到会把 restTemplate
的拦截器上面在加上一个新的 LoadBalancerInterceptor
,由此看到我们上面的猜想是正确的。然后我们通过 LoadBalancerInterceptor
拦截器的 intercept
执行方法能够看到,根据获取到的 ServiceName
服务名称和创建一个 LoadBalancerRequest
请求去远程调用接口服务实例接口。
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
// 从请求地址中获取 serviceName
String serviceName = originalUri.getHost();
// 根据 LoadBalancerClient进行调用服务接口
return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
}
4、BlockingLoadBalancerClient 负载均衡客户端实现
从代码中我们看到 LoadBalancerClient
的抽象接口只有一个实现类就是 BlockingLoadBalancerClient
,从具体的执行代码中能够看到
/**
* LoadBalancerInterceptor核心调用的方法,包含了获取服务实例,远程调用
*
**/
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
String hint = getHint(serviceId);//默认 default
// 根据请求构造一个RequestAdapter
LoadBalancerRequestAdapter<T, DefaultRequestContext> lbRequest = new LoadBalancerRequestAdapter<>(request,
new DefaultRequestContext(request, hint));
// 从 AnnotationConfigApplicationContext 获取 LoadBalancerLifecycle
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = getSupportedLifecycleProcessors(serviceId);
// LoadBalancerLifecycle 默认实现是 MicrometerStatsLoadBalancerLifecycle onStart方法是个空
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
//根据负载均衡算法获取到当前的服务实例
ServiceInstance serviceInstance = choose(serviceId, lbRequest);
if (serviceInstance == null) {
//实例找不到,直接标记当前请求 DISCARD 并且抛出异常
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, new EmptyResponse())));
throw new IllegalStateException("No instances available for " + serviceId);
}
// 拿到服务实例、LoadBalancerRequest去调用请求http服务接口
return execute(serviceId, serviceInstance, lbRequest);
}
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request)
throws IOException {
// 创建一个想响应对象
DefaultResponse defaultResponse = new DefaultResponse(serviceInstance);
// 根据请求构造一个RequestAdapter,并且循环调用 onStartRequest方法,为请求
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = getSupportedLifecycleProcessors(serviceId);
Request lbRequest = request instanceof Request ? (Request) request : new DefaultRequest<>();
supportedLifecycleProcessors
.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, new DefaultResponse(serviceInstance)));
try {
// 使用 LoadBalancerRequest去调用请求http服务接口
T response = request.apply(serviceInstance);
//处理 ClientResponse
Object clientResponse = getClientResponse(response);
//标记当前请求成功
supportedLifecycleProcessors
.forEach(lifecycle -> lifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS,
lbRequest, defaultResponse, clientResponse)));
return response;
}
。。。
}
@Override
public <T> ServiceInstance choose(String serviceId, Request<T> request) {
// 从 AnnotationConfigApplicationContext获取ReactorServiceInstanceLoadBalancer接口的默认实现 RoundRobinLoadBalancer轮训负载均衡组件
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId);
if (loadBalancer == null) {
return null;
}
// 从RoundRobinLoadBalancer轮训负载均衡组件中获取到当前请求服务实例的 ServiceInstance,采用轮训算法
Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
if (loadBalancerResponse == null) {
return null;
}
// 返回一个ServiceInstance
return loadBalancerResponse.getServer();
}
我们从代码中看到最主要的获取服务实例的方法是 choose(String serviceId, Request<T> request)
方法,获取实例的方法主要是通过根据 SpringBean
获取到 ReactiveLoadBalancer
4.1 loadBalancerClientFactory.getInstance方法解析
在 LoadBalancerClientFactory
工厂类中其核心是调用父类的 NamedContextFactory
的 getInstance
,在父类中会去维护一个 Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
数据结构,以 key:服务名称,value:Spring的ApplicationContext,的数据结构进行存储,而第一次服务请求时找不到对应的 ApplicationContext
就会去创建一个新的 AnnotationConfigApplicationContext
spring的上下文,然后后面想要获取一些别的组件全部都是通过 AnnotationConfigApplicationContext.getBean()
方法进行获取。
主要是在创建 AnnotationConfigApplicationContext
对象时,会去注册一些组件,主要的是:LoadBalancerClientFactory
创建client工厂, LoadBalancerClientConfiguration
负载均衡client的配置类, PropertyPlaceholderAutoConfiguration
配置信息。
4.2 负载均衡请求方式判断阻塞与非阻塞
新版的springcloud
代码引入了 Reactive
的一些组件还有使用了大量的Java8的函数式编程,而在 loadbalancer
里面在进行负载均衡请求处理是分为 ReactiveSupport(非阻塞)
和 BlockingSupport(阻塞)
两种方式去请求,默认是使用的 BlockingSupport(阻塞)
的配置,如果要是想使用 ReactiveSupport(非阻塞)
需要引入 org.springframework.web.reactive.function.client.WebClient
相关jar包。
4.3 ServiceInstanceListSupplier 对象详解
在默认情况下负载均衡会使用 org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration.BlockingSupportConfiguration
配置类,并且会初始化基于缓存的 ServiceInstanceListSupplier
使用了 Builder
的设计模式
@Bean
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "default",
matchIfMissing = true)
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
//通过 ServiceInstanceListSupplier.builder() 模式去构建一个获取ServiceInstanceList的Supplier,并且提供cache服务实例的功能
return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withCaching().build(context);
}
org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplierBuilder#withDiscoveryClient
public ServiceInstanceListSupplierBuilder withDiscoveryClient() {
// 创建一个DiscoveryClientServiceInstanceListSupplier,能够从Eureka实例列表中获取到相应的实例
this.baseCreator = context -> {
ReactiveDiscoveryClient discoveryClient = context.getBean(ReactiveDiscoveryClient.class);
return new DiscoveryClientServiceInstanceListSupplier(discoveryClient, context.getEnvironment());
};
return this;
}
org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplierBuilder#withCaching
public ServiceInstanceListSupplierBuilder withCaching() {
//创建一个 CachingServiceInstanceListSupplier ,会把上面的创建一个DiscoveryClientServiceInstanceListSupplier放进去,
//并且从Spring中获取LoadBalancerCacheManager作为缓存组件
this.cachingCreator = (context, delegate) -> {
ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
.getBeanProvider(LoadBalancerCacheManager.class);
if (cacheManagerProvider.getIfAvailable() != null) {
return new CachingServiceInstanceListSupplier(delegate, cacheManagerProvider.getIfAvailable());
}
return delegate;
};
return this;
}
org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplierBuilder#build
public ServiceInstanceListSupplier build(ConfigurableApplicationContext context) {
//把ConfigurableApplicationContext放入创建好的函数方法进行执行
ServiceInstanceListSupplier supplier = baseCreator.apply(context);
for (DelegateCreator creator : creators) {
supplier = creator.apply(context, supplier);
}
if (this.cachingCreator != null) {
supplier = this.cachingCreator.apply(context, supplier);
}
return supplier;
}
从上面能够看到 ServiceInstanceListSupplierBuilder
对象创建了 DiscoveryClientServiceInstanceListSupplier
和 CachingServiceInstanceListSupplier
两个主要的函数式对象,从 DiscoveryClientServiceInstanceListSupplier
对象能够知道,这个对象是从 eureka
注册中心根据服务名称获取该服务实例列表,CachingServiceInstanceListSupplier
能够知道,在从 DiscoveryClientServiceInstanceListSupplier
获取到的数据会缓存到一个 Cache
中,其主要实现是 LoadBalancerCacheManager
,而且两个对象都会维护一个 serviceInstances
实例集合
4.4 DiscoveryClientServiceInstanceListSupplier
我们从 DiscoveryClientServiceInstanceListSupplier
构造函数中就能直接看到,在创建 DiscoveryClientServiceInstanceListSupplier
对象时,就会直接去从 DiscoveryClient
获取该实例id的实例列表
//Blocking 获取
public DiscoveryClientServiceInstanceListSupplier(DiscoveryClient delegate, Environment environment) {
//读取loadbalancer.client.name服务名称
this.serviceId = environment.getProperty(PROPERTY_NAME);
//注册表请求超时时间,默认30S
resolveTimeout(environment);
//创建一个Flux函数,当delegate.getInstances(serviceId)请求超时,或者异常情况下,返回一个空的集合
this.serviceInstances = Flux.defer(() -> Flux.just(delegate.getInstances(serviceId)))
.subscribeOn(Schedulers.boundedElastic()).timeout(timeout, Flux.defer(() -> {
logTimeout();
return Flux.just(new ArrayList<>());
})).onErrorResume(error -> {
logException(error);
return Flux.just(new ArrayList<>());
});
}
4.5 CachingServiceInstanceListSupplier
从 CachingServiceInstanceListSupplier
构造方法中看到了,会有缓存数据的操作,会有一个 CacheManager
的方法,而 CacheManager
主要实现是 LoadBalancerCacheManager
接口,默认实现是 DefaultLoadBalancerCacheManager
对象
public CachingServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, CacheManager cacheManager) {
// 调用父类接口
super(delegate);
// 创建一个缓存的函数式组件,通过CacheManager获取对应的Cache
this.serviceInstances = CacheFlux.lookup(key -> {
// TODO: configurable cache name
Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
if (cache == null) {
return Mono.empty();
}
List<ServiceInstance> list = cache.get(key, List.class);
//缓存未命中
if (list == null || list.isEmpty()) {
return Mono.empty();
}
return Flux.just(list).materialize().collectList();
//如果缓存获取不到,就调用 DiscoveryClientServiceInstanceListSupplier 远程获取 Eureka注册中心服务实例
}, delegate.getServiceId()).onCacheMissResume(delegate.get().take(1))
.andWriteWith((key, signals) -> Flux.fromIterable(signals).dematerialize().doOnNext(instances -> {
Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
if (cache == null) {
//找不到不做任何操作
}
else {
//如果远程调用找到服务实例,那时候就更新缓存
cache.put(key, instances);
}
}).then());
}
DefaultLoadBalancerCacheManager
public class DefaultLoadBalancerCacheManager implements LoadBalancerCacheManager {
// 初始化缓存数据结构
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
// 根据spring.cloud.loadbalancer.cache配置文件和缓存名称进行初始化缓存
public DefaultLoadBalancerCacheManager(LoadBalancerCacheProperties loadBalancerCacheProperties,
String... cacheNames) {
cacheMap.putAll(createCaches(cacheNames, loadBalancerCacheProperties).stream()
.collect(Collectors.toMap(DefaultLoadBalancerCache::getName, cache -> cache)));
}
private Set<DefaultLoadBalancerCache> createCaches(String[] cacheNames,
LoadBalancerCacheProperties loadBalancerCacheProperties) {
//根据缓存组名称进行初始化缓存
//初始化缓存容量为256,
//缓存过期时间为 35S
return Arrays.stream(cacheNames).distinct()
.map(name -> new DefaultLoadBalancerCache(name,
new ConcurrentHashMapWithTimedEviction<>(loadBalancerCacheProperties.getCapacity(),
new DelayedTaskEvictionScheduler<>(aScheduledDaemonThreadExecutor())),
loadBalancerCacheProperties.getTtl().toMillis(), false))
.collect(Collectors.toSet());
}
}
从这可以看到在创建 DefaultLoadBalancerCacheManager
时就会去根据 LoadBalancerCacheProperties
配置缓存参数,其主要的就是缓存的过期时间,从 org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheProperties#ttl
字段能看到,默认的缓存过期时间是 35s
##5、 根据 ServiceInstanceListSupplier 去获取服务实例信息
上面我们讲完了 ServiceInstanceListSupplier
的初始化,然后就可以根据我们初始化的 Supplier
来进行查询相应的服务详情信息。
@Override
// see original
// https://github.com/Netflix/ocelli/blob/master/ocelli-core/
// src/main/java/netflix/ocelli/loadbalancer/RoundRobinLoadBalancer.java
public Mono<Response<ServiceInstance>> choose(Request request) {
// 获取到的是 CachingServiceInstanceListSupplier 函数对象
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
//从CachingServiceInstanceListSupplier获取服务实例,先从缓存获取,
//缓存获取不到就从DiscoveryClientServiceInstanceListSupplier获取Eureka服务实例注册表数据
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()) {
return new EmptyResponse();
}
// TODO: enforce order? 计算lun'x
int pos = Math.abs(this.position.incrementAndGet());
ServiceInstance instance = instances.get(pos % instances.size());
return new DefaultResponse(instance);
}
根据上面的剖析,我们再看这段代码就很明白了,直接从缓存获取,如果缓存获取不到,那么就从 Eureka
注册中心获取,获取到列表集合之后,默认是轮训请求,根据服务实例的个数进行计算得到一个正确的服务实例,进行返回。
6、LoadBalancerRequest 执行请求
根据上面的处理,我们得到了一个正确的 ServiceInstance
,然后拿到这个实例数据调用 LoadBalancerRequest
去请求对应的接口方法
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request)
throws IOException {
// 创建一个想响应对象
DefaultResponse defaultResponse = new DefaultResponse(serviceInstance);
// 根据请求构造一个RequestAdapter,并且循环调用 onStartRequest方法,为请求
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = getSupportedLifecycleProcessors(serviceId);
Request lbRequest = request instanceof Request ? (Request) request : new DefaultRequest<>();
supportedLifecycleProcessors
.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, new DefaultResponse(serviceInstance)));
try {
// 使用 LoadBalancerRequest去调用请求http服务接口
T response = request.apply(serviceInstance);
//处理 ClientResponse
Object clientResponse = getClientResponse(response);
//标记当前请求成功
supportedLifecycleProcessors
.forEach(lifecycle -> lifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS,
lbRequest, defaultResponse, clientResponse)));
return response;
}
。。。
}
7、总结
根据以上的流程分析,基本上把 spring-cloud-loadbalance
组件的运行流程最主要的部分已经全部分析完了。当然了,负载均衡不仅仅只有轮训一种,但是轮训是使用的最多的,如果想修改负载均衡策略,只需要在 SpringBoot
的配置文件中修改 spring.cloud.loadbalancer.configurations
配置参数即可。
我根据以上源码分析做了一个流程图,以供大家更好的理解。