万字深度解析:spring cloud负载均衡是怎么实现的

1、什么是负载均衡

负载均衡,它的含义简单理解就是当访问一个应用集群的时候,尽可能使请求均匀地落到集群中的某一台机器中,不至于使某一台机器的接收的请求过多。也即是说,因为先有了集群,所以才会有负载均衡这个概念。当访问单机的服务时,谈不上负载均衡。负载均衡,又分为客户端负载均衡和服务端负载均衡。

(1)客户端负载均衡

客户端负载均衡指的是当客户端要调用远程服务的时候,自己决定要调用远程服务中的哪一台机器。当客户端能够获取到远程服务的所有ip和对应的port信息的时候,它就可以使用某种负载均衡算法,从这些ip中选择一个ip(也包括对应的端口号),然后对选中的ip上的服务直接发起调用。

(2)服务端负载均衡

服务端负载均衡指的是客户端已经把请求发出来,服务端来决定将请求转发到集群中的某台机器上。我们常用的nginx就承担了这种职责,我们直接访问nginx的ip和应用的路径,nginx根据配置的应用的转发信息,将请求转发到后端的某台机器。

spring cloud官方提供了它自己的客户端负载均衡的抽象和实现,本篇我们就来看下它的实现原理。

2、前期回顾

我们在之前的文章中谈到在spring cloud中使用nacos服务注册和发现功能的时候,pom文件中引入了如下的依赖:

 

xml

复制代码

       <dependency>             <groupId>com.alibaba.cloud</groupId>             <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>         </dependency>

然后我们又写了如下的代码:

 

less

复制代码

@Configuration public class BeanConfig {     @LoadBalanced     @Bean     public RestTemplate restTemplate(){         return new RestTemplate();     } }

创建restTemplate实例的时候使用了@LoadBalanced注解,然后我们就可以直接使用容器中的restTemplate实例以下面的代码调用目标服务:

 

arduino

复制代码

//此处直接写服务名称provider,不需要指定端口号 return result = restTemplate.getForObject("http://provider/hello",String.class);

为什么我们这么写就能访问到目标服务呢?这里面的原理到底是啥?其实这里的restTemplate还是很普通的RestTemplate对象,只不过它比我们平时直接用@Bean注解生成的restTemplate要多了一点属性。我们先对这块的功能做下概述:

 

arduino

复制代码

1、整个getForObject方法调用的过程中会调用到负载均衡拦截器对象LoadBalancerInterceptor。 2、负载均衡拦截器对象LoadBalancerInterceptor又会调用BlockingLoadBalancerClient对象来处理请求。 3、BlockingLoadBalancerClient在执行过程中会先解析provider服务对应的所有的ip和port信息,这块就涉及到服务发现的客户端从服务注册中心读取服务实例列表,以我们当前例子为例,就是nacos客户端会去nacos服务器查询provider服务下面到底有哪些机器信息,会把它们在nacos服务器上注册的ip和port等信息都读取到本地。 4、默认的基于轮询算法的负载均衡器RoundRobinLoadBalancer会根据自己的算法从这些服务实例列表中选择一个实例,返回给BlockingLoadBalancerClient。 5、BlockingLoadBalancerClient提取服务实例对象里面的信息(ip和port)用来替换原来请求路径中的服务名,比如将provider替换成192.168.1.1:8081,那么此时完整的url就是http://192.168.1.1:8081/hello,最后就直接调用该接口访问目标服务器上的数据

上面是一个大概的请求执行过程,一个小小的restTemplate对象之所以能拥有负载均衡和服务发现的能力,和@LoadBalanced这个注解有很大的关系。接下来我们就来分析下这里面涉及到的对象都是哪里来的。

3、原理讲解

我们pom文件中引入的spring-cloud-starter-alibaba-nacos-discovery包会自动导入spring-cloud-commons包和spring-cloud-loadbalancer包,进而又导入了一堆自动配置类。spring-cloud-loadbalancer包是spring cloud官方提供的负载均衡的实现。

其实负载均衡组件存在的意义(功能),说点大白话,就是要从一堆服务器中选一个服务器出来,然后再访问这台被选中的服务器。我们可以把这一堆服务器比作是一个服务列表提供者对象,我们能从它里面直接拿到这些服务器的信息。而我们本文用到的nacos服务发现组件,其实也算是一个服务列表提供者对象,因为我们也可以从它里面获取到nacos远程服务上存储的关于某个服务名称下面的服务器(实例)信息。因此,我们在此处结合前面的例子分析spring cloud负载均衡功能的时候,也会涉及到nacos服务发现相关的功能。负载均衡组件要操作的对象就是nacos服务发现组件从nacos服务器上读取到的指定服务名下面的所有服务实例信息。

3.1、@LoadBalanced注解

首先我们先来看下@LoadBalanced注解,它位于Spring cloud commons包,有如下的定义:

 

less

复制代码

/**  * Annotation to mark a RestTemplate or WebClient bean to be configured to use a  * LoadBalancerClient.  * @author Spencer Gibb  */ @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Qualifier public @interface LoadBalanced { }

它本身很简单,@Qualifier是它的元注解,所以它本身就是被当做一个标记使用。那么谁会来解析(利用)这个标记呢?就是下面的LoadBalancerAutoConfiguration配置类。

3.2、LoadBalancerAutoConfiguration配置类

LoadBalancerAutoConfiguration位于spring-cloud-commons包,它有如下的定义:

 

java

复制代码

public class LoadBalancerAutoConfiguration {     @LoadBalanced     @Autowired(required = false)     private List<RestTemplate> restTemplates = Collections.emptyList();     @Autowired(required = false)     private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();     @Bean     public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(             final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {         return () -> restTemplateCustomizers.ifAvailable(customizers -> {             for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {                 for (RestTemplateCustomizer customizer : customizers) {                     customizer.customize(restTemplate);                 }             }         });     }     @Bean     @ConditionalOnMissingBean     public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {         return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);     }     @Configuration(proxyBeanMethods = false)     @Conditional(RetryMissingOrDisabledCondition.class)     static class LoadBalancerInterceptorConfig {         @Bean         public LoadBalancerInterceptor loadBalancerInterceptor(LoadBalancerClient loadBalancerClient,                 LoadBalancerRequestFactory requestFactory) {             return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);         }         @Bean         @ConditionalOnMissingBean         public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {             return restTemplate -> {                 List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());                 list.add(loadBalancerInterceptor);                 restTemplate.setInterceptors(list);             };         }     }    //省略其他代码  }

我们来对这个配置类做下拆解。

3.2.1、配置类的restTemplates字段

这个配置类有这个字段声明:

 

less

复制代码

    @LoadBalanced     @Autowired(required = false)     private List<RestTemplate> restTemplates = Collections.emptyList();

它的意思就是注入spring容器中所有标记了@LoadBalanced注解的restTemplate对象,如果我们在之前的BeanConfig配置类增加如下代码:

 

typescript

复制代码

    @Bean     public RestTemplate restTemplate1(){         return new RestTemplate();     }

那这个restTemplate1对象是不会被注入到restTemplates列表中的,restTemplates列表中依然只有一个对象。这就是@LoadBalanced注解的作用,它只是被用作一个标记,来标记哪些对象可以为我所用。关于@Qualifier注解相关的具体解析,大家感兴趣的话可以先查看spring源码中ContextAnnotationAutowireCandidateResolver类的父类QualifierAnnotationAutowireCandidateResolver中的内容。

3.2.2、配置类的内部类LoadBalancerInterceptorConfig

LoadBalancerInterceptorConfig是LoadBalancerAutoConfiguration的内部类,同时它也是一个配置类。从它的名字我们可以看出,它是一个负载均衡拦截器配置类。该配置类有如下的作用:

 

复制代码

(1)生成LoadBalancerInterceptor拦截器对象 (2)生成RestTemplateCustomizer对象,并将LoadBalancerInterceptor对象设置到restTemplate对象的  Interceptors属性中。

其实我们的restTemplate对象并没用到代理的技术,它就是比我们平常使用的restTemplate对象多了一个负载均衡的拦截器。那这个负载均衡拦截器是啥时候被设置到restTemplate对象中的呢?

3.2.3、SmartInitializingSingleton实例对象

LoadBalancerAutoConfiguration配置类还定义了一个bean对象,名字叫loadBalancedRestTemplateInitializerDeprecated,它是SmartInitializingSingleton类型的。那SmartInitializingSingleton又是个啥呢?

我们来看下它的定义:

 

vbnet

复制代码

public interface SmartInitializingSingleton {     /**      * Invoked right at the end of the singleton pre-instantiation phase,      * with a guarantee that all regular singleton beans have been created      * already. {@link ListableBeanFactory#getBeansOfType} calls within      * this method won't trigger accidental side effects during bootstrap.      * <p><b>NOTE:</b> This callback won't be triggered for singleton beans      * lazily initialized on demand after {@link BeanFactory} bootstrap,      * and not for any other bean scope either. Carefully use it for beans      * with the intended bootstrap semantics only.      */     void afterSingletonsInstantiated(); }

它内部只有一个抽象方法,SmartInitializingSingleton类型的bean对象会在spring初始化初始化bean的阶段结束时被调用。

spring容器启动的时候会调用AbstractApplicationContext的refresh方法,该方法内部有如下代码:

 

scss

复制代码

            // Initialize event multicaster for this context.                 initApplicationEventMulticaster();                 // Initialize other special beans in specific context subclasses.                 onRefresh();                 // Check for listener beans and register them.                 registerListeners();                 // Instantiate all remaining (non-lazy-init) singletons.                 finishBeanFactoryInitialization(beanFactory);                 // Last step: publish corresponding event.                 finishRefresh();

而finishBeanFactoryInitialization方法内部又会执行如下代码:

 

scss

复制代码

// Instantiate all remaining (non-lazy-init) singletons. beanFactory.preInstantiateSingletons();

此处的preInstantiateSingletons方法会确保所有的非懒加载的bean对象都会被实例化。而preInstantiateSingletons方法内部在初始化所有的非懒加载的bean之后就会执行如下代码:

 

scss

复制代码

// Trigger post-initialization callback for all applicable beans...         for (String beanName : beanNames) {             Object singletonInstance = getSingleton(beanName);             if (singletonInstance instanceof SmartInitializingSingleton) {                 StartupStep smartInitialize = this.getApplicationStartup().start("spring.beans.smart-initialize")                         .tag("beanName", beanName);                 SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;                 if (System.getSecurityManager() != null) {                     AccessController.doPrivileged((PrivilegedAction<Object>) () -> {                         smartSingleton.afterSingletonsInstantiated();                         return null;                     }, getAccessControlContext());                 }                 else {                     //重点,触发回调                     smartSingleton.afterSingletonsInstantiated();                 }                 smartInitialize.end();             }         }

上面这段代码的意思就是:循环spring容器中的bean对象,如果当前bean对象是SmartInitializingSingleton类型的,就执行它的afterSingletonsInstantiated方法。

那么到这里我们已经知道SmartInitializingSingleton被调用的地方了,咱们再回到LoadBalancerAutoConfiguration配置类。它是这么创建SmartInitializingSingleton对象的:

 

kotlin

复制代码

   @Bean     public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(             final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {         return () -> restTemplateCustomizers.ifAvailable(customizers -> {             for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {                 for (RestTemplateCustomizer customizer : customizers) {                     customizer.customize(restTemplate);                 }             }         });     }

它内部会针对每个标记了@LoadBalanced注解的restTemplate对象挨个调用spring容器中的RestTemplateCustomizer对象的customize方法。我们在3.2.2部分提到的RestTemplateCustomizer对象,就是在此处执行的。也就是说,当SmartInitializingSingleton实例对象的afterSingletonsInstantiated方法被调用后,我们的restTemplate对象的拦截器列表中就多了一个LoadBalancerInterceptor对象。

3.3、spring-cloud-loadbalancer包的配置类

spring-cloud-loadbalancer包是spring cloud官方提供的客户端负载均衡的实现,我们需要重点关注下这个包的自动配置类。

3.3.1、BlockingLoadBalancerClientAutoConfiguration配置类

这个配置类定义了LoadBalancerClient类型的bean信息,真实类型是BlockingLoadBalancerClient,后面负载均衡拦截器执行的时候会用到它。

 

less

复制代码

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

3.3.2、LoadBalancerClientConfiguration配置类

这个配置类自动配置了如下的两个bean,RoundRobinLoadBalancer和ServiceInstanceListSupplier:

 

java

复制代码

    @Bean     @ConditionalOnMissingBean     public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,             LoadBalancerClientFactory loadBalancerClientFactory) {         String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);         return new RoundRobinLoadBalancer(                 loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);     }      //省略部分代码       //但是这个bean对象不会在这里生成,当我们集成了nacos的服务发现时,会由nacos的自动配置类生成该对象。@Order注解会控制bean的加载顺序         @Bean         @ConditionalOnBean(DiscoveryClient.class)         @ConditionalOnMissingBean         @Conditional(DefaultConfigurationCondition.class)         public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(                 ConfigurableApplicationContext context) {             return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withCaching().build(context);         }

3.4、spring-cloud-starter-alibaba-nacos-discovery包的配置类

nacos服务发现的自动配置包主要就是用来提供一些服务发现的组件对象,并将其注册到spring容器中,供前面的loadbalancer包中的组件对象使用。

3.4.1、NacosLoadBalancerClientConfiguration配置类

NacosLoadBalancerClientConfiguration位于包中,它提供ServiceInstanceListSupplier类型的bean对象,简单来说,它就是一个服务列表提供者对象,而该对象的真实类型是DiscoveryClientServiceInstanceListSupplier:

 

less

复制代码

       @Bean         @ConditionalOnBean(DiscoveryClient.class)         @ConditionalOnMissingBean         @ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "default", matchIfMissing = true)         public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(                 ConfigurableApplicationContext context) {             return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient()                     .build(context);         }

3.4.2、NacosDiscoveryClientConfiguration配置类

它提供了DiscoveryClient的实例,它会和nacos服务器交互,能根据指定的服务名称去nacos服务器上查询该服务名下面所有的服务实例信息。上面的ServiceInstanceListSupplier实例内部会用到它。

 

typescript

复制代码

   @Bean     public DiscoveryClient nacosDiscoveryClient(             NacosServiceDiscovery nacosServiceDiscovery) {         return new NacosDiscoveryClient(nacosServiceDiscovery);     }

3.5、RestTemplate对象的执行过程

3.5.1、RestTemplate内部方法执行过程

当我们执行下面的代码时

 

arduino

复制代码

restTemplate.getForObject("http://provider/hello",String.class);

会调用InterceptingClientHttpRqeust的execute方法,该方法在其父类AbstractClientHttpRequest中:

 

kotlin

复制代码

//AbstractClientHttpRequest类 public final ClientHttpResponse execute() throws IOException {             this.assertNotExecuted();             ClientHttpResponse result = this.executeInternal(this.headers);             this.executed = true;             return result;         }

我们再来看下executeInternal方法:

 

ini

复制代码

//AbstractClientHttpRequest类 protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {         byte[] bytes = this.bufferedOutput.toByteArray();         if (headers.getContentLength() < 0L) {             headers.setContentLength((long)bytes.length);         }         ClientHttpResponse result = this.executeInternal(headers, bytes);         this.bufferedOutput = new ByteArrayOutputStream(0);         return result;     }

它内部调用了重载的executeInternal方法,而这个重载的方法是在InterceptingClientHttpRequest中实现的:

 

java

复制代码

//InterceptingClientHttpRequest类 protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {         InterceptingClientHttpRequest.InterceptingRequestExecution requestExecution = new InterceptingClientHttpRequest.InterceptingRequestExecution();         return requestExecution.execute(this, bufferedOutput);     }

这里重点就来了,InterceptingClientHttpRequest的内部类InterceptingRequestExecution的execute方法被执行:

 

ini

复制代码

public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {             if (this.iterator.hasNext()) {                 //负载均衡的拦截器在这里被调用                 ClientHttpRequestInterceptor nextInterceptor = (ClientHttpRequestInterceptor)this.iterator.next();                 return nextInterceptor.intercept(request, body, this);             } else {                 HttpMethod method = request.getMethod();                 Assert.state(method != null, "No standard HTTP method");                 ClientHttpRequest delegate = InterceptingClientHttpRequest.this.requestFactory.createRequest(request.getURI(), method);                 request.getHeaders().forEach((key, value) -> {                     delegate.getHeaders().addAll(key, value);                 });                 if (body.length > 0) {                     if (delegate instanceof StreamingHttpOutputMessage) {                         StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage)delegate;                         streamingOutputMessage.setBody((outputStream) -> {                             StreamUtils.copy(body, outputStream);                         });                     } else {                         StreamUtils.copy(body, delegate.getBody());                     }                 }                 return delegate.execute();             }         }

上面的代码就调用了我们先前提到的负载均衡过滤器LoadBalancerInterceptor,剩下的代码就交给了LoadBalancerInterceptor,由LoadBalancerInterceptor执行并将结果返回给RestTemplate。

3.5.2、LoadBalancerInterceptor执行过程

我们接着看intercept方法:

 

java

复制代码

//LoadBalancerInterceptor类 @Override     public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,             final ClientHttpRequestExecution execution) throws IOException {         final URI originalUri = request.getURI();         String serviceName = originalUri.getHost();         Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);         return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));     }

里面的loadBalancer对象就是我们在3.3部分提到的BlockingLoadBalancerClient对象。

3.5.3、BlockingLoadBalancerClient执行过程

我们继续看它的execute方法。

 

ini

复制代码

//BlockingLoadBalancerClient类 @Override     public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {         String hint = getHint(serviceId);         LoadBalancerRequestAdapter<T, TimedRequestContext> lbRequest = new LoadBalancerRequestAdapter<>(request,                 buildRequestContext(request, hint));         Set<LoadBalancerLifecycle> supportedLifecycleProcessors = getSupportedLifecycleProcessors(serviceId);         supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));         //重点,根据服务的名称从对应的服务列表中选择服务实例         ServiceInstance serviceInstance = choose(serviceId, lbRequest);         if (serviceInstance == null) {             supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(                     new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, new EmptyResponse())));             throw new IllegalStateException("No instances available for " + serviceId);         }         return execute(serviceId, serviceInstance, lbRequest);     }

里面很重要的一行代码就是:

 

ini

复制代码

ServiceInstance serviceInstance = choose(serviceId, lbRequest);

它会根据服务id(名称)选择一个可用的服务实例,供后续直接调用执行。

 

ini

复制代码

//BlockingLoadBalancerClient类  @Override     public <T> ServiceInstance choose(String serviceId, Request<T> request) {         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();     }

choose方法内部的loadBalancer对象其实就是3.3.2部分看到的RoundRobinLoadBalancer对象。

3.5.4、RoundRobinLoadBalancer对象的choose方法

 

vbscript

复制代码

public Mono<Response<ServiceInstance>> choose(Request request) {         ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider                 .getIfAvailable(NoopServiceInstanceListSupplier::new);         return supplier.get(request).next()                 .map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));     }

方法里面的supplier对象就是我们在3.4.1部分提到的服务列表提供者DiscoveryClientServiceInstanceListSupplier对象。它内部就会调用nacos的NacosDiscoveryClient对象,来从nacos服务器获取指定服务名称下面的服务实例信息。

4、小结

本篇从restTemplate对象入手,简单分析了restTemplate如何借助spring cloud官方的负载均衡组件以及nacos服务发现组件的功能来完成了请求的处理过程。但是有一点需要注意的是:如果我们传给restTemplate对象的url地址包含的是域名或者ip,那我们就不能使用这个经过负载后的restTemplate对象,而应该在配置类中创建一个普通的bean对象,上面不能加@LoadBalanced注解,比如:

 

typescript

复制代码

    @Bean     public RestTemplate restTemplate2(){         return new RestTemplate();     }

经过负载后的restTemplate只能处理带服务名称的url地址,而不能处理带域名或ip的url地址,否则就会报无法解析服务名的错误。因此,当我们系统集成服务发现组件的数据,我们系统是可能会用到多个restTemplate对象。

另外,本篇我们讲的负载均衡客户端是spring cloud官方提供的loadbalancer包,其实我们可以直接把这个包排除掉,然后使用ribbon负载均衡组件。这个替换过程很简单,只需要修改maven依赖,而不必更改我们的系统代码。之所以如此方便,也要得益于spring cloud commons包提供的各种抽象接口以及以及默认实现组件,以后我们会找时间再来聊聊这块的知识~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值