负载均衡器的源码分析及自定义LoadBalancer负载均衡算法

655 阅读7分钟

源码地址

本章将继续来讲解微服务架构中负载均衡器的相关知识点,主要知识点如下:

  1. 结合源码来分析负载均衡器的原理,主要解释它到底是怎么起作用的,以及它都做了些什么。
  2. 实现一个自定义的负载均衡算法并进行测试。

Spring Cloud LoadBalancer 源码解析

“问题不清晰,看源码分析。”

LoadBalancer 自动配置源码分析

在编码代码时仅仅是添加了 spring-cloud-starter-loadbalancer 依赖和一个 @LoadBalance 的注解,负载均衡器就生效了。毋庸置疑,肯定是 Spring Boot 框架的自动装配(Auto Configuration)机制生效了。

本章节介绍的 LoadBalancer 自动配置类是 org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration,其源码在 spring-cloud-commons-3.1.1.jar 中,如下图所示。

有一个与 LoadBalancerAutoConfiguration 同名的类,不过类路径不同,读者要注意区分。

LoadBalancerAutoConfiguration 自动配置类的定义和源码注释如下:

package org.springframework.cloud.client.loadbalancer;


@Configuration(proxyBeanMethods = false)

@ConditionalOnClass(RestTemplate.class)

@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerClientsProperties.class)
public class LoadBalancerAutoConfiguration {
	...已省略部分代码
        
	@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);
			};
		}

	}
}


由源码可知,LoadBalancerAutoConfiguration 自动配置类的生效条件有两个。

首先是当前项目的 classpath 下存在 RestTemplate 这个类。由于在 pom.xml 文件中引入了 spring-boot-starter-web 依赖,而 spring-boot-starter-web 依赖中包括了 spring-web.jar,RestTemplate 类就在 spring-web.jar 中定义,因此该条件会生效。

另外一个条件是当前 IOC 容器中存在 LoadBalancerClient 类型的 Bean。LoadBalancerClient 是一个接口,其唯一的实现类为 org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient 类。而 BlockingLoadBalancerClient 类也有一个自动配置类 BlockingLoadBalancerClientAutoConfiguration,在 BlockingLoadBalancerClientAutoConfiguration 类定义上明确有一行代码:

@AutoConfigureBefore({ org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.class,
		AsyncLoadBalancerAutoConfiguration.class })


也就是说,自动配置类 BlockingLoadBalancerClientAutoConfiguration 一定会在 org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration 自动配置类之前先走完自动配置流程,然后向 IOC 容器中注册 BlockingLoadBalancerClient 类型的 Bean。所以第二个条件也会生效。

好的,继续来看看 LoadBalancerAutoConfiguration 自动配置类生效后做了哪些事情。

根据源码可知,在自动配置类生效后,会向 IOC 容器中注册一个 org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory 类型的 Bean、一个 org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor 类型的 Bean 和一个 RestTemplateCustomizer 类型的 Bean。其中 RestTemplateCustomizer 是 RestTemplate 类的定制器,也就是说,在该自动配置类生效后,会获取当前 IOC 容器中 RestTemplate 类型的 Bean,并且把一个 LoadBalancerInterceptor 类型的拦截器注入到 RestTemplate 类型的 Bean 中。

简单来说,就是 LoadBalancerAutoConfiguration 自动配置类生效后,会在 RestTemplate 工具中做一个定制化的修改,塞一个拦截器进去。

为了更直观的感受这个过程,打断点然后走一遍流程。在自定义的 RestTemplateConfig 类中的 23 行,LoadBalancerAutoConfiguration 类中的 89 行和 98 行分别打一个断点,然后以 debug 模式启动项目。

在启动过程中,程序直接在 RestTemplateConfig 类中的 23 行这个断点处停住了,如下图所示。

此时正在构造一个 RestTemplate 类型的 Bean 并将其注册到 IOC 容器中。注意此时构造的这个 Bean——RestTemplate@4735(这是笔者在分析时的结果,读者在分析时生成的 RestTemplate 内容可能不同)。接着,点击 Resume Program 按钮跳过这个断点,程序会分别在 LoadBalancerAutoConfiguration 类中 89 行和 98 行的断点处停住。

如上图图所示,在 98 行的这行代码执行时就会向 IOC 容器中 RestTemplate 类型的 Bean 中添加一个拦截器,而此时获取到的 RestTemplate 类型的 Bean 就是自定义 RestTemplateConfig 类中构造的那个 Bean——RestTemplate@4735。也就是说,本来 RestTemplate 类是好好的,但是 LoadBalancer 集成进来之后,又把它给改造了,往里面加了一个拦截器,拦截器中有一个重要的类 BlockingLoadBalancerClient

此时 RestTemplate 类的内心 OS 应该是下面这句话:

纵使我还是我,我已不是我。

好的,以上就是 LoadBalancer 自动配置时的源码分析,主要包括自动配置类的生效条件解释,以及自动配置类生效之后做了哪些事情,读者可以根据章节中的分析过程来理解和实践。

最后需要注意,LoadBalancer 自动配置类不止本章节中提到的 LoadBalancerAutoConfiguration 这一个。如果未使用 RestTemplate 工具,而是使用了 Web Flux 的 WebClient 工具,它的自动配置类是另外一个。当前的自动配置类就不会生效了,篇幅原因就不再拓展了,感兴趣的可以自行去看,其过程和作用是类似的。

发起请求的源码分析

当然,前文中讲到的源码和作用,是项目启动时就做好的事情,更像是一个准备过程。什么意思呢?就是项目启动时,RestTemplateConfig 类的实例对象已经构造好了随时可以使用,而且它还被 LoadBalancer 自动配置类改造过了,里面多了一个拦截器 LoadBalancerInterceptor,拦截器中所需的 BlockingLoadBalancerClient 类型的实例对象也构造好了可以随时使用。但是,这些对象都在 IOC 容器中 stand by,并没有开始真正工作。

想要它们开始工作并且弄懂它们是怎么工作的,就需要解答下面的这三个问题:

  1. RestTemplate 中没有拦截器时是怎么工作的?

  2. 被定制化后的 RestTemplate 的对象又是怎么进入拦截器逻辑的?

  3. 拦截器中请求的发起流程是什么样子的?

接下来笔者就结合源码,来一一解答这些问题。

RestTemplate 中没有拦截器时是怎么工作的?

RestTemplate 类中的 getForObject() 方法源码如下:

public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
    RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
    HttpMessageConverterExtractor<T> responseExtractor =
        new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
    return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);
}


发起请求后,最终会调用 org.springframework.http.client.InterceptingClientHttpRequest 类中的 execute() 方法,源码及注释如下:

public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
    
    if (this.iterator.hasNext()) {
        ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
        
        return nextInterceptor.intercept(request, body, this);
    }
    else {
        
        HttpMethod method = request.getMethod();
        Assert.state(method != null, "No standard HTTP method");
        ClientHttpRequest delegate = 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();
    }
}


在 execute() 方法中,会判断是否存在拦截器,如果有拦截器,就执行其拦截器中的 intercept() 方法。如果没有拦截器,直接构造 ClientHttpRequest 对象然后发起请求了。比如,第 9 章服务通信基础中的代码案例,当时并没有引入微服务架构中的套件或者组件。就是在没有拦截器的情况下,直接发起的 HTTP 请求,这种过程比较简单。

被定制化后的 RestTemplate 的对象又是怎么进入拦截器逻辑的?

通过刚刚的源码分析知道了 execute() 方法中会判断是否存在拦截器。在引入了微服务架构中的组件后,LoadBalancer 自动配置生效时会往 RestTemplate 类型的实例中注入了 LoadBalancerInterceptor 拦截器的,所以就会进入拦截器中的 intercept(),此时的请求处理就由这个拦截器来接管了。

为了更清楚的看懂这个流程,可以在 org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor 类的 53 行打一个断点,并以 debug 模式启动项目。然后访问如下地址:

http://localhost:8105/consumerTest

请求发出后程序就会在这个断点出停住,如下图所示。

在图中可以看到完整的方法调用链。

一切都由下方这行编写的代码开始:

return restTemplate.getForObject(SERVICE_URL + "/goodsServiceTest", String.class);


然后进入 restTemplate.getForObject() 方法中,进入到 InterceptingClientHttpRequest 类中的 execute() 方法后会判断是否存在拦截器。现在是存在拦截器的,所以就执行到了 LoadBalancerInterceptor 类中的 intercept() 方法,源码如下:

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));
}


拦截器中处理请求的流程是什么样子的?

引入负载均衡器后,请求的处理最终会交由 BlockingLoadBalancerClient 类来处理。对于 BlockingLoadBalancerClient 类,读者应该不会感到陌生,在服务发现的源码分析中有提到这个类。源码及源码注释如下:

public class BlockingLoadBalancerClient implements LoadBalancerClient {

    ...已省略部分代码
    
	private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory;

	@Override
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
		String hint = getHint(serviceId);
		LoadBalancerRequestAdapter<T, DefaultRequestContext> lbRequest = new LoadBalancerRequestAdapter<>(request,
				new DefaultRequestContext(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);
	}

	@Override
	public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request)
			throws IOException {
		DefaultResponse defaultResponse = new DefaultResponse(serviceInstance);
		Set<LoadBalancerLifecycle> supportedLifecycleProcessors = getSupportedLifecycleProcessors(serviceId);
		Request lbRequest = request instanceof Request ? (Request) request : new DefaultRequest<>();
		supportedLifecycleProcessors
				.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, new DefaultResponse(serviceInstance)));
		try {
      
			T response = request.apply(serviceInstance);
			Object clientResponse = getClientResponse(response);
			supportedLifecycleProcessors
					.forEach(lifecycle -> lifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS,
							lbRequest, defaultResponse, clientResponse)));
			return response;
		}
		catch (IOException iOException) {
			supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
					new CompletionContext<>(CompletionContext.Status.FAILED, iOException, lbRequest, defaultResponse)));
			throw iOException;
		}
		catch (Exception exception) {
			supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
					new CompletionContext<>(CompletionContext.Status.FAILED, exception, lbRequest, defaultResponse)));
			ReflectionUtils.rethrowRuntimeException(exception);
		}
		return null;
	}
    
    
   	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();
	}
    
}


通过源码可知,这个类的主要作用就是获取一个负载均衡器,然后调用负载均衡器中的方法获取一个可用的实例,之后调用 LoadBalancerRequestFactory 类中的 createRequest() 方法创建请求对象并根据已知的实例信息获取到真实的请求地址,然后发起请求并接收响应结果。

负载均衡器的源码分析

Spring Cloud LoadBalancer 内置了两个负载均衡器,都实现自 ReactorServiceInstanceLoadBalancer 接口,分别使用了轮询算法和随机算法。默认采用的是使用了轮询算法的 RoundRobinLoadBalancer 类,源码及源码注释如下:

package org.springframework.cloud.loadbalancer.core;

public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    ...已省略部分代码
        
	@SuppressWarnings("rawtypes")
	@Override
	public Mono<Response<ServiceInstance>> choose(Request request) {
        
		ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
				.getIfAvailable(NoopServiceInstanceListSupplier::new);
        
		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()) {
			if (log.isWarnEnabled()) {
				log.warn("No servers available for service: " + serviceId);
			}
			return new EmptyResponse();
		}
        
        
        
		int pos = Math.abs(this.position.incrementAndGet());

		ServiceInstance instance = instances.get(pos % instances.size());

		return new DefaultResponse(instance);
	}

}


这个负载均衡器实现很简单,有一个 AtomicInteger 原子类型的 position 变量,从 ServiceInstanceListSupplier 中读取所有可用的实例列表,然后对 position 原子加 1,对列表大小取模,返回列表中这个位置的服务实例 ServiceInstance,非常标准的轮询算法。

自定义负载均衡算法

接下来,笔者接结合实际的编码实现一个自定义的负载均衡算法,并使用该负载均衡器来测试负载均衡的效果。

首先,在 ltd.newbee.cloud 包下新建 balancer 包,然后新建 NewBeeCloudLoadBalancer 类。注意,该类一定要实现 ReactorServiceInstanceLoadBalancer 接口。源码如下:

package ltd.newbee.cloud.balancer;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class NewBeeCloudLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    private String serviceName;

    public NewBeeCloudLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceName) {
        this.serviceName = serviceName;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    }

    private AtomicInteger atomicCount = new AtomicInteger(0);

    private AtomicInteger atomicCurrentIndex = new AtomicInteger(0);

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable();
        return supplier.get().next().map(this::getInstanceResponse);
    }


    
    private Response<ServiceInstance> getInstanceResponse(
            List<ServiceInstance> instances) {
        ServiceInstance serviceInstance = null;

        if (instances.isEmpty()) {
            System.out.println("注册中心无可用实例:" + serviceName);
            return new EmptyResponse();
        }

        
        int requestNumber = atomicCount.incrementAndGet();

        
        if (requestNumber < 2) {
            serviceInstance = instances.get(atomicCurrentIndex.get());
        } else {
            
            atomicCount = new AtomicInteger(0);

            
            atomicCurrentIndex.incrementAndGet();

            if (atomicCurrentIndex.get() >= instances.size()) {
                atomicCurrentIndex = new AtomicInteger(0);
                serviceInstance = instances.get(instances.size() - 1);
                return new DefaultResponse(serviceInstance);
            }
            
            serviceInstance = instances.get(atomicCurrentIndex.get() - 1);
        }
        return new DefaultResponse(serviceInstance);
    }

    @Override
    public Mono<Response<ServiceInstance>> choose() {
        return ReactorServiceInstanceLoadBalancer.super.choose();
    }
}


自定义的负载均衡算法与轮询算法类似,不过并不是执行一次请求就使用下一个实例。而是每个实例先执行 2 次才继续轮询到下一个实例,该值由 atomicCount 变量控制。

自定义的负载均衡算法编写完成后,还需要做一次配置才能使用。在 config 包中新建配置类 NewBeeCloudLoadBalancerConfiguration,源码如下:

package ltd.newbee.cloud.config;

import ltd.newbee.cloud.balancer.NewBeeCloudLoadBalancer;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

public class NewBeeCloudLoadBalancerConfiguration {

    @Bean
    public ReactorLoadBalancer<ServiceInstance> customLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {

        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);

        return new NewBeeCloudLoadBalancer(loadBalancerClientFactory.getLazyProvider(name,
                ServiceInstanceListSupplier.class), name);
    }
}


最后,需要在启动类上添加一个 @LoadBalancerClient 注解,将该配置类和目标服务做好关联。启动类改动如下:

@SpringBootApplication
@EnableDiscoveryClient

@LoadBalancerClient(value = "newbee-cloud-goods-service",configuration = NewBeeCloudLoadBalancerConfiguration.class)
public class LoadBalancerApplication {

    public static void main(String[] args) {
        SpringApplication.run(LoadBalancerApplication.class, args);
    }

}


最后,重启项目并测试效果。当然,读者如果有任何问题或者想要和笔者讨论的内容,都可以在评论区留下看法,笔者会根据读者的反馈和问题继续整理和完善本章节内容。

原文地址 juejin.cn