负载均衡这个东西,昂相信每个软件开发者应该都接触过。spring cloud ribbon是spring cloud 对Netflix Ribbon的封装,可以让我们轻松的把REST请求转换成客户端负载均衡的服务调用。spring cloud ribbon虽然只是一个工具类的框架,但她几乎存在于每一个基于spring cloud构建的微服务里。

要想通过spring cloud ribbon实现负载均衡,非常简单,只需要两步:

  1. 服务提供者启动多个实例注册到注册中心
  2. 服务消费者直接调用被@LoadBalanced注解修饰过的RestTemplate来实现服务的调用。

非常简单,这样就能将高可用的服务提供者配合负载均衡一起实现了。

下面进入主题,RestTemplate的分析

一.API提供

  1. GET请求
    GET请求主要有两种方式:
        1.1  getForEntity   返回的是ResponseEntity<T>。该对象是Spring对HTTP请求的封装,存储了HTTP的几个重要元素,如请求状态码的枚举HttpStatus,Http请求头HttpHeaders以及泛型的请求体对象。拥有3种重载方法,具体的就不展开了
        1.2  getForObject   放回的是<T>泛型,可以理解为是对getForEntity的进一步封装。通过HttpMessageConverterExtractor对HTTP的请求响应体body进行对象转换,直接返回包装好的对象。
  2. POST请求
    POST请求有三种方法进行调用:
       2.1  postForEntity  返回的是ResponseEntity<T>,同getForEntity类似
       2.2  postForObject  放回的是<T>泛型,同getForObject类似
       3.3  postForLocation  返回的是URI,该方法实现了以POST请求提交资源,然后返回资源的URI,有3种重载方法
  3. PUT请求
    即对put方法进行调用,3种重载方法  ,都没有返回值,都没有返回值
    put(String url, @Nullable Object request, Object... uriVariables)     
    put(String url, @Nullable Object request, Map<String, ?> uriVariables)
    put(URI url, @Nullable Object request)
  4. DELETE请求
    对delete方法进行调用,3种重载方法,都没有返回值
    delete(String url, Object... uriVariables)
    delete(String url, Map<String, ?> uriVariables)
    delete(URI url)  

------------------------------------------------------------------------------------------------------------------------------------------------------

二.源码简单分析

首先 查看@LoadBalanced的代码

/**
 * Annotation to mark a RestTemplate 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 {
}

 从注释里可以看到,这个注解使用来给RestTemplate做标记,使LoadBalancerClient来配置它。

接下来,搜索LoadBalancerClient可以发现,是spring cloud定义的一个接口 

public interface LoadBalancerClient {
	
	ServiceInstance choose(String serviceId);

	<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;

	URI reconstructURI(ServiceInstance instance, URI original);

}

从spring cloud开放的接口可以看到客户端实现的负载均衡中要具备几种功能

  • 根据传入的serviceId,从负载均衡器里获取一个对应服务的实例
  • 使用serviceId挑选出来的服务实例执行请求
  • 根据传入的实例对象构建出一个合适的host:port格式的url

接下来看看为客户端负载均衡器的自动化配置类LoadBalancerAutoConfiguration,首先,看下源码

/**
 * Auto configuration for Ribbon (client side load balancing).
 *
 * @author Spencer Gibb
 * @author Dave Syer
 * @author Will Tran
 * @author Gang Li
 */
@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 loadBalancedRestTemplateInitializerDeprecated(
			final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
		return () -> restTemplateCustomizers.ifAvailable(customizers -> {
            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 restTemplate -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                        restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
		}
	}

	@Configuration
	@ConditionalOnClass(RetryTemplate.class)
	public static class RetryAutoConfiguration {

		@Bean
		@ConditionalOnMissingBean
		public LoadBalancedRetryFactory loadBalancedRetryFactory() {
			return new LoadBalancedRetryFactory() {};
		}
	}

	@Configuration
	@ConditionalOnClass(RetryTemplate.class)
	public static class RetryInterceptorAutoConfiguration {
		@Bean
		@ConditionalOnMissingBean
		public RetryLoadBalancerInterceptor ribbonInterceptor(
				LoadBalancerClient loadBalancerClient, LoadBalancerRetryProperties properties,
				LoadBalancerRequestFactory requestFactory,
				LoadBalancedRetryFactory loadBalancedRetryFactory) {
			return new RetryLoadBalancerInterceptor(loadBalancerClient, properties,
					requestFactory, loadBalancedRetryFactory);
		}

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

从其类头部的注解可以知道,要实现负载均衡需要满足两个条件

  • @ConditionalOnClass(RestTemplate.class):RestTemplate必须存在当前工程里
  • @ConditionalOnBean(LoadBalancerClient.class):在当前工程中必须要存在LoadBalancerClient实现的bean

从其代码中可以看出,主要做了一下三件事:

  1. 创建了LoadBalancerInterceptor的bean,用于对客户端发起请求时进行拦截,以实现客户端负载均衡
  2. 创建了RestTemplateCustomizer的bean,给restTemplate添加LoadBalancerInterceptor
  3. 维护了一个被@LoadBalanced修饰的RestTemplate列表,并初始化。

接下来,看看LoadBalancerInterceptor时如何将一个普通的请求变成客户端负载均衡的,源码敬上:

/**
 * @author Spencer Gibb
 * @author Dave Syer
 * @author Ryan Baxter
 * @author William Tran
 */
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();
		Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
		return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
	}
}

LoadBalancerClient和LoadBalancerRequestFactory实在之前的自动化配置类里注入的,当一个被@LoadBalanced修饰的RestTemplate发起一个请求时,就会被LoadBalancerInterceptor的intercept方法拦截,我们可以看到,LoadBalancerInterceptor时把请求转到具体的LoadBalancerClient进行执行。看到这里,LoadBalancerClient还只是一个抽象的负载均衡接口,所以要去看其具体实现,通过IDE,可以很简单的在org.springframework.cloud.netflix.ribbon包下发现LoadBalancerClient的实现类RibbonLoadBalancerClient。

public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		Server server = getServer(loadBalancer);
		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);
	}

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

可以看到,首先是获取ILoadBalancer,然后根据传入的serviceId去获取具体的服务实例

protected Server getServer(ILoadBalancer loadBalancer) {
		if (loadBalancer == null) {
			return null;
		}
		return loadBalancer.chooseServer("default");
	}

通过getServer方法可以发现,并没有使用LoadBalancerClient接口中的choose方法,而是使用了ILoadBalancer的chooseServer方法。首先我们来看下ILoadBalancer的定义

public interface ILoadBalancer {

	public void addServers(List<Server> newServers);
	

	public Server chooseServer(Object key);
	

	public void markServerDown(Server server);
	

	@Deprecated
	public List<Server> getServerList(boolean availableOnly);


    public List<Server> getReachableServers();

	public List<Server> getAllServers();
}
  • 向负载均衡器里维护的服务实例列表增加实例
  • 根据某种策略,从实例列表里挑选出一个具体的服务实例
  • 通知和标识某个具体实例已经停止工作
  • 已废弃
  • 获取当前正常运行的所有实例
  • 获取所有的已知实例,包括正常和已经停止的

通过IDE所展示的类关系,可以看到ILoadBalancer有很多实现,那么Ribbon默认采用的是哪个实现呢?通过Ribbon的配置类RibbonClientConfiguration可以看到,默认采用的是ZoneAwareLoadBalancer

@Bean
	@ConditionalOnMissingBean
	public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
			ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
			IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
		if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
			return this.propertiesFactory.get(ILoadBalancer.class, config, name);
		}
		return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
				serverListFilter, serverListUpdater);
	}

好了,下面再回到LoadBalancerClient的execute方法,再通过ZoneAwareLoadBalancer的chooseServer方法获取到对应的实例server后,包装成RibbonServer,然后使用该对象回调LoadBalancerInterceptor中LoadBalancerRequest的apply方法,而apply就会调用execution.execute,即InterceptingClientHttpRequest下的InterceptingRequestExecution的execute方法

@Override
		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();
			}
		}

可以看到,再创建请求时,调用的是requestFactory.createRequest(request.getURI(), method);而这的request.getURI()方法会调用apply里的ServiceRequestrWrapper对象里从重写的getURI方法

@Override
	public URI getURI() {
		URI uri = this.loadBalancer.reconstructURI(
				this.instance, getRequest().getURI());
		return uri;
	}

使用的是RibbonLoadBalancerClient中实现的reconstructURI方法

@Override
	public URI reconstructURI(ServiceInstance instance, URI original) {
		Assert.notNull(instance, "instance can not be null");
		String serviceId = instance.getServiceId();
		RibbonLoadBalancerContext context = this.clientFactory
				.getLoadBalancerContext(serviceId);

		URI uri;
		Server server;
		if (instance instanceof RibbonServer) {
			RibbonServer ribbonServer = (RibbonServer) instance;
			server = ribbonServer.getServer();
			uri = updateToSecureConnectionIfNeeded(original, ribbonServer);
		} else {
			server = new Server(instance.getScheme(), instance.getHost(), instance.getPort());
			IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);
			ServerIntrospector serverIntrospector = serverIntrospector(serviceId);
			uri = updateToSecureConnectionIfNeeded(original, clientConfig,
					serverIntrospector, server);
		}
		return context.reconstructURIWithServer(server, uri);
	}

从这可以看到,通过ServiceInstance实例对象的saerviceId,从SPringClientFactory类的clientFactory对象中获取对应的serviceId的上下文RibbonLoadBalancerContext,然后,instance之前说过,是RibbonServer的实例,所以直接从RibbonServer里拿到server,再获取对应的uri,updateToSecureConnectionIfNeeded方法就是加上对应的协议,之后呢,就调用RibbonLoadBalancerContext的reconstructURIWithServer

public URI reconstructURIWithServer(Server server, URI original) {
        String host = server.getHost();
        int port = server.getPort();
        String scheme = server.getScheme();
        
        if (host.equals(original.getHost()) 
                && port == original.getPort()
                && scheme == original.getScheme()) {
            return original;
        }
        if (scheme == null) {
            scheme = original.getScheme();
        }
        if (scheme == null) {
            scheme = deriveSchemeAndPortFromPartialUri(original).first();
        }

        try {
            StringBuilder sb = new StringBuilder();
            sb.append(scheme).append("://");
            if (!Strings.isNullOrEmpty(original.getRawUserInfo())) {
                sb.append(original.getRawUserInfo()).append("@");
            }
            sb.append(host);
            if (port >= 0) {
                sb.append(":").append(port);
            }
            sb.append(original.getRawPath());
            if (!Strings.isNullOrEmpty(original.getRawQuery())) {
                sb.append("?").append(original.getRawQuery());
            }
            if (!Strings.isNullOrEmpty(original.getRawFragment())) {
                sb.append("#").append(original.getRawFragment());
            }
            URI newURI = new URI(sb.toString());
            return newURI;            
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }

从实现中可以看到,它从server里拿出对应的主机名,端口,然后在进行拼接整合,形成最终要访问的具体地址。

看到这,已经大致的沥青了spring cloud ribbon中实现客户端负载均衡的基本东西,知道了是如何通过LoadBalancerInterceptor对RestTemplate的请求进行拦截,并利用Spring cloud的LoadBalancerClient将逻辑服务名转换为具体实例地址的过程。同时通过LoadBalancerClient的实现RibbonLoadBalancerClient,就可以知道再使用Ribbon实现的负载均衡时,实际使用的还是Ribbon中定义的ILoadBalancer接口的实现,且自动化配置类会使用ZoneAwareLoadBalancer作为客户端负载均衡的实现。