SpringCloud负载均衡组件
_时光煮雨 人气:0前言
上一篇文章中我们通过自己开发了一个负载均衡组件,实现了随机算法的负载均衡功能,如果要实现其他算法,还需要修改代码增加相应的功能。这一篇文章,我们将介绍一个更简单的负载均衡实现,使用**@LoadBalanced**注解实现负载均衡的功能。
项目实战
创建项目
同样的,我们的项目现在依然有一个registry注册中心,一个provider服务提供者,接下来,我们再次修改一下consumer服务消费者的代码:
@EnableEurekaClient @SpringBootApplication @RestController public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } @Autowired DiscoveryClient discoveryClient; @Autowired RestTemplate restTemplate; @GetMapping("/hello2") public String hello2(String name) { String returnInfo = restTemplate.getForObject( "http://provider/hello?name={1}", String.class, name); return returnInfo; } }
在这个版本,同样的,还是创建RestTemplate Bean对象,不同的是上面仅仅增加了@LoadBalanced注解。
启动项目验证
依然正确返回了结果!
太神奇了吧,比我们自己开发的负载均衡组件简单太多了吧,仅仅在restTemplate() 方法上面增加了一个@LoadBalanced注解,怎么就实现的呢?废话不说,为了一探究竟,扒一扒源码吧!
源码分析
首先,点击@LoadBalanced注解进去,没有什么特别之处,那么我们在想想,Spring在创建Bean实例的时候,注解在什么地方起了作用?什么?不知道?翻一下这篇文章吧:
肝了两周,一张图解锁Spring核心源码
通过回顾Spring启动以及Bean的生命周期创建过程,我们就会发现加上@LoadBalancer注解后,项目启动时就会加载LoadBalancerAutoConfiguration这个配置类(通过spring-cloud-commons包下面的的spring.factories)。通过查看该配置类源码,发现其有个静态内部类LoadBalancerInterceptorConfig,其内部又创建了一个负载均衡拦截器:LoadBalancerInterceptor,该拦截器包含有一个loadBalancerClient参数:
@ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"}) static class LoadBalancerInterceptorConfig { 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); }; } }
我们继续点击LoadBalancerInterceptor类进入,发现intercept方法,该方法中调用了LoadBalancerClient的execute方法,
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { URI originalUri = request.getURI(); String serviceName = originalUri.getHost(); Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution)); }
LoadBalancerClient是一个接口,点击进去我们发现其实现类是RibbonLoadBalancerClient,查看其继承关系:
通过接口中的方法名称,我们可以猜想,choose方法就是选择其中服务列表中其中一个服务,reconstructURI方法就是重新构造请求的URI。
选择服务
choose方法是在RibbonLoadBalancerClient实现类中实现的
public ServiceInstance choose(String serviceId, Object hint) { Server server = this.getServer(this.getLoadBalancer(serviceId), hint); return server == null ? null : new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server)); }
在这个方法中,先调用 getServer 方法获取服务,这个方法最终会调用 ILoadBalancer 接口的 chooseServer 方法,而 ILoadBalancer 接口的实现类默认是ZoneAwareLoadBalancer。
ZoneAwareLoadBalancer 继承自 DynamicServerListLoadBalancer ,而在 DynamicServerListLoadBalancer 的构造方法中,调用了 this.restOfInit(clientConfig);在restOfInit这个方法中,通过 this.updateListOfServers()来获取服务列表;
而在chooseServer ()方法中,就会根据负载均衡算法,选择其中一个服务并返回:
BaseLoadBalancer zoneLoadBalancer = this.getLoadBalancer(zone); server = zoneLoadBalancer.chooseServer(key);
地址替换
选择其中一个服务信息后,怎么将接口从 http://provider/hello 变为 http://localhost:8003/hello 呢?还记得上面我们说的reconstructURI方法吗?通过配置类LoadBalancerAutoConfiguration加载后,会注入LoadBalancerInterceptor拦截器,该拦截器会拦截我们的请求,并对请求地址进行处理,重构方法的具体实现在 LoadBalancerContext 类的 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; } else { if (scheme == null) { scheme = original.getScheme(); } if (scheme == null) { scheme = (String)this.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 var8) { throw new RuntimeException(var8); } } }
可以看到该方法中,将原始的请求地址original,替换成了选取的服务的IP和端口。并最终调用该服务的接口方法。
看到这里,再想想我们上一章的内容,是不是有异曲同工之妙?
总结
通过添加@LoadBalanced注解,就及其简单的实现了负载均衡的功能,与其说是Ribbon的强大,不如说是Spring的强大,Spring在整个上下文创建过程中,在不同的时机开放了一个又一个的接口,这就为各种组件的继承提供了遍历,同时也进一步促进了Spring生态的快速发展。
加载全部内容