今天看啥  ›  专栏  ›  zchanglin

RestTemplate与负载均衡器

zchanglin  · 掘金  ·  · 2021-02-02 15:15
阅读 58

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 是放置在服务器端的组件。当请求来自客户端时,它们将转到负载均衡器,负载均衡器将为请求指定服务器。负载均衡器使用的最简单的算法是随机指定。在这种情况下,大多数负载平衡器是用于控制负载平衡的硬件集成软件。

服务端负载均衡的特点:

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

客户端负载均衡

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

客户端负载均衡的特点:

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

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

1、直接填写服务地址

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

2、使用 LoadBalancerClient

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

3、注入 RestTemplate Bean

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

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

/**
 * 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 {

}
复制代码

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

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
    // 如 http://SHOP-CLIENT/shop/show -> http://localhost:8080/shop/show
	URI reconstructURI(ServiceInstance instance, URI original);
}
复制代码

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

ServiceInstance choose(String serviceId);
复制代码

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

/**
 * Auto-configuration for Ribbon (client-side load balancing).
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
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)
	@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(proxyBeanMethods = false)
	@ConditionalOnClass(RetryTemplate.class)
	public static class RetryAutoConfiguration {

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

	}
    
	@Configuration(proxyBeanMethods = false)
	@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);
			};
		}

	}

}
复制代码

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 的源码可以来看下:

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,
				this.requestFactory.createRequest(request, body, execution));
	}

}



@FunctionalInterface
public interface ClientHttpRequestInterceptor {
    ClientHttpResponse intercept(HttpRequest var1, byte[] var2, ClientHttpRequestExecution var3) throws IOException;
}
复制代码

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

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

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

public interface ILoadBalancer {
    // 向负载均衡器中维护的实例列表增加服务实例
    void addServers(List<Server> var1);

    // 表示通过某种策略,从负载均衡服务器中挑选出一个具体的服务实例
    Server chooseServer(Object var1);

    // 表示用来通知和标识负载均衡器中某个具体实例已经停止服务
    void markServerDown(Server var1);

    // 表示获取当前正常工作的服务实例列表
    List<Server> getReachableServers();
    
    // 表示获取所有的服务实例列表,包括正常的服务和停止工作的服务
    List<Server> getAllServers();
}
复制代码

我们看最基础的 BaseLoadBalancer 即可:

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

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




原文地址:访问原文地址
快照地址: 访问文章快照