从源码角度深入解析spring cloud的负载均衡组件Ribbon

提示:本文内容较长,且涉及了很多源码,请谨慎阅读。
本文为Ribbon系列第一篇,主要分析了RestTemplate在Ribbon实现负载均衡的流程。


前言

Spring Cloud Netflix Ribbon是一种客户端负载均衡的组件。在微服务系统中,不同的微服务之间往往需要通信。例如在商城系统中,用户在访问产品微服务的时候,如果下单某个产品,那么产品微服务就需要调用订单微服务,为用户生成订单。在调用某个微服务的时候,这个微服务经常会有多个实例,如何选取其中的一个实例去调用,则需要通过负载均衡算法来实现了。这就是Ribbon的作用。


提示:以下是本篇文章正文内容

一、负载均衡概述

负载均衡分为硬件负载均衡和软件负载均衡。不过我们关心的是软件负载均衡。
负载均衡有两个需要解决的基本问题:

  • 从哪里选取服务实例。在Spirng Cloud微服务系统中,Eureka服务治理中心维护了一个微服务实例清单,具体微服务实例会执行服务获取,得到实例清单,缓存到本地,同时按照一个事件间隔更新这个实例清单(因为实例清单也在不断维护和变化)。
  • 如何选择服务实例。这个通过具体的负载均衡策略来解决。

二、关于Ribbon

Rinbbon的使用主要和RestTemplate(微服务用来调用Rest风格请求的模板,详情请见另一篇文章关于Spring中调用Rest风格请求的模板——RestTemplate)有关,此处不赘述了,本文的重点是从源码来弄清楚Ribbon的具体原理。

我们知道,在RestTemplate的类上加上@LoadBalanced,就开启了负载均衡。所以我们从这个注解开始,对Ribbon展开探究。
打开这个注解的源码,可以看到这样一个注释:

Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient

这句话的意思是加上这个注解,接口对象LoadBalancerClient就会对RestTemplate进行处理。所以我们接下来就研究LoadBalancerClient接口,打开这个接口:

public interface LoadBalancerClient extends ServiceInstanceChooser {
   <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
   <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
   URI reconstructURI(ServiceInstance instance, URI original);

可以看到这个接口继承了ServiceInstanceChooser接口,有三个方法,再查看这个接口的实现类,有两个:
在这里插入图片描述
ServiceInstanceChooser定义了一个方法,如下:

ServiceInstance choose(String serviceId);

这个方法有一个参数serviceId,这个微服务的id,实例的配置项spring.application.name,这个方法返回的是一个微服务实例。
我们现在来看上面提到的接口LoadBalancerClient(把源码再复制一遍):

public interface LoadBalancerClient extends ServiceInstanceChooser {
   <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
   <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
   URI reconstructURI(ServiceInstance instance, URI original);

因为这个接口继承了ServiceInstanceChooser接口,所以同样拥有choose方法,因此这个接口共有4个方法,现在具体分析一下上面三个方法:

  • execute(String serviceId, LoadBalancerRequest request)
    来看一下这个方法的注释:

execute request using a ServiceInstance from the LoadBalancer for the specified service

意思是用一个通过负载均衡得到的服务实例来执行请求。

  • execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request)
    这个方法的注释和上个方法一样,不过两个方法的参数不同,此方法多了一个ServiceInstance serviceInstance。
  • URI reconstructURI(ServiceInstance instance, URI original),此方法的注释:

Create a proper URI with a real host and port for systems to utilize.Some systems use a URI with the logical serivce name as the host, such as http://myservice/path/to/service. This will replace the service name with the host:port from the ServiceInstance.

这段读起来有点拗口,通俗点说就是原来的url请求里面写的是微服务名字,这个方法在得到具体实例的信息以后,会对原来的url进行重写,将微服务名字换成具体实例的主机和端口号。

下面我们来看看上面的两个execute方法的具体实现,前面我们说过,LoadBalancerClient有两个实现类,现在看一下第2个RibbonLoadBalancerClient,我们先来看看第一个execute方法在这个类中的实现:

@Override
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request)
			throws IOException {
		return execute(serviceId, request, null);
	}

可以看到这个方法只是调用了另一个execute方法,我们来看一下这个被调用的execute方法:

public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
			throws IOException {
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		Server server = getServer(loadBalancer, hint);
		if (server == null) {
			throw new IllegalStateException("No instances available for " + serviceId);
		}
		RibbonServer ribbonServer = new RibbonServer(serviceId, server,
				isSecure(server, serviceId),
				serverIntrospector(serviceId).getMetadata(server));

		return execute(serviceId, ribbonServer, request);
	}

这个方法在接口中没有,相比上个execute多了个一个参数Object hint。
首先通过微服务seviceId获取负载均衡器loadBalancer,然后通过负载均衡器和hint获得具体的微服务实例,如果实例为空,抛出异常,然后新建一个RibbonServer对象,将实例信息包装到这个对象里,再调用最后一个execute方法来执行请求。
我们现在来看一下最后一个execute方法,如下:

@Override
	public <T> T execute(String serviceId, ServiceInstance serviceInstance,
			LoadBalancerRequest<T> request) throws IOException {
		Server server = null;
		if (serviceInstance instanceof RibbonServer) {
			server = ((RibbonServer) serviceInstance).getServer();
		}
		if (server == null) {
			throw new IllegalStateException("No instances available for " + serviceId);
		}

		RibbonLoadBalancerContext context = this.clientFactory
				.getLoadBalancerContext(serviceId);
		RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);

		try {
			T returnVal = request.apply(serviceInstance);
			statsRecorder.recordStats(returnVal);
			return returnVal;
		}
		// catch IOException and rethrow so RestTemplate behaves correctly
		catch (IOException ex) {
			statsRecorder.recordStats(ex);
			throw ex;
		}
		catch (Exception ex) {
			statsRecorder.recordStats(ex);
			ReflectionUtils.rethrowRuntimeException(ex);
		}
		return null;
	}

首先获取服务实例,然后创建结果分析器,再调用request的apply方法处理请求,最后将处理结果放入结果分析器中,返回结果。

我们从来没有创建过LoadBalancerClient接口对象的实例,却能够实现负载均衡的效果,说明了Spring Boot为我们自动装配了相关的对象,打开类RibbonAutoConfiguration,就可以看到创建对象的代码,如下:

@Bean
	@ConditionalOnMissingBean(LoadBalancerClient.class)
	public LoadBalancerClient loadBalancerClient() {
		return new RibbonLoadBalancerClient(springClientFactory());
	}

看到这里,请大家思考一个问题,为什么在RestTemplate上加上注解,就能实现负载均衡的效果呢,又是如何找到那三个execute方法的呢?答案是使用了拦截器LoadBanlancerInterceptor,我们来看看这个拦截器的代码,如下:

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

	private LoadBalancerClient loadBalancer;
	private LoadBalancerRequestFactory requestFactory;

	public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
		this.loadBalancer = loadBalancer;
		this.requestFactory = requestFactory;
	}

	public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
		// for backwards compatibility
		this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
	}

	@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
			final ClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI();
		String serviceName = originalUri.getHost();
		return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
	}
}

这段代码并不复杂,核心是intercept方法,而intercept方法又调用了LoadBalancerClient对象的execute方法。看到这儿我们就明白了,只要在RestTemplate上加上@LoadBalanced,这个拦截器就会起作用,就会找到LoadBalancerClient对象的execute方法去执行。
但是,拦截器LoadBanlancerInterceptor的对象又是如何创建的呢?答案还是SpringBoot的自动装配,我们可以看一下LoadBalancerAutoConfiguration的代码,如下:

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

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

	@Bean
	public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
			final List<RestTemplateCustomizer> customizers) {
		return new SmartInitializingSingleton() {
			@Override
			public void afterSingletonsInstantiated() {
				for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
					for (RestTemplateCustomizer customizer : customizers) {
						customizer.customize(restTemplate);
					}
				}
			}
		};
	}

	@Autowired(required = false)
	private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

	@Bean
	@ConditionalOnMissingBean
	public LoadBalancerRequestFactory loadBalancerRequestFactory(
			LoadBalancerClient loadBalancerClient) {
		return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
	}

	@Configuration
	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
	static class LoadBalancerInterceptorConfig {
		@Bean
		public LoadBalancerInterceptor ribbonInterceptor(
				LoadBalancerClient loadBalancerClient,
				LoadBalancerRequestFactory requestFactory) {
			return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
		}

		@Bean
		@ConditionalOnMissingBean
		public RestTemplateCustomizer restTemplateCustomizer(
				final LoadBalancerInterceptor loadBalancerInterceptor) {
			return new RestTemplateCustomizer() {
				@Override
				public void customize(RestTemplate restTemplate) {
					List<ClientHttpRequestInterceptor> list = new ArrayList<>(
							restTemplate.getInterceptors());
					list.add(loadBalancerInterceptor);
					restTemplate.setInterceptors(list);
				}
			};
		}
	}

	@Configuration
	@ConditionalOnClass(RetryTemplate.class)
	static class RetryAutoConfiguration {
		@Bean
		public RetryTemplate retryTemplate() {
			RetryTemplate template =  new RetryTemplate();
			template.setThrowLastExceptionOnExhausted(true);
			return template;
		}

		@Bean
		@ConditionalOnMissingBean
		public LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory() {
			return new LoadBalancedRetryPolicyFactory.NeverRetryFactory();
		}

		@Bean
		public RetryLoadBalancerInterceptor ribbonInterceptor(
				LoadBalancerClient loadBalancerClient, LoadBalancerRetryProperties properties,
				LoadBalancedRetryPolicyFactory lbRetryPolicyFactory,
				LoadBalancerRequestFactory requestFactory) {
			return new RetryLoadBalancerInterceptor(loadBalancerClient, retryTemplate(), properties,
					lbRetryPolicyFactory, requestFactory);
		}

		@Bean
		@ConditionalOnMissingBean
		public RestTemplateCustomizer restTemplateCustomizer(
				final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
			return new RestTemplateCustomizer() {
				@Override
				public void customize(RestTemplate restTemplate) {
					List<ClientHttpRequestInterceptor> list = new ArrayList<>(
							restTemplate.getInterceptors());
					list.add(loadBalancerInterceptor);
					restTemplate.setInterceptors(list);
				}
			};
		}
	}
}

代码虽多,但我们只需要注意创建拦截器对象的部分,并不复杂。


总结

以上是Ribbon实现负载均衡的流程:首先通过LoadvBalancerInterceptor拦截RestTemplate,然后在其intercept方法调用LoadBalancerClient接口的execute方法来执行负载均衡。

关于Ribbon负载均衡器和具体的策略在下一篇文章中分析。

本文参考杨开振的《Spring Cloud微服务和分布式系统实践》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值