Nacos下 Ribbon 原理分析

Ribbon 原理解析

1 初始化 Ribbon 配置信息

  1. 在 nacos 作为注册中心的项目中,需要引入服务发现的依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

其中会引入 spring-cloud-commons 依赖,而该依赖又会引入 loadbancer 的东西,而这个就是 ribbon 的核心。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-commons</artifactId>
</dependency>
  1. 通过案例分析,restTemplate 需要使用 @LoadBalanced 实现负载均衡,那么首先看下 @LoadBalanced,注释中翻译下就是这个注解用来标记 restTemplate 和 webClient 的 bean作为配置使用 LoadBalancerClient, 而 LoadBalancerClient 则是一个接口,定义了执行请求的方法。
@LoadBalanced
@Bean("xxxTemplate")// 自定义 bean name,方便后续区分。
public RestTemplate restTemplate() {
    return new RestTemplate();
}
/**
 * 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 {
}
  1. 在 loaderbanlancer 包下,查看下所有类,发现 LoadBalancerAutoConfiguration 自动配置类,那么很显然这个类就是实现负载均衡的配置加载类了,其中该类注解上添加了 LoadBalancerClient.class 和 RestTemplate.class,表示需要这两个 bean 都存在才会执行使用该类,而 LoadBalancerClient 该接口,只有一个实现,那么就是 RibbonLoadBalancerClient,也就是负载均衡客户端,如下图。
    Nacos下 Ribbon 原理分析
  2. 然后继续分析,看到了 restTemplates 将所有的 restTemplate 进行自动注入,这里可以看出是要将每一个 restTemplate 装配功能,实现负载均衡。这里可以尝试运行 springboot 项目,看下要进行什么操作。
  3. 初始化 LoadBalancerInterceptor, 然后将 LoadBalancerInterceptor 拦截器配置到 restTemplate 中。
@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);
    }

    // 将拦截器封装到 restTemplate
    @Bean
    @ConditionalOnMissingBean
    public RestTemplateCustomizer restTemplateCustomizer(
            final LoadBalancerInterceptor loadBalancerInterceptor) {
        return restTemplate -> {
            List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                    restTemplate.getInterceptors());
            list.add(loadBalancerInterceptor);
            restTemplate.setInterceptors(list);
        };
    }
}
  1. LoadBalancerInterceptor 作用
    在拦截器中获取到 originalUri,serviceName,后续猜测就是通过 serviceName,然后向注册中心中,获取到对应的注册的服务ip,最后将最开始的 url,进行替换。测试代码如下:已经在 nacos 上注册了两个 nacos-component-provider 服务,端口号分别是 8080 和 8081,url=http://nacos-component-provider/hystrixI/1,然后一步一步进行 debug,观察 restTemplate 的执行情况。
@GetMapping(value = "/hystrixII/{id}")
public String testRestTemplate(@PathVariable Integer id){
    return restTemplate.getForObject("http://nacos-component-provider/hystrixI/" + id, String.class);
}

2 restTemplate 负载均衡执行过程

  1. restTemplate 执行,首先设置返回的信息格式,因为还没有具体的执行。
    Nacos下 Ribbon 原理分析
  2. 添加 URI 的扩展属性,例如参数等,然后调用RestTemplate#execute(String, HttpMethod, RequestCallback, ResponseExtractor, java.lang.Object…)执行请求
    Nacos下 Ribbon 原理分析
  3. 创建 request 请求,然后执行请求方法
    Nacos下 Ribbon 原理分析
  4. 判断该请求是否已经执行过,如果是,那么就会抛出异常,否则继续执行。
    Nacos下 Ribbon 原理分析
  5. 接着执行 InterceptingClientHttpRequest#executeInternal,主要是执行拦截器中的方法,执行完后,最后才会执行最终发送请求url的方法。
    Nacos下 Ribbon 原理分析
  6. 由于这里 restTemplate 请求中的拦截器列表只有一个,那么就直接就可以走到 LoadBalancerInterceptor#intercept 的方法,这里就是发送请求的核心方法。
    Nacos下 Ribbon 原理分析
  7. 这个 loadBalancer 就是 RibbonLoadBalancerClient,执行负载均衡的功能
    Nacos下 Ribbon 原理分析
  8. 接着调用 RibbonLoadBalancerClient#execute(String, LoadBalancerRequest, Object) 方法,getLoadBalancer(serviceId) 方法,这里使用 serviceId,这里主要是为了做一个缓存,当再次访问该负载均衡器时,不用再去创建一个。
    Nacos下 Ribbon 原理分析
  9. 调用 RibbonLoadBalancerClient#getLoadBalancer 方法,获取该服务的负载均衡器,如果这里第一次获取时会使用IOC的原理,由于 ILoadBalancer.class 类型的实例没有,那么就会去创建一个ZoneAwareLoadBalancer 对象,代码如下,ZoneAwareLoadBalancer 负载均衡器:具备区域意识、动态服务列表的负载均衡器,同时它继承 DynamicServerListLoadBalancer,也就是说会同步 nacos上注册的服务信息节点。
public ILoadBalancer getLoadBalancer(String name) {
    return getInstance(name, ILoadBalancer.class);
}
// 当没有 ILoadBalancer 类型的对象时,则会去创建一个,执行下面的方法
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
        ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
        IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
    if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
        return this.propertiesFactory.get(ILoadBalancer.class, config, name);
    }
    // 该ZoneAwareLoadBalancer负载均衡器:具备区域意识、动态服务列表的负载均衡器
    return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
            serverListFilter, serverListUpdater);
}
  1. 负载均衡器就是 ZoneAwareLoadBalancer(动态服务列表负载均衡器),主要负责获取从 nacos 上已经注册好的服务,或者是移除已经下线的服务。每一个服务都有对应的负载均衡器。
    Nacos下 Ribbon 原理分析
  2. getServer(loadBalancer, hint) 方法,这里就可以猜想出来,对应的负载均衡器,就会将我们请求的 url 根据某种规则,轮询、随机等规则,选择一个服务进行使用,默认是采用轮询的方式去获取实例。
    Nacos下 Ribbon 原理分析
  3. 执行 PredicateBasedRule#choose 方法中的chooseRoundRobinAfterFiltering 方法:从nacos上读取到的该 serviceName 对应的所有 serverList,过滤出可用的服务,然后进行轮询。下面是轮询的规则的具体实现
    Nacos下 Ribbon 原理分析
// 轮询的规则的具体实现
//modulo:可用服务列表的大小
private int incrementAndGetModulo(int modulo) {
    for (;;) {
        // 该类维护一个自增器,然后每次访问都+1,对可用列表进行取模,实现轮询的效果
        int current = nextIndex.get();
        int next = (current + 1) % modulo;
        if (nextIndex.compareAndSet(current, next) && current < modulo)
            return current;
    }
}
  1. 根据服务名和负载均衡器,获取到一个服务信息,包括 ip:port 等信息,用于后续替换 url 请求。
    Nacos下 Ribbon 原理分析
  2. request.apply(serviceInstance) 这是一个函数接口,具体的执行的方法是 this.requestFactory.createRequest(request, body, execution),这个方法就是原来httpRequest 进行封装为 ServiceRequestWrapper serviceRequest,然后使用该 serviceRequest 去执行请求。
    Nacos下 Ribbon 原理分析
  3. 接着又回到了 InterceptingClientHttpRequest#execute(HttpRequest, byte[])方法,由于目前已经执行将所有的拦截器都执行完了,那么现在会执行 else 中的部分,真正的去发送请求,下面代码就是构建后新的 URI 请求。
//这个就是 serviceRequest.getURI(),将原来的http://nacos-component-provider/hystrixI/1,替换为 ip:port形式的请求
public URI getURI() {
    // 执行替换的方法
    URI uri = this.loadBalancer.reconstructURI(this.instance, getRequest().getURI());
    return uri;
}

Nacos下 Ribbon 原理分析
16. 最后构造一个委托类 delegate, 执行修改后的 URI,后续的代码就不需要再 debug 了,因为都是 http 请求相关的,处理响应后的内容等。
Nacos下 Ribbon 原理分析

上一篇:ribbon灰度发布


下一篇:忘忧阁--Java8系列之Optional类(五)