RestTemplate与负载均衡器

本文主要是介绍SpringCloud构建微服务系统的Ribbon负载均衡器和网络请求框架RestTemplate,另外将会分析负载均衡器的源码,通过实例证明如何通过Ribbon和RestTemplate相结合实现负载均衡。现在假设有一个分布式系统,该系统由在不同计算机上运行的许多服务组成。当用户数量很大时,通常会为服务创建多个副本。每个副本都在另一台计算机上运行,此时有助于在服务器之间平均分配传入流量。

客户端发现与服务端发现

在一个系统中,服务通常需要调用其他服务。单体应用中,服务通过语言级别的方法或者过程调用另外的服务。在传统的分布式部署中,服务运行在固定,已知的地址主机和端口,因此可以请求的通过HTTP/REST或其他RPC机制调用。 然而,一个现代的微服务应用通常运行在虚拟或者容器环境,服务实例数和它们的地址都在动态改变。因此需要实现一种机制,允许服务的客户端向动态变更的一组短暂的服务实例发起请求,这就是服务注册与发现,服务注册与发现是微服务架构中最重要的基础组件

我们接下来需要搞清楚的是什么是客户端发现,什么是服务端发现?

这里的注册中心其实就相当于青楼的老鸨,A是嫖客,B是小姐。这样一比喻相信各位老司机都知道三者之间的交互逻辑了。客户端发现就是当A需要调用B服务时,请求注册中心(B服务在启动时会将信息注册到注册中心),注册中心将一份完整的可用服务列表返回给 A 服务,A 服务自行决定使用哪个 B 服务。客户端发现的特点:

  • 简单直接,不需要代理的介入
  • 客户端(A)知道所有实际可用的服务地址
  • 客户端(A)需要自己实现负载均衡逻辑

使用客户端发现的例子:Eureka

服务端发现相对于客户端发现,多了一个代理,代理帮A从众多的B中挑选一个B。服务端发现的特点:

  • 由于代理的介入,服务(B)与注册中心,对 A 是不可见的

使用服务端发现的例子:Nginx、ZooKeeper、Kubernetes

客户端与服务端负载均衡

通过理解客户端发现与服务端发现的区别,我们明白其实调用哪个服务取决于客户端还是服务端是由什么决定的呢?那就是取决于服务注册与发现使用的是客户端发现还是服务端发现。

服务端负载均衡

服务器端负载均衡器,我们比较常见的例如Nginx、F5是放置在服务器端的组件。当请求来自客户端时,它们将转到负载均衡器,负载均衡器将为请求指定服务器。负载均衡器使用的最简单的算法是随机指定。在这种情况下,大多数负载平衡器是用于控制负载平衡的硬件集成软件。 mark 服务端负载均衡的特点:

  • 对客户端不透明,客户端不知道服务器端的服务列表,甚至不知道自己发送请求的目标地址存在负载均衡器。
  • 服务器端维护负载均衡服务器,控制负载均衡策略和算法。

客户端负载均衡

当负载均衡器位于客户端时,客户端得到可用的服务器列表然后按照特定的负载均衡策略,分发请求到不同的服务器 。 mark

客户端负载均衡的特点:

  • 对客户端透明,客户端需要知道服务器端的服务列表,需要自行决定请求要发送的目标地址。
  • 客户端维护负载均衡服务器,控制负载均衡策略和算法。
  • 目前单独提供的客户端实现比较少(本文只分析Ribbon),大部分都是在框架内部自行实现。

RestTemplate三种使用方式

RestTemplate是Spring框架提供的一种用于访问Rest服务的客户端,RestTemplate提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率。 之前我们使用的较多的是Apache的OKHttp这个包库,或者是根据HttpUrlConnection封装的库,现在有了更好的选择,那就是RestTemplate:

1、直接填写服务地址

Order应用想要直接访问Shop应用的接口,填写服务地址直访问即可。

mark

2、使用LoadBalancerClient

使用LoadBalancerClient的choose()获得ServiceInstance,也就是这两个应用必须先向Eureka Server注册,然后通过Client的名称来选择对应的服务实例:

mark

3、注入RestTemplate Bean

注入RestTemplate bean,使用服务名称访问即可:

mark

Ribbon负载均衡源码分析

在上面的例子中我们使用了RestTemplate并且开启了客户端负载均衡功能,开启负载均衡很简单,只需要在RestTemplate的bean上再添加一个@LoadBalanced注解即可,我们可以从这个注解开始分析:

 1/**
 2 * Annotation to mark a RestTemplate or WebClient bean to be configured to use a
 3 * LoadBalancerClient.
 4 * @author Spencer Gibb
 5 */
 6@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
 7@Retention(RetentionPolicy.RUNTIME)
 8@Documented
 9@Inherited
10@Qualifier
11public @interface LoadBalanced {
12
13}

这个注解是用来给RestTemplate做标记,配置LoadBalancerClient,那么我们需要关注的类就是LoadBalancerClient了,LoadBalancerClient表示客户端负载均衡器,并且继承了ServiceInstanceChooser:

 1public interface LoadBalancerClient extends ServiceInstanceChooser {
 2    // 使用从负载均衡器中挑选出来的服务实例来执行请求
 3	<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
 4    // 使用从负载均衡器中挑选出来的服务实例来执行请求
 5	<T> T execute(String serviceId, ServiceInstance serviceInstance,
 6			LoadBalancerRequest<T> request) throws IOException;
 7	// 为系统构建一个合适的URI
 8    // 如 http://SHOP-CLIENT/shop/show -> http://localhost:8080/shop/show
 9	URI reconstructURI(ServiceInstance instance, URI original);
10}

ServiceInstanceChooser从名字上我们就可以看出,这是需要给出服务实例选择的具体实现,也就是实现choose方法: 根据传入的服务名serviceId从客户端负载均衡器中挑选一个对应服务的实例:

1ServiceInstance choose(String serviceId);

至于具体的配置我们还需要看LoadBalancerAutoConfiguration类的源码,该类是客户端负载均衡服务器的自动化配置类,该类的源码如下:

  1/**
  2 * Auto-configuration for Ribbon (client-side load balancing).
  3 */
  4@Configuration(proxyBeanMethods = false)
  5@ConditionalOnClass(RestTemplate.class)
  6@ConditionalOnBean(LoadBalancerClient.class)
  7@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
  8public class LoadBalancerAutoConfiguration {
  9
 10	@LoadBalanced
 11	@Autowired(required = false)
 12	private List<RestTemplate> restTemplates = Collections.emptyList();
 13
 14	@Autowired(required = false)
 15	private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
 16
 17	@Bean
 18	public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
 19			final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
 20		return () -> restTemplateCustomizers.ifAvailable(customizers -> {
 21			for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
 22				for (RestTemplateCustomizer customizer : customizers) {
 23					customizer.customize(restTemplate);
 24				}
 25			}
 26		});
 27	}
 28
 29	@Bean
 30	@ConditionalOnMissingBean
 31	public LoadBalancerRequestFactory loadBalancerRequestFactory(
 32			LoadBalancerClient loadBalancerClient) {
 33		return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
 34	}
 35
 36	@Configuration(proxyBeanMethods = false)
 37	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
 38	static class LoadBalancerInterceptorConfig {
 39
 40		@Bean
 41		public LoadBalancerInterceptor ribbonInterceptor(
 42				LoadBalancerClient loadBalancerClient,
 43				LoadBalancerRequestFactory requestFactory) {
 44			return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
 45		}
 46
 47		@Bean
 48		@ConditionalOnMissingBean
 49		public RestTemplateCustomizer restTemplateCustomizer(
 50				final LoadBalancerInterceptor loadBalancerInterceptor) {
 51			return restTemplate -> {
 52				List<ClientHttpRequestInterceptor> list = new ArrayList<>(
 53						restTemplate.getInterceptors());
 54				list.add(loadBalancerInterceptor);
 55				restTemplate.setInterceptors(list);
 56			};
 57		}
 58
 59	}
 60
 61	@Configuration(proxyBeanMethods = false)
 62	@ConditionalOnClass(RetryTemplate.class)
 63	public static class RetryAutoConfiguration {
 64
 65		@Bean
 66		@ConditionalOnMissingBean
 67		public LoadBalancedRetryFactory loadBalancedRetryFactory() {
 68			return new LoadBalancedRetryFactory() {
 69			};
 70		}
 71
 72	}
 73    
 74	@Configuration(proxyBeanMethods = false)
 75	@ConditionalOnClass(RetryTemplate.class)
 76	public static class RetryInterceptorAutoConfiguration {
 77
 78		@Bean
 79		@ConditionalOnMissingBean
 80		public RetryLoadBalancerInterceptor ribbonInterceptor(
 81				LoadBalancerClient loadBalancerClient,
 82				LoadBalancerRetryProperties properties,
 83				LoadBalancerRequestFactory requestFactory,
 84				LoadBalancedRetryFactory loadBalancedRetryFactory) {
 85			return new RetryLoadBalancerInterceptor(loadBalancerClient, properties,
 86					requestFactory, loadBalancedRetryFactory);
 87		}
 88
 89		@Bean
 90		@ConditionalOnMissingBean
 91		public RestTemplateCustomizer restTemplateCustomizer(
 92				final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
 93			return restTemplate -> {
 94				List<ClientHttpRequestInterceptor> list = new ArrayList<>(
 95						restTemplate.getInterceptors());
 96				list.add(loadBalancerInterceptor);
 97				restTemplate.setInterceptors(list);
 98			};
 99		}
100
101	}
102
103}

LoadBalancerAutoConfiguration类上有两个关键注解,分别是@ConditionalOnClass(RestTemplate.class)和@ConditionalOnBean(LoadBalancerClient.class),说明Ribbon如果想要实现负载均衡的自动化配置需要满足两个条件:第一个,RestTemplate类必须存在于当前工程的环境中;第二个,在Spring容器中必须有LoadBalancerClient的实现Bean。

RetryInterceptorAutoConfiguration类的ribbonInterceptor方法返回了一个拦截器叫做LoadBalancerInterceptor,这个拦截器的作用主要是在客户端发起请求时进行拦截,进而实现客户端负载均衡功能, 其中的restTemplateCustomizer方法返回了一个RestTemplateCustomizer,这个方法主要用来给RestTemplate添加LoadBalancerInterceptor拦截器。LoadBalancerAutoConfiguration中的restTemplates是一个被@LoadBalanced注解修饰的RestTemplate对象列表,通过restTemplateCustomizer方法对每个 RestTemplate对象添加上LoadBalancerInterceptor拦截器。

那其实就是这些拦截器让一个普通的RestTemplate对象拥有了负载均衡的功能,LoadBalancerInterceptor的源码可以来看下:

 1public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
 2
 3	private LoadBalancerClient loadBalancer;
 4
 5	private LoadBalancerRequestFactory requestFactory;
 6
 7	public LoadBalancerInterceptor(LoadBalancerClient loadBalancer,
 8			LoadBalancerRequestFactory requestFactory) {
 9		this.loadBalancer = loadBalancer;
10		this.requestFactory = requestFactory;
11	}
12
13	public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
14		// for backwards compatibility
15		this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
16	}
17
18	@Override
19	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
20			final ClientHttpRequestExecution execution) throws IOException {
21		final URI originalUri = request.getURI();
22		String serviceName = originalUri.getHost();
23		Assert.state(serviceName != null,
24				"Request URI does not contain a valid hostname: " + originalUri);
25		return this.loadBalancer.execute(serviceName,
26				this.requestFactory.createRequest(request, body, execution));
27	}
28
29}
30
31
32
33@FunctionalInterface
34public interface ClientHttpRequestInterceptor {
35    ClientHttpResponse intercept(HttpRequest var1, byte[] var2, ClientHttpRequestExecution var3) throws IOException;
36}

当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会被LoadBalancerInterceptor类的intercept方法拦截,在这个方法中直接通过getHost方法就可以获取到服务名(因为我们在使用RestTemplate调用服务的时候,使用的是服务名而不是域名,所以这里可以通过getHost直接拿到服务名然后去调用execute方法发起请求)。

接下来我们去看看LoadBalancerClient的具体实现 —— RibbonLoadBalancerClient:在execute方法的具体视线中,不难发现首先获取到的就是ILoadBalancer:

mark

这是一个接口,添加服务实例,选择服务实例,获取所有服务实例等方法均在其中:

 1public interface ILoadBalancer {
 2    // 向负载均衡器中维护的实例列表增加服务实例
 3    void addServers(List<Server> var1);
 4
 5    // 表示通过某种策略,从负载均衡服务器中挑选出一个具体的服务实例
 6    Server chooseServer(Object var1);
 7
 8    // 表示用来通知和标识负载均衡器中某个具体实例已经停止服务
 9    void markServerDown(Server var1);
10
11    // 表示获取当前正常工作的服务实例列表
12    List<Server> getReachableServers();
13    
14    // 表示获取所有的服务实例列表,包括正常的服务和停止工作的服务
15    List<Server> getAllServers();
16}

我们看最基础的BaseLoadBalancer即可:

mark

mark

不难发现,其实默认的负载均衡策略采用的是轮询的方式。至于负载均衡的策略,其实也有很多种实现:

mark

总结一下就是RestTemplate发起一个请求,这个请求被LoadBalancerInterceptor给拦截了,拦截后将请求的地址中的服务逻辑名转为具体的服务地址,然后继续执行请求的一个过程。