Spring Cloud负载均衡组件Ribbon源码分析

Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。 通过 Spring Cloud 的封装, 可以让我们轻松地将面向服务的 REST 模板请求自动转换成客户端负载均衡的服务调用。

1.Ribbon介绍

通过Spring Cloud Ribbon的封装, 我们在微服务架构中使用客户端负载均衡调用非常 简单, 只需要如下两步:

  • 服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心。
  • 服务消费者直接通过调用被@LoadBalanced 注解修饰过的 RestTemplate 来实现面向服务的接口调用。

这样,我们就可以将服务提供者的高可用以及服务消费者的负载均衡调用一起实现了。

2.基础入门使用

@EnableDiscoveryClient 
@SpringBootApplication 
public class ConsumerApplication { 
	@Bean 
	@LoadBalanced 
	RestTemplate restTemplate () { 
		return new RestTemplate (); 
	} 
	public static void main(String[] args) { 
		SpringApplication.run(ConsumerApplication.class, args); 
	} 
}

参照第一点,首先通过@EnableDiscoveryClient 让该应用注册为 Eureka 客户端应用,同时, 在该主类中创建 RestTemplate 的 Spring Bean 实例,并通过@LoadBalanced 注解开启客户端负载均衡。所以我们看出要使用Ribbon负载均衡策略离不开RestTemplate这个类的。


3.RestTemplate

那前面说到的RestTemplate是什么呢? 简而言之就是一http请求工具,支持Get、Post、Delete等请求方法,同时还可以根据返回值,自动封装成对象体,这边以Get请求为示例。 在 RestTemplate 中, 对 GET 请求可以通过如下两个方法进行调用实现。

  1. getForEntity 函数,返回的是 ResponseEntity,该对象是 Spring对 HTTP 请求响应的封装。该方法有三种重构方法,用法大同小异可以自由选择。 (访问USER-SERVER 服务的/user,同时将didi替换到{1}占位符处,构建请求url)
RestTemplate restTemplate = new RestTemplate(); 
// 我们可以将String.class换成想要的实体类 如User.class等
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://USERSERVICE/user?name= {1} ", String.class, "didi") ; 
String body = responseEntity. getBody();
  1. getForObject函数,该方法同样有三种重构方法,用法同上。 (当不需要关注请求响应除 body 外的其他内容时, 该函数就非常好用, 可以少一个从Response 中获取 body的步骤。 它与 getForEntity 函数类似)
RestTemplate restTemplate = new RestTemplate() ; 
// 无参用法
String result = restTemplate.getForObject(uri, String.class);

4.源码分析

  1. 我们都知道,与之前使用区别就是,增添了个RestTemplate的Bean,并且用注解@LoadBanlanced修饰了该Bean,所以看@LoadBalanced注解源码的注释中可以知道, 该注解用来给RestTemplate做标记, 以使用负载均衡的客户端(LoadBalancerClient)来配置它。
  2. 当我们有RestTemplate的类时候,就会实现Ribbon的负载均衡自动化配置LoadBalancerAutoConfiguration类。
@Configuration 
@ConditionalOnClass(RestTemplate.class) 
@ConditionalOnBean(LoadBalancerClient.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); 
			}
	@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);
			}
		};
	}

	@Bean 
	public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient) { 
		return new LoadBalancerinterceptor(loadBalancerClient);
	}
}
  1. 在该自动化负载均衡配置,我们就能发现端倪,里面会创建一个LoadBalancerInterceptor的Bean。
  2. 顾名思义LoadBalancerInterceptor就是一个拦截器,将我们RestTemplate的请求拦截下来,通过负载均衡策略进行选择,然后得到我们这次应该请求的具体服务实例地址。
  3. 我们可以在LoadBalancerInterceptor核心方法intercept()中看到具体是怎么拦截的。
@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, 
		new LoadBalancerRequest<ClientHttpResponse> () { 
			@Override 
			public ClientHttpResponse apply(final Serviceinstance instance) throws Exception { 
				HttpRequest serviceRequest = new ServiceRequestWrapper(request, 
				instance); 
				return execution.execute(serviceRequest, body);
			}
		}
	); 
}
  1. 通过源码以及之前的自动化配置类, 我们可以看到在拦截器中注入了 LoadBalancerClient 的实现。 当一个被@LoadBalanced 注解修饰的 RestTemplate 对象向外发起 HTTP 请求时, 会被 LoadBalancerinterceptor 类的 intercept 函数所拦截,所以关键就是intercept方法。
  2. 我们可以看到intercept方法关键就是使用了传入的LoadBalancerClient调用他的execute方法来进行处理,那我们需要找到具体LoadBalancerClient实现类是在哪里创建的,就可以知道他是怎么进行处理的。
  3. 通过查看Ribbon的源码, 可以在org.springframework.cloud.netflix.ribbon 包下找到对应的实现类 RibbonLoadBalancerClient这个实现类,可以看到他的execute方法就是关键。
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 " + service Id) ; 
	}	
	// 创建Ribbon服务封装类,ServerInstance的实现类
	RibbonServer ribbonServer = new RibbonServer(serviceid, server, isSecure(server, 
serviceid), serverintrospector(serviceid) .getMetadata(server)); 
	// 创建ribbon负载均衡上下文,用来记录数据日志等
	RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceid); 
	// 创建数据记录器
	RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server); 
	try { 
		// 关键!请求执行负载均衡器筛选后的server具体实例,返回请求结果T
		T returnVal = request.apply(ribbonServer); 
		// 记录数据
		statsRecorder.recordStats(returnVal); 
		return returnVal; 
	}
	catch (IOException ex) { 
		StatsRecorder.recordStats(ex);
		throw ex; 
	catch (Exception ex) { 
		StatsRecorder.recordStats(ex);
		ReflectionUtils.rethrowRuntimeException(ex);
	return null; 
	}
				
}
  1. 可以看到,在 execute 函数的实现中,第一步做的就是通过 getServer 根据传入的服务名 serviceId 去获得具体的服务实例:
  2. 在getServer方法中调用传进来的ILoadBalancer 类进行chooseService选择具体实例,
public interface ILoadBalancer { 
	// 向负载均衡器中维护的实例列表增加服务实例。
	public void addServers(List<Server> newServers); 
	// 通过某种策略, 从负载均衡器中挑选出 一个具体的服务实例。
	public Server chooseServer(Objectkey);
	public void markServerDown(Server server); 
	public List<Server> getReachableServers () ; 
	public List<Server> getA11Servers(); 
}
  1. 而对于该接口的实现,整理出如下图所示的结构。可以看到BaseLoadBalancer 类 实现了基础的负载均衡,而DynarnicServerListLoaclBalancer和 ZoneAwareLoaclBalancer 在负载均衡的策略上做了一些功能的扩展。 在这里插入图片描述

  2. 我们通过RibbonClientConfiguration 配置类, 可以知道在整合时默认采用了 ZoneAwareLoadBalancer 来调用chooseService方法实现负载均衡器

@Bean 
@ConditionalOnMissingBean 
public ILoadBalancer ribbonLoadBalancer(IClientConfig config, 
	ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, 
	IRule rule, IPing ping) { 
	ZoneAwareLoadBalancer<Server> balancer = LoadBalancerBuilder.newBuilder() .withClientConfig(config) .withRule(rule) .withPing(ping) 
.withServerListFilter(serverListFilter).withDynamicServerList(serverList).buildDynamicServerListLoad.Balancer(); 
	return balancer; 
}

  1. 我们这个时候返回到第8点看LoadBalanceClient执行到 T returnVal = request.apply(ribbonServer); 是如何执行服务请求的。
  2. 可以看到拦截器传入了LoadBalancerRequest类重写了apply方法,调用request.apply就是调用这个方法了
@Override 
public ClientHttpResponse apply(final Serviceinstance instance)
throws Exception ( 
	HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance); 
	return execution.execute(serviceRequest, body);
}
  1. 接下来我们看到调用了拦截器传入的ClientHttpRequestExecution进行处理,可以看到核心方法,里面拿到了我们传进来的ServiceRequestWrapper调用getURI获取URI。 ClientHttpRequest delegate = requestFactory.createRequest {request.getURI(), request. getMethod());
  2. 而getURI方法此时,它就会使用RibbonLoadBalancerClient中实现的reconstructURI 来组织具体请求的服务实例地址。
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); 
	Server server = new Server(instance.getHost(), instance.getPort()); 
	boolean secure = isSecure(server, serviceid); 
	URI uri = original; 
	if (secure) { 
		uri = UriComponentsBuilder.fromUri(uri).scheme ("https").build().toUri(); 
	}
	return context.reconstructURIWithServer(server, uri);
}
  1. reconstructURI 函数中我们可以看到,它通过 ServiceInstance 实例对象的服务名字, 从 SpringClientFactory 类的 clientFactory 对象中获取对应服务名字的负载均衡器的上下文RibbonLoadBalancerContext 对象。 然后根据ServiceInstance 中的信息来构建具体服务实例信息的 Server 对象 , 并使用RibbonLoadBalancerContext对象的reconstructURIWithServer 函数来构建服务实例的URI。

  2. SpringClientFactoryRibbonLoadBalancerContext是什么?

SpringClientFactory 类是一个用来创建客户端负载均衡器的工厂类,该工厂类会为每一个不同名的 Ribbon 客户端生成不同的 Spring 上下文。

RibbonLoadBalancerContext类是 LoadBalancerContext的子类,该类用于存储一些被负载均衡器使用的上下文内容和 API操作(reconstructURIWithServer就是其中之一 )。

  1. 我们可以看到在reconstructURI中我们使用的是ServiceInstance而reconstructURIWithServer使用的是Server作为参数,我们可以看到reconstructURIWithServer从Server 对象中获取 host 和 port 信息, 然后根据以服务名为 host 的 URI 对象 original 中获取其他请求信息进行拼接,然后进行服务请求调用。

总结

分析到这里, 我们已经可以大致理清Spring Cloud Ribbon中实现客户端负载均衡的基 本脉络,了解了它是如何通过 LoadBalancerinterceptor拦截器对 RestTemplate 的请求进行拦截, 并利用Spring Cloud的负载均衡器 LoadBalancerClient将以逻辑服 务名为host的 URI转换成具体的服务实例地址的过程。 同时通过分析LoadBalancerClient的Ribbon实现RibbonLoadBalancerClient, 可以知道在使用ribbon实现负载均衡器的时候,实际使用的还是Ribbon中定义的ILoadBalancer接口的实现,自动化配置会采ZoneAwareLoadBalancer的实例来实现客户端负载均衡。

  • LoadBalancerinterceptor:核心负载均衡拦截器
  • RibbonLoadBalancerClient:负载均衡客户端
  • ILoadBalancer:负载均衡器接口
  • ZoneAwareLoadBalancer:默认实现负载均衡器

参考:《Spring Cloud微服务实战》

end
  • 作者:Endwas(联系作者)
  • 发表时间:2021-05-28 16:46
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转博主转载的文章,请附上原文链接
  • 公众号转载:请在文末添加作者名字和博客地址
  • 评论