SpringCloud-2020.3版本 LoadBalance源码解析

SpringCloud LoadBalancer 负载均衡

SpringCloud负载均衡组件一直使用的是 Netflix-Ribbon组件,但是在 SpringCloud 2020版本以后 SpringCloud剔除掉了 出 eureka-servereureka-client 除外的所有 Netflix组件,但是官方也提供了一些替代品如下图,由此也能从中看到 Ribbon的负载均衡组件被 SpringCloud-Loadbalancer组件进行代替,这次我们就来讲一下 SpringCloud-Loadbalancer的使用流程

使用的技术栈

SpringBoot 2.4.6

SpringCloud 2020.0.3

SpringCloud-Netflix-Eureka-Server

SpringCloud-Netflix-Eureka-Client

小弟有一个开源项目,希望大家可以多多一键三连,谢谢大家

nirvana-reborn

后续的源码解析也都会进行同步更新上去

image-20210518110911185

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工厂类中其核心是调用父类的 NamedContextFactorygetInstance,在父类中会去维护一个 Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();数据结构,以 key:服务名称,value:Spring的ApplicationContext,的数据结构进行存储,而第一次服务请求时找不到对应的 ApplicationContext就会去创建一个新的 AnnotationConfigApplicationContextspring的上下文,然后后面想要获取一些别的组件全部都是通过 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对象创建了 DiscoveryClientServiceInstanceListSupplierCachingServiceInstanceListSupplier两个主要的函数式对象,从 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配置参数即可。

我根据以上源码分析做了一个流程图,以供大家更好的理解。
在这里插入图片描述

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值