Ribbon之源码剖析

附:SpringCloud之系列汇总跳转地址

简介

这篇文章是关于Spring Cloud Ribbon源码的解析的文章,在开始前大家必须搞清楚一件事,那就是Spring Cloud Ribbon和Netflix Ribbon,这个很关键,因我刚开始就弄混了,以为Spring Cloud Ribbon就是Netflix的Ribbon,这对查资料会有很大的误区。

Spring Cloud Ribbon 和 Netflix Ribbon

  1. Spring Cloud Ribbon是在Netflix Ribbon的基础上做了进一步的封装,使它更加适合于微服务。
  2. 服务清单来源不同,Spring Cloud Ribbon的路由的服务清单是通过定时任务从Eureka Client同步过来的,Netflix Ribbon需要手动设置。
  3. 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

 上面代码的步骤如下:

  1. 相关数据配置在config文件中,通过Archaius ConfigurationManager 加载配置数据。
  2. 通过ClientFactory创建RestClient 和 ZoneAwareLoadBalancer(负载均衡器)。
  3. 使用构建器构建http请求。请注意,我们只提供URI的路径部分(“/”)。一旦服务器被ZoneAwareLoadBalancer(负载均衡器)选中,完整的请求链接将由RestClient计算。
  4. 发送请求是通过RestClient 的executeWithLoadBalancer()方法触发的。
  5. 可以通过修改配置文件来动态的修改可用的服务的列表。

通过上面的步骤我们可以知道,网络请求的动作由RestClient实现,负载均衡的服务清单的维护和负载均衡的算法是在ZoneAwareLoadBalancer中实现的。

Spring Cloud Ribbon如何实现均衡器功能

  1. 如何发起一个实现了负载均衡器的请求
@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请求

附:SpringCloud之系列汇总跳转地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值