客户端负载均衡 Spring Cloud Ribbon

前言

文章中所说源码版本:

  • spring-cloud-starter-netflix-ribbon : 2.0.0.RELEASE
  • spring-cloud-alibaba-nacos : 2.2.5.RELEASE
  • nacos-example: master

简介

Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。通过Spring Cloud 的封装,可以让我们轻松地将面向服务的 REST 模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过 Ribbon 来实现的。所以,对Spring Cloud Ribbon 的理解和使用,对于我们使用 Spring Cloud 来构建微服务非常重要。

客户端负载均衡

如下图是简单的负载均衡的工作方式,在客户端调用服务端时,不直接调用,而是请求负载均衡器,通过负载均衡算法选择一个服务端的信息返回给客户端,然后再进行调用。 image.png

如何使用 ribbon

添加对应依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    <version>${spring-cloud-netflix.version}</version>
</dependency>
复制代码

restTemplate 添加 @Loadbalance 注解

@LoadBalanced
@Bean
public RestTemplate restTemplate() {
    return new RestTemplate();
}
复制代码

直接使用 restTemplate 进行服务调用

@RestController
public class TestController {

    private final RestTemplate restTemplate;

    @Autowired
    public TestController(RestTemplate restTemplate) {this.restTemplate = restTemplate;}

    @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
    public String echo(@PathVariable String str) {
        return restTemplate.getForObject("http://service-provider/echo/" + str, String.class);
    }
}
复制代码

观察这个调用,我们发现在使用 restTemplate 时将服务名 service-provider 作为域名直接调用了。但是我们其实没有配置这个域名的解析,这里是如何查找到 service-provider 后端的 ip 并发起调用的?如果后端服务有多个节点,如何进行选择?带着问题,我们往下看 ribbon 的源码解析。

Ribbon 源码解析

@LoadBalance 注解做了啥

这里其实大家可以尝试一下,如果不给 restTemplate 添加 @Loadbalance 注解的话,service-provider 这个服务名是没办法解析的。首先我们看看 @Loadbalance 这个注解到底做了什么。

@LoadBalanced

/**
 * Annotation to mark a RestTemplate 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 {
}
复制代码

我们看看注解的定义,仅有一个 @Qualifier 和注释中的内容:标记 RestTemplate 以使用 LoadBalancerClient。我们再看看这个注解除了我们的工程中用到的,还有哪些地方使用到了这个注解。

LoadBalancerAutoConfiguration

org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration 中,获取了所有添加了 @Loadbalanced 注解的 RestTemplate。在 loadBalancedRestTemplateInitializerDeprecated 中获取了所有的 RestTemplateCustomizer 并执行了 customize() 方法。

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();

@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
                final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
        return () -> restTemplateCustomizers.ifAvailable(customizers -> {
    for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
        for (RestTemplateCustomizer customizer : customizers) {
            customizer.customize(restTemplate);
        }
    }
});
}
复制代码

这里的 RestTemplateCustomizer 也是在该配置下声明的:

@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);
        };
}
复制代码

可以看到这里的 restTemplateCustomizer 中仅仅是向 restTemplate 中添加了一个 loadBalancerInterceptor,而 loadBalancerInterceptor 是在 org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.LoadBalancerInterceptorConfig#ribbonInterceptor 方法中声明的一个 bean。

到这个可以基本了解和确认,@Loadbalanced 的作用是添加一个名为 loadBalancerInterceptor 的过滤器到 restTemplate 中。

如何获取到服务对应的节点列表

这里我们通过 restTemplate 的调用过程来分析 loadbalance 负载的实现。

RestTemplate#doExecute

我们直接来看 org.springframework.web.client.RestTemplate#doExecute 方法,因为不管是 GET 还是 POST 等方法,最终调用的都是 doExecute 方法:

@Nullable
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
      @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

   Assert.notNull(url, "URI is required");
   Assert.notNull(method, "HttpMethod is required");
   ClientHttpResponse response = null;
   try {
      ClientHttpRequest request = createRequest(url, method);
      if (requestCallback != null) {
         requestCallback.doWithRequest(request);
      }
      response = request.execute();
      handleResponse(url, method, response);
      return (responseExtractor != null ? responseExtractor.extractData(response) : null);
   }
   catch (IOException ex) {
      String resource = url.toString();
      String query = url.getRawQuery();
      resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
      throw new ResourceAccessException("I/O error on " + method.name() +
            " request for "" + resource + "": " + ex.getMessage(), ex);
   }
   finally {
      if (response != null) {
         response.close();
      }
   }
}
复制代码

这个方法,主要做了三件事:

  • 通过 createRequest(url, method) 方法获取请求的 request 对象。
  • 发起请求调用。
  • 处理返回数据,并返回给调用方。

看起来如果要处理请求 uri 中的域名信息,应该是在 createRequest 时处理的。

HttpAccessor#createRequest

protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
   ClientHttpRequest request = getRequestFactory().createRequest(url, method);
   if (logger.isDebugEnabled()) {
      logger.debug("Created " + method.name() + " request for "" + url + """);
   }
   return request;
}
复制代码

这个方法是用来创建一个请求对象的。

InterceptingHttpAccessor#getRequestFactory

@Override
public ClientHttpRequestFactory getRequestFactory() {
   // 获取客户端请求的所有拦截器
   List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
   if (!CollectionUtils.isEmpty(interceptors)) {
      ClientHttpRequestFactory factory = this.interceptingRequestFactory;
      // 构建一个 InterceptingClientHttpRequestFactory 工厂,并且将所有的拦截器传入
      if (factory == null) {
         factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
         this.interceptingRequestFactory = factory;
      }
      return factory;
   }
   else {
      return super.getRequestFactory();
   }
}
复制代码

InterceptingHttpAccessor#getInterceptors

这里的 getInterceptors 就会获取所有的拦截器列表,而在上一节 @Loadbalanced 会对所有的 RestTemplate 添加一个 loadBalancerInterceptor 拦截器。

HttpAccessor#createRequest

而再返回到 createRequest 方法,这里调用的其实就是 InterceptingClientHttpRequestFactory#createRequest 方法,返回 InterceptingClientHttpRequest 对象到 doExecute

RestTemplate.doExecute

creatRequest 继续往下看,在获取到 request 对象后,调用了 request.execute()

response = request.execute();
复制代码

AbstractClientHttpRequest#execute

@Override
public final ClientHttpResponse execute() throws IOException {
   assertNotExecuted();
   ClientHttpResponse result = executeInternal(this.headers);
   this.executed = true;
   return result;
}
复制代码

在父类的 execute 方法中会调用 executeInternal(this.headers)。而通过上面的分析,我们知道这里的 request 对象是 InterceptingClientHttpRequest

InterceptingClientHttpRequest#executeInternal

@Override
protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
   InterceptingRequestExecution requestExecution = new InterceptingRequestExecution();
   return requestExecution.execute(this, bufferedOutput);
}
复制代码

这里再次调用了 InterceptingRequestExecution#execute

InterceptingRequestExecution#execute

@Override
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
   if (this.iterator.hasNext()) {
      ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
      return nextInterceptor.intercept(request, body, this);
   }
   else {
      HttpMethod method = request.getMethod();
      Assert.state(method != null, "No standard HTTP method");
      ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
      request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
      if (body.length > 0) {
         if (delegate instanceof StreamingHttpOutputMessage) {
            StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate;
            streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream));
         }
         else {
            StreamUtils.copy(body, delegate.getBody());
         }
      }
      return delegate.execute();
   }
}
复制代码

这个方法主要有两个处理逻辑:

  • 如果还有下一个拦截器,则调用拦截器的 intercept 方法对请求进行拦截。
  • 否则,按照正常处理逻辑进行远程调用

发起远程调用其实就是建立 HttpConnection 进行远程通信。这里我们主要关心在调用过程中 LoadBalancerInterceptor 是如何发挥作用的。

通过前面的分析,我们知道,这里最少还有一个 LoadBalancerInterceptor 是由 @Loadbalanced 添加的。

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();
   Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
   return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
复制代码

这里的 loadBalancer 是在构造参数中传入的,其实就是 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);
}
复制代码
  • 根据 serviceId 获取一个 ILoadBalancer
  • 调用 org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient#getServer 方法获取一个服务实例。
  • 判断 server 的值是否为空。这里的 server 其实就是实际的服务节点,存储了服务节点的一些元信息,比如 host、port 等。

RibbonLoadBalancerClient#getServer

protected Server getServer(ILoadBalancer loadBalancer) {
   if (loadBalancer == null) {
      return null;
   }
   return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
复制代码

这里实际上调用了 loadBalancer.chooseServer 这个方法,ILoadBalancer 这个是一个负载均衡接口,其类图如下:

image.png

  • AbstractLoadBalancer 实现了 ILoadBalancer 接口,定义服务分组的枚举。
  • BaseLoadBalancer 实现了负载均衡器的基本功能。比如服务列表维护、服务存活状态检测、负载均衡算法选择 server 等。但是这里只是实现了基本功能,有些复杂场景还无法实现,比如动态服务列表,server 过滤等。
  • DynamicServerListLoadBalancer 是 BaseLoadbalancer 的一个子类,它对基础负载均衡提供了扩展,从名字上可以看出,它提供了动态服务列表的特性。
  • ZoneAwareLoadBalancer 它是在DynamicServerListLoadBalancer的基础上,增加了以Zone的 形式来配置多个LoadBalancer的功能。

这里可以看到 org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration#ribbonLoadBalancer 中默认采用的是 ZoneAwareLoadBalancer;

@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);
   }
   return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
         serverListFilter, serverListUpdater);
}
复制代码

ZoneAwareLoadBalancer#chooseServer

这个方法的主要逻辑:

  • 如果没有开启区域配置,且 zone 的数量小于等于 1 ,不用做区域相关的处理,直接调用父类方法返回。
  • 获取所有可用状态的可用区 -> ZoneAvoidanceRule.getAvailableZones.
  • 从可用区列表中随机选择一个可用区 -> ZoneAvoidanceRule.randomChooseZone.
  • 再从选出来的可用区中选择一个 server -> zoneLoadBalancer.chooseServer
public Server chooseServer(Object key) {
    if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
        logger.debug("Zone aware logic disabled or there is only one zone");
        return super.chooseServer(key);
    }
    Server server = null;
    try {
        LoadBalancerStats lbStats = getLoadBalancerStats();
        Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
        logger.debug("Zone snapshots: {}", zoneSnapshot);
        if (triggeringLoad == null) {
            triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
                    "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
        }

        if (triggeringBlackoutPercentage == null) {
            triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
                    "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
        }
        Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
        logger.debug("Available zones: {}", availableZones);
        if (availableZones != null &&  availableZones.size() < zoneSnapshot.keySet().size()) {
            String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
            logger.debug("Zone chosen: {}", zone);
            if (zone != null) {
                BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
                server = zoneLoadBalancer.chooseServer(key);
            }
        }
    } catch (Exception e) {
        logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
    }
    if (server != null) {
        return server;
    } else {
        logger.debug("Zone avoidance logic is not invoked.");
        return super.chooseServer(key);
    }
}
复制代码

BaseLoadBalancer#chooseServer

public Server chooseServer(Object key) {
    if (counter == null) {
        counter = createCounter();
    }
    counter.increment();
    if (rule == null) {
        return null;
    } else {
        try {
            return rule.choose(key);
        } catch (Exception e) {
            logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
            return null;
        }
    }
}
复制代码

分析到这里,我们已经搞明白 Ribbon 是如何运用负载均衡算法从服务列表中获取一个目标服务的地址并进行访问的。接下来我们继续与服务列表的动态加载过程。

在与注册中心结合时,服务列表怎么加载

这块因为是动态加载服务列表,通过前面的分析,我们已经知道 DynamicServerListLoadBalancer 是 BaseLoadbalancer 的一个子类提供了动态服务列表的特性。

DynamicServerListLoadBalancer#DynamicServerListLoadBalancer

public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
                                     ServerList<T> serverList, ServerListFilter<T> filter,
                                     ServerListUpdater serverListUpdater) {
    super(clientConfig, rule, ping);
    this.serverListImpl = serverList;
    this.filter = filter;
    this.serverListUpdater = serverListUpdater;
    if (filter instanceof AbstractServerListFilter) {
        ((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
    }
    restOfInit(clientConfig);
}
复制代码

DynamicServerListLoadBalancer 的构造方法中,调用了 com.netflix.loadbalancer.DynamicServerListLoadBalancer#restOfInit 方法用于初始化或者更新服务列表。

DynamicServerListLoadBalancer#restOfInit

void restOfInit(IClientConfig clientConfig) {
    boolean primeConnection = this.isEnablePrimingConnections();
    // turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
    this.setEnablePrimingConnections(false);
    enableAndInitLearnNewServersFeature();
    // 更新服务列表
    updateListOfServers();
    if (primeConnection && this.getPrimeConnections() != null) {
        this.getPrimeConnections()
                .primeConnections(getReachableServers());
    }
    this.setEnablePrimingConnections(primeConnection);
    LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
}
复制代码

这里我们主要分析 updateListOfServers 方法。

DynamicServerListLoadBalancer#updateListOfServers

@VisibleForTesting
public void updateListOfServers() {
    List<T> servers = new ArrayList<T>();
    if (serverListImpl != null) {
        servers = serverListImpl.getUpdatedListOfServers();
        LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
                getIdentifier(), servers);

        if (filter != null) {
            servers = filter.getFilteredListOfServers(servers);
            LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
                    getIdentifier(), servers);
        }
    }
    updateAllServerList(servers);
}
复制代码

updateListOfServers 主要做了几个逻辑:

  • 判断当前的 serverListImpl 是否为空,如果不为空,则执行具体是 serverList 获取服务列表并更新。
  • 如果 serverListImple 为空,更新服务列表为空列表。

那这里的 serverListImpl 是怎么来的呢?

这里看前面的构造方法,是构造方法注入的,在 org.springframework.cloud.alibaba.nacos.ribbon.NacosRibbonClientConfiguration#ribbonServerList 配置中,注入了 nacos 的 serverList。

所以这里的 serverListImpl.getUpdatedListOfServers() 我们直接看 nacos 的实现即可。

NacosServerList#getUpdatedListOfServers

@Override
public List<NacosServer> getUpdatedListOfServers() {
   return getServers();
}

private List<NacosServer> getServers() {
   try {
      List<Instance> instances = discoveryProperties.namingServiceInstance()
            .selectInstances(serviceId, true);
      return instancesToServerList(instances);
   }
   catch (Exception e) {
      throw new IllegalStateException(
            "Can not get service instances from nacos, serviceId=" + serviceId,
            e);
   }
}
复制代码

这里看着就比较清晰了,其实就是请求了 nacos 的 namingService 服务,获取所有的服务节点信息并返回。

RibbonLoadBalancerClient#execute

再次回到 RibbonLoadBalancerClient#execute 方法中,这里 getServer(loadBalancer) 返回了从 nacos 中获取到服务列表信息,并根据负载均衡规则选择一个服务节点。

@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);
}
复制代码

execute

这里调用了另一个 execute 的重载方法,在这个方法中终会调用apply方法,这个方法会向一个具体的实例发送请求。

@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
   Server server = null;
   if(serviceInstance instanceof RibbonServer) {
      server = ((RibbonServer)serviceInstance).getServer();
   }
   if (server == null) {
      throw new IllegalStateException("No instances available for " + serviceId);
   }

   RibbonLoadBalancerContext context = this.clientFactory
         .getLoadBalancerContext(serviceId);
   RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);

   try {
       // 请求发起
      T returnVal = request.apply(serviceInstance);
      statsRecorder.recordStats(returnVal);
      return returnVal;
   }
   // catch IOException and rethrow so RestTemplate behaves correctly
   catch (IOException ex) {
      statsRecorder.recordStats(ex);
      throw ex;
   }
   catch (Exception ex) {
      statsRecorder.recordStats(ex);
      ReflectionUtils.rethrowRuntimeException(ex);
   }
   return null;
}
复制代码

LoadBalancerRequest#apply

request 是 LoadBalancerRequest 接口,它里面提供了一个 apply 方法。这个参数是在 org.springframework.cloud.client.loadbalancer.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();
   Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
   return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
复制代码

这里请求了 requestFactory.createRequest(request, body, execution)

LoadBalancerRequestFactory#createRequest

public LoadBalancerRequest<ClientHttpResponse> createRequest(final HttpRequest request,
      final byte[] body, final ClientHttpRequestExecution execution) {
   return instance -> {
           // 包装 request 对象
           HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
           if (transformers != null) {
               for (LoadBalancerRequestTransformer transformer : transformers) {
                   serviceRequest = transformer.transformRequest(serviceRequest, instance);
               }
           }
           return execution.execute(serviceRequest, body);
       };
}
复制代码

从代码中可以看到,最终传递的 request 对象,被 ServiceRequestWrapper 做了一下包装。

ServiceRequestWrapper#getURI

@Override
public URI getURI() {
   URI uri = this.loadBalancer.reconstructURI(
         this.instance, getRequest().getURI());
   return uri;
}
复制代码

RibbonLoadBalancerClient#reconstructURI

@Override
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);

   URI uri;
   Server server;
   if (instance instanceof RibbonServer) {
      RibbonServer ribbonServer = (RibbonServer) instance;
      server = ribbonServer.getServer();
      uri = updateToSecureConnectionIfNeeded(original, ribbonServer);
   } else {
      server = new Server(instance.getScheme(), instance.getHost(), instance.getPort());
      IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);
      ServerIntrospector serverIntrospector = serverIntrospector(serviceId);
      uri = updateToSecureConnectionIfNeeded(original, clientConfig,
            serverIntrospector, server);
   }
   return context.reconstructURIWithServer(server, uri);
}
复制代码

reconstructURI这个方法,实际上是重构URI,也就是把一个 http://服务名/ 转化为 http://地址/ 的过程。

  • 首先获得一个 serviceId 。
  • 根据 serviceId 获得一个 RibbonLoadBalancerContext 对象,这个是用来存储一些被负载均衡器使用的上下文内容。
  • 调用 reconstructURIWithServer 方法来构建服务实例的URI

LoadBalancerContext#reconstructURIWithServer

这里的实现逻辑比较好理解,首先从 Server 中获得 host 和 port 信息。然后将原始的以服务名为 host 的 uri 替换为目标服务器的地址

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;
    }
    if (scheme == null) {
        scheme = original.getScheme();
    }
    if (scheme == null) {
        scheme = 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 e) {
        throw new RuntimeException(e);
    }
}
复制代码

搞明白了 URI 的转换过程,我们再回到 createRequest 方法,终会调用 execution.execute 来创建一个 ClientHttpRespose 对象。这里实际上是调用 ClientHttpRequestExcution 接口的 execute 方法。其实就是通过已经转换好的 url 发起远程通信了,这里后续的请求过程就不再继续分析了。

ribbon 源码流程图

image.png

如上就是根据本篇文章的源码分析整理出的调用流程图。

总结

本小节主要讨论了 Spring Cloud Ribbon 是如何实现负载均衡的。

主要有以下几点:

  1. @Loadbalanced 注解其实是为 RestTemplate 增加了一个 LoadBalancerInterceptor 的拦截器,用于拦截所有的请求。
  2. 拦截器的主要行为是获取当前服务的可用服务节点,并通过负载均衡算法选择一个节点作为最终调用的节点。并重构 url 中的 serviceId 为实际节点的 IP:PORT ,再进行 HTTP 调用。
  3. 在于注册中心,如 Nacos 整合时,ZoneAwareLoadBalancer 的父类 DynamicServerListLoadBalancer 会请求 NacosServerList#getUpdatedListOfServers 方法获取所有可用的服务节点并添加到缓存中。
分类:
后端