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包提供的各种抽象接口以及以及默认实现组件,以后我们会找时间再来聊聊这块的知识~