简介
这篇文章是关于Spring Cloud Ribbon源码的解析的文章,在开始前大家必须搞清楚一件事,那就是Spring Cloud Ribbon和Netflix Ribbon,这个很关键,因我刚开始就弄混了,以为Spring Cloud Ribbon就是Netflix的Ribbon,这对查资料会有很大的误区。
Spring Cloud Ribbon 和 Netflix Ribbon
- Spring Cloud Ribbon是在Netflix Ribbon的基础上做了进一步的封装,使它更加适合于微服务。
- 服务清单来源不同,Spring Cloud Ribbon的路由的服务清单是通过定时任务从Eureka Client同步过来的,Netflix Ribbon需要手动设置。
- Spring Cloud Ribbon的均衡器使用的是Netflix Ribbon的ZoneAwareLoadBalancer。
所以如果我们想理解Spring Cloud Ribbon首先应该理解Netflix Ribbon的工作原理
Netflix Ribbon如何实现均衡器功能
先看一段Netflix Ribbon实现简单路由的demo,代码如下:
public static void main(String[] args) throws Exception {
ConfigurationManager.loadPropertiesFromResources("sample-client.properties"); // 1
System.out.println(ConfigurationManager.getConfigInstance().getProperty("sample-client.ribbon.listOfServers"));
RestClient client = (RestClient) ClientFactory.getNamedClient("sample-client"); // 2
HttpClientRequest request = HttpClientRequest.newBuilder().setUri(new URI("/")).build(); // 3
for (int i = 0; i < 20; i++) {
HttpClientResponse response = client.executeWithLoadBalancer(request); // 4
System.out.println("Status code for " + response.getRequestedURI() + " :" + response.getStatus());
}
ZoneAwareLoadBalancer lb = (ZoneAwareLoadBalancer) client.getLoadBalancer();
System.out.println(lb.getLoadBalancerStats());
ConfigurationManager.getConfigInstance().setProperty(
"sample-client.ribbon.listOfServers", "www.linkedin.com:80,www.google.com:80"); // 5
System.out.println("changing servers ...");
Thread.sleep(3000); // 6
for (int i = 0; i < 20; i++) {
HttpClientResponse response = client.executeWithLoadBalancer(request);
System.out.println("Status code for " + response.getRequestedURI() + " : " + response.getStatus());
response.releaseResources();
}
System.out.println(lb.getLoadBalancerStats()); // 7
}
配置文件如下:
sample-client.ribbon.listOfServers=www.microsoft.com:80,www.yahoo.com:80,www.google.com:80
上面代码的步骤如下:
- 相关数据配置在config文件中,通过Archaius ConfigurationManager 加载配置数据。
- 通过ClientFactory创建RestClient 和 ZoneAwareLoadBalancer(负载均衡器)。
- 使用构建器构建http请求。请注意,我们只提供URI的路径部分(“/”)。一旦服务器被ZoneAwareLoadBalancer(负载均衡器)选中,完整的请求链接将由RestClient计算。
- 发送请求是通过RestClient 的executeWithLoadBalancer()方法触发的。
- 可以通过修改配置文件来动态的修改可用的服务的列表。
通过上面的步骤我们可以知道,网络请求的动作由RestClient实现,负载均衡的服务清单的维护和负载均衡的算法是在ZoneAwareLoadBalancer中实现的。
Spring Cloud Ribbon如何实现均衡器功能
- 如何发起一个实现了负载均衡器的请求
@Autowired
RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "helloFallback")
public String hiService(String name) {
return restTemplate.getForObject("http://service-hi/hi?name="+name,String.class);
}
RestTemplate 是Spring自己封装的http请求的客户端,也就是说它只能发送一个正常的Http请求,这跟我们要求的负载均衡是有出入的,还有就是这个请求的链接上的域名是我们微服务的一个服务名,而不是一个真正的域名,那它是怎么实现负载均衡功能的呢?开启负载均衡的关键在@LoadBalanced这个注解上:
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
上面是@LoadBalanced的定义,这就是一个普通的标记注解,作用就是修饰RestTemplate让其拥有负载均衡的能力,全局搜索发现在LoadBalancerAutoConfiguration.java这个类里用到了。
@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 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);
}
}
}
};
}
//这里的restTemplates是所有的被@LoadBalanced注解的集合,这就是标记注解的作用(Autowired是可以集合注入的)
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(
LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
}
//生成一个LoadBalancerInterceptor的Bean
@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
//给注解了@LoadBalanced的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);
}
};
}
}
...//省略后面代码
}
看到这里,我们应该大致知道@loadBalanced的作用了,就是起到一个标记RestTemplate的作用,当服务启动时,标记了的RestTemplate对象里面就会被自动加入LoadBalancerInterceptor拦截器,这样当RestTemplate发起http请求时,会被LoadBalancerInterceptor的intercept拦截,而intercept里面又调用了LoadBalancerClient接口实现类的execute方法
2.LoadBalancerClient
负载均衡器的核心类为LoadBalancerClient, LoadBalancerClient可以获取负载均衡的服务提供者的实例信息。为了演示,在RibbonController重新写一个接口“/testRibbon”,通过LoadBalancerCIient去选择一个eureka-client的服务实例的信息,并将该信息返回,继续在eureka-ribbon-client工程上修改,代码如下:
@RestController
public class RibbonController {
...//省略代码
@Autowired
private LoadBalancerClient loadBalancer;
@GetMapping ("/testRibbon")
public String testRibbon() {
ServiceInstance instance = loadBalancer.choose("eureka-client");
return instance.getHost() + ":" + instance.getPort();
}
}
启动工程,在浏览器上多次访问http://localhost:8764/testRibbon,浏览器会轮流显示如下内容 :
localhost:8672
localhost:8673
可见,LoadBalancerClient 的choose("eureka-client'’)方法可以轮流得到eureka-client的两个服务实例的信息。负载均衡器LoadBalancerClient定时从EurekaClient获取服务注册列表信息,并将服务注册列表信息缓存一份。在LoadBalancerClient 调用choose()方法时,根据负载均衡策略选择一个服务实例的信息,从而进行了负载均衡。
3.拦截器都做了什么?
上面提到过,发起http后请求后,请求会到达到达拦截器中,在拦截其中实现负载均衡,先看看代码:
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));
}
}
我们可以看到在intercept()方法中实现拦截的具体逻辑,首先会根据传进来的请求链接,获取微服务的名字serviceName,然后调用LoadBalancerClient的execute(String serviceId, LoadBalancerRequest<T> request)方法,这个方法直接返回了请求结果,所以真正的路由逻辑在LoadBalancerClient的实现类中,而这个实现类就是RibbonLoadBalancerClient,看看execute()的源码:
@Override
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);
}
首先是获得均衡器ILoadBalancer这个类上面讲到过这是Netflix Ribbon中的均衡器,这是一个抽象类,具体的实现类是ZoneAwareLoadBalancer。上面也讲到过,每一个微服务名对应一个均衡器,均衡器中维护着该微服务名下所有的服务清单。getLoadBalancer()方法通过serviceId获得对应的均衡器,getServer()方法通过对应的均衡器对应的路由的算法下计算得到需要路由到的Server,Server中有该服务的具体域名等相关信息。得到了具体的Server后执行正常的Http请求,整个请求的负载均衡逻辑就完成了。
最后附一张Eureka与Ribbon整合的流程图
总结
总的来说,Ribbon结合RestTemplate的工作流程如下:
- 注入一个RestTemplate Bean,并为它加上@LoadBalanced注解,让其拥有负载均衡的能力
- 被标记了的RestTemplate对象,会被加入LoadBalancerInterceptor拦截器,当RestTemplate发起http请求时,会被LoadBalancerInterceptor的intercept拦截,而intercept里面又调用了LoadBalancerClient接口实现类的execute方法
- execute方法中,首先执行getLoadBalancer()方法,通过serviceId获得对应的均衡器,然后执行getServer(loadBalancer)方法,根据负载均衡策略选中一个服务实例的信息(Ribbon会从 Eureka Client里获取到对应的服务注册表),发起Http请求