Spring Cloud之负载均衡与服务调用

580 阅读17分钟

负载均衡

负载均衡(Load Balancing)是一种计算机技术,用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源中分配负责,以达到最优化资源使用、最大化吞吐量、最小化响应时间,同时避免出现过载的目的。

常见的负载均衡算法:

  • 随机(Random)算法:在实例列表中随机选择某个实例。

  • 轮询(RoundRobin)算法:循环取下一个。

  • 最少连接数(Least Connections)算法:每次取连接数最少的实例。

  • 一致性(Consistent Hashing)算法:基于一致性哈希算法总是将相同参数的请求落在同一实例上。

  • 权重随机(Weightd Random)算法:根据权重+随机选择某个实例。

Spring Cloud LoadBalancer负载均衡组件

SCL是新一代Spring Cloud客户端负载均衡的实现。2019年7月3日,在Hoxton.M1的发布公告上。Spring宣布更新该项目来代替Netflix Ribbon。

SCL相关的代码在spring-cloud-commons模块:

  • ServiceInstanceChooser:服务实例选择器,根据服务名获取一个服务实例(ServiceInstance)。

  • LoadBalancerClient:客户端负责均衡器,继承ServiceInstanceChooser,会根据ServiceInstanceRequest请求信息执行最终结果。

  • BlockingLoadBalancerClient:基于Spring Cloud LoadBalancer的LoadBalancerClient默认实现。

  • RibbonLoadBalancerClient:基于Netflix Ribbon的LoadBalancerClient实现。

image.png

ServiceInstanceChooser

ServiceInstanceChooser服务实例选择器定义:

public interface ServiceInstanceChooser {

   /**
    * 根据服务名得到一个服务实例
    * @param serviceId 服务名
    * @return 对应服务名下的服务实例
    */
   ServiceInstance choose(String serviceId);

}

自定义RandomServiceInstanceChooser(随机算法)获取ServiceInstance,再使用RestTemplate进行服务调用。

// 自定义服务实例选择器
public class RandomServiceInstanceChooser implements ServiceInstanceChooser {

    private final DiscoveryClient discoveryClient;

    private final Random random;

    public RandomServiceInstanceChooser(DiscoveryClient discoveryClient) {
        this.discoveryClient = discoveryClient;
        random = new Random();
    }

    @Override
    public ServiceInstance choose(String serviceId) {
        List<ServiceInstance> serviceInstanceList =
                discoveryClient.getInstances(serviceId);
        return serviceInstanceList.get(random.nextInt(serviceInstanceList.size()));
    }
}
    // 方式一
    @Bean
    public RestTemplate normalRestTemplate() {
        return new RestTemplate();
    }

    @Bean
    public RandomServiceInstanceChooser randomServiceInstanceChooser(DiscoveryClient discoveryClient) {
        return new RandomServiceInstanceChooser(discoveryClient);
    }

    @GetMapping("/customChooser")
    public String customChooser() {
      ServiceInstance serviceInstance = randomServiceInstanceChooser.choose(serviceName);
      return normalRestTemplate.getForObject(
        "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/", String.class);
    }
    // 方式二
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate;
    }
    @GetMapping("/customChooser")
    public String customChooser() {
      return restTemplate.getForObject("http://" + serviceName + "/", String.class);
    }

@LoadBalanced

spring-cloud-commons模块中的META-INF/spring-factories文件里存在LoadBalancerAutoConfiguration这个自动化配置类,根据工厂加载机制会被ApplicationContext加载。自动化配置类内部的Bean构造代码如下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

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

   @Autowired(required = false)
   private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

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

}
  1. 获取ApplicationContext中所有被@LoadBalanced注解修饰的RestTemplate

  2. List<RestTemplateCustomizer>>是ApplicationContext存在的RestTemplateCustomizer Bean的集合。

  3. 遍历RestTemplate集合,并使用RestTemplateCustomizer集合给每个RestTemplate定制。

//org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.java
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate") // 1
static class LoadBalancerInterceptorConfig {

   @Bean
   public LoadBalancerInterceptor ribbonInterceptor( // 2
         LoadBalancerClient loadBalancerClient,
         LoadBalancerRequestFactory requestFactory) {
      return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
   }

   @Bean
   @ConditionalOnMissingBean
   public RestTemplateCustomizer restTemplateCustomizer( // 3
         final LoadBalancerInterceptor loadBalancerInterceptor) { // 4 
      return restTemplate -> {
         List<ClientHttpRequestInterceptor> list = new ArrayList<>(
               restTemplate.getInterceptors());
         list.add(loadBalancerInterceptor); // 5
         restTemplate.setInterceptors(list);
      };
   }

}
  1. 条件注解。LoadBalancerInterceptorConfig配置类只有在ClassLoader不存在RetryTemplate时才会生效。

  2. 定义LoadBalancerInterceptorBean,这个拦截器继承ClientHttpRequestInterceptor,可以被添加到RestTemplate拦截器列表中。

  3. 定义RestTemplateCustomizerBean,会在LoadBalancerAutoConfiguration里的RestTemplateCustomizer列表中存在。

  4. LoadBalancerInterceptor参数是代码2处创建的Bean。

  5. 使用lambda表达式在RestTemplate的拦截器列表添加LoadBalancerInterceptor拦截器。

如果ClassLoader存在RetryTemplate,会触发另外一个配置类:RetryInterceptorAutoConfiguration。该配置类内部的操作与LoadBalancerInterceptorConfig配置类唯一的区别就是构造RetryLoadBalancerInterceptor拦截器(跟LoadBalancerInterceptor相比,在RestTemplate调用失败的情况下会进行重试操作)。

LoadBalancerInterceptor拦截器:

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

   private LoadBalancerClient loadBalancer;

   private LoadBalancerRequestFactory requestFactory;

   public LoadBalancerInterceptor(LoadBalancerClient loadBalancer,
         LoadBalancerRequestFactory requestFactory) { // 1
      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(); // 2
      Assert.state(serviceName != null,
            "Request URI does not contain a valid hostname: " + originalUri);
      return this.loadBalancer.execute(serviceName, // 3
            this.requestFactory.createRequest(request, body, execution));
   }

}
  1. LoadBalancerInterceptor构造器需要LoadBalancerClientLoadBalancerRequestFactory参数(默认会在LoadBalancerAutoConfiguration里被构造,开发者可以进行覆盖)。前者需要根据负载均衡请求和服务名做真正的服务调用,后者构造负载均衡请求,构造过程中会使用LoadBalancerRequestTransformer对请求做一些自定义转换操作(默认情况下,LoadBalancerRequestTransformer接口无任何实现类,开发者可以根据业务构造Bean进行Request的转换操作)。

  2. 服务名使用URI中的host信息。

  3. 使用LoadBalancerClient客户端负责均衡器做真正的服务调用。

LoadBalancerClient

LoadBalancerClient(客户端负载均衡器)会根据负载均衡器请求和服务名执行真正的负载均衡操作,该接口具体定义如下:

public interface LoadBalancerClient extends ServiceInstanceChooser {

   /**
    * 使用负载均衡得到的 ServiceInstance 为指定的服务执行请求
    * @param serviceId 服务名
    * @param request 负载均衡请求
    * @param <T> type of the response
    * @throws IOException in case of IO issues.
    * @return 基于选中的服务实例 ServiceInstance 在 LoadBalancerRequest 回调中的返回结果
   <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;

   /**
    * 使用负载均衡得到的 ServiceInstance 为指定的服务执行请求
    * @param serviceId 服务名
    * @param serviceInstance 服务实例
    * @param request 负载均衡请求
    * @param <T> type of the response
    * @throws IOException in case of IO issues.
    * @return 基于选中的服务实例 ServiceInstance 在 LoadBalancerRequest 回调中的返回结果
    */
   <T> T execute(String serviceId, ServiceInstance serviceInstance,
         LoadBalancerRequest<T> request) throws IOException;

   /**
    * 使用服务实例的 ServiceInstance 中的host和port属性构造出真正的URI
    * @param instance 服务实例
    * @param original 带有服务名的URLI
    * @return 重新构造的URI
    */
   URI reconstructURI(ServiceInstance instance, URI original);

}
  • reconstructURI方法。这个方法用于重新构造URI。比如,要访问nacos-provider-lb服务下的“/”路径,这个URI为http://nacos-provider-lb/。nacos-provider-lb服务在注册中心有10个服务实例,某个服务实例ServiceInstance的IP为192.168.1.100,端口为8080.那么重新构造的真正的URI为http://192.168.1.100:8080/

  • execute方法。有两个重载方法,其中一个方法比另外一个方法多了ServiceInstance服务实例参数。没有ServiceInstance参数的方法内部会通过choose方法(父接口ServiceInstanceChooser提供)使用负载均衡算法得到一个ServiceInstance,然后调用带有ServiceInstance参数的execute方法。

LoadBalancerClient默认实现类为基于SCL的BlockingLoadBalancerClient

public class BlockingLoadBalancerClient implements LoadBalancerClient {

   private final LoadBalancerClientFactory loadBalancerClientFactory;

   public BlockingLoadBalancerClient(
         LoadBalancerClientFactory loadBalancerClientFactory) {
      this.loadBalancerClientFactory = loadBalancerClientFactory; // 1
   }

   @Override
   public <T> T execute(String serviceId, LoadBalancerRequest<T> request)
         throws IOException {
      ServiceInstance serviceInstance = choose(serviceId); // 2
      if (serviceInstance == null) {
         throw new IllegalStateException("No instances available for " + serviceId);
      }
      return execute(serviceId, serviceInstance, request);
   }

   @Override
   public <T> T execute(String serviceId, ServiceInstance serviceInstance,
         LoadBalancerRequest<T> request) throws IOException {
      try {
         return request.apply(serviceInstance); // 3
      }
      catch (IOException iOException) {
         throw iOException;
      }
      catch (Exception exception) {
         ReflectionUtils.rethrowRuntimeException(exception);
      }
      return null;
   }

   @Override
   public URI reconstructURI(ServiceInstance serviceInstance, URI original) {
      return LoadBalancerUriTools.reconstructURI(serviceInstance, original); // 4
   }

   @Override
   public ServiceInstance choose(String serviceId) {
      ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory
            .getInstance(serviceId); //5
      if (loadBalancer == null) {
         return null;
      }
      Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose())
            .block();
      if (loadBalancerResponse == null) {
         return null;
      }
      return loadBalancerResponse.getServer();
   }

}
  1. BlockingLoadBalancerClient构造函数依赖LoadBalancerClientFactoryLoadBalancerClientFactory是一个用于创建ReactiveLoadBalancer的工厂类,LoadBalancerClientFactory内部维护着一个Map,该Map用于保存各个服务的ApplicationContext(Map的key是服务名)。每个ApplicationContext内部维护对应服务的一些配置和Bean。

  2. 没有ServiceInstance参数的execute方法内部会调用choose方法获取ServiceInstance,然后调用另外一个重载的execute方法。

  3. ServiceInstance参数的execute方法把负载均衡操作直接委托给LoadBalancerRequest负载均衡请求处理。

  4. 根据URI和找到的服务实例ServiceInstance重新构造一个URI,这个过程被封装在LoadBalancerUriTools工具类里。

  5. 代码2处提到的choose方法会返回服务实例ServiceInstancechoose方法首先会根据服务名和loadBalancerClientFactory得到的该服务名对应的ReactiveLoadBalancerBean,然后调用ReactiveLoadBalancerchoose方法得到服务实例ServiceInstance

总结

  1. @LoadBalanced注解修饰RestTemplate后,会根据RestTemplateCustomizerRestTemplate做定制化操作。这个定制化操作一定含有一个添加LoadBalancerInterceptor负载均衡拦截器的操作。此外,我们还可以扩展添加符合业务需求的自定义定制化操作。

    image 1.png

  2. LoadBalancerInterceptor负载均衡拦截器拦截的背后会通过LoadBalancerClientexecute方法完成最终的调用。

    image 2.png

Spring Cloud LoadBalancer还提供了@LoadBalancerClient注解用于进行自定义的配置操作。

@LoadBalancerClient注解有3个属性,分别是value:Stringname:Stringconfiguration:Class[],name和value属性表示同一个含义,即服务名,且只能设置一个属性。

@LoadBalancerClients注解的defaultConfiguration属性表示默认的配置类,所有的BlockingLoadBalancerClient都会使用这些配置类里的配置。

Netflix Ribbon负载均衡

Netflix Ribbon是Netflix开源的客户端负载均衡组件,在Spring Cloud LoadBalancer出现之前,它是Spring Cloud 生态里唯一的负载均衡组件。

RibbonLoadBalancerClient

Netflix Ribbon对应的LoadBalancerClient实现类为RibbonLoadBalancerClient

public class RibbonLoadBalancerClient implements LoadBalancerClient {

   private SpringClientFactory clientFactory; // 1

   public RibbonLoadBalancerClient(SpringClientFactory clientFactory) {
      this.clientFactory = clientFactory; // 2
   }

   @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; // 3
      if (instance instanceof RibbonServer) {
         RibbonServer ribbonServer = (RibbonServer) instance; // 4
         server = ribbonServer.getServer();
         uri = updateToSecureConnectionIfNeeded(original, ribbonServer);
      }
      else {
         server = new Server(instance.getScheme(), instance.getHost(),
               instance.getPort()); // 5
         IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);
		 // 6
         ServerIntrospector serverIntrospector = serverIntrospector(serviceId);
         uri = updateToSecureConnectionIfNeeded(original, clientConfig,
               serverIntrospector, server);
      }
      return context.reconstructURIWithServer(server, uri); // 7
   }

   @Override
   public ServiceInstance choose(String serviceId) {
      return choose(serviceId, null);
   }

   public ServiceInstance choose(String serviceId, Object hint) {
      Server server = getServer(getLoadBalancer(serviceId), hint); // 8
      if (server == null) {
         return null;
      }
      return new RibbonServer(serviceId, server, isSecure(server, serviceId),
            serverIntrospector(serviceId).getMetadata(server)); // 9
   }

   @Override
   public <T> T execute(String serviceId, LoadBalancerRequest<T> request)
         throws IOException {
      return execute(serviceId, request, null);
   }

   public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
         throws IOException {
      ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
      Server server = getServer(loadBalancer, hint); // 10
      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);
   }

   @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);
      // 11
      RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);

      try {
         T returnVal = request.apply(serviceInstance); // 12
         statsRecorder.recordStats(returnVal); // 13
         return returnVal;
      }
      catch (IOException ex) {
         statsRecorder.recordStats(ex); 
         throw ex;
      }
      catch (Exception ex) {
         statsRecorder.recordStats(ex);
         ReflectionUtils.rethrowRuntimeException(ex);
      }
      return null;
   }
  ...

}
  1. SpringClientFactory是一个与LoadBalancerClientFactory作用类似的工厂类,其内部维护着一个Map,这个Map用于保存各个服务的ApplicationContext(Map的key表示服务名)。每个ApplicationContext内部维护对应服务的一些配置和Bean。

  2. SpringClientFactoryRibbonAutoConfiguration自动化配置类中被构造,可以通过构造器注入的方式注入。

  3. reconstructURI方法与Spring Cloud LoadBalancer中的BlockingLoadBalancerClient实现完全不一样。

    • BlockingLoadBalancerClient直接委托给LoadBalancerUriTools#reconstructURI方法实现,其内部使用ServiceInstance进行相应的属性替换;

    • RibbonLoadBalancerClient内部基于新的类com.netflix.loadbalancer.Server(表示一个服务器实例,内部有host、port、schema、zone等属性)来实现。

  4. RibbonServerRibbonLoadBalancerClient的内部类,其实现了ServiceInstance接口,内部维护着一个Server属性,同时还有其他3个属性:serviceId:String(服务名)、secure:boolean(是否使用HTTPS)、metadata:Map<String,String>(服务器实例等元数据)。

  5. 基于ServiceInstance构造Server

  6. ServerIntrospector接口可以根据Server调用isSecure和getMetadata方法获取secure和metadata信息。

  7. 使用RibbonLoadBalancerContext#reconstructURIWithServer方法基于Server和老的URI重新构造新的URI。

  8. choose方法返回负载均衡策略得到的最终实例,将负载均衡的操作委托给ILoadBalancer接口的实现类,默认的实现是ZoneAwareLoadBalancer

  9. 返回的服务实例ServiceInstance使用RibbonServer,secure和metadata使用ServerIntrospector获取。

  10. 调用过程与choose方法选择服务实例的步骤一致

  11. 每次服务调用会使用RibbonStatsRecorder内部的ServerStats对象进行数据统计。每个实例都有独立的ServerStats对象。

  12. 真正的服务调用操作,使用LoadBalancerRequest完成。

  13. 服务调用成功,进行状态记录。

SCL和Netflix Ribbon对应功能对比

功能/组件Spring Cloud LoadBalancerNetflix Ribbon
负载均衡ReactiveLoadBalancer & ServerInstanceListSupplierILoadBalancer & IRule
服务实例ServiceInstanceServiceInstance & RibbonServer & Server & ServerIntrospector
服务调用统计信息-ServerStats & LoadBalancerStats

RobbonServer和Server

Server表示一个服务器实例:

public class Server {

    ...

    public static final String UNKNOWN_ZONE = "UNKNOWN";
    private String host; // 域名
    private int port = 80; // 端口,默认80
    private String scheme; // schema,如http或https
    private volatile String id; // 服务器实例ID
    private volatile boolean isAliveFlag; // 是否还存活,如ping不通,则可能会false
    private String zone = UNKNOWN_ZONE; // 服务器所在的zone
    private volatile boolean readyToServe = true; // 是否可以对我提供服务
    // meta信息,云厂商可以有不同的操作
    private MetaInfo simpleMetaInfo = new MetaInfo() {
        @Override
        public String getAppName() {
            return null;
        }

        @Override
        public String getServerGroup() {
            return null;
        }

        @Override
        public String getServiceIdForDiscovery() {
            return null;
        }

        @Override
        public String getInstanceId() {
            return id;
        }
    };
  	...
}

RibbonServerRibbonLoadBalancerClient的内部类:

// RibbonLoadBalancerClient.java
public static class RibbonServer implements ServiceInstance {

   private final String serviceId;

   private final Server server;

   private final boolean secure;

   private Map<String, String> metadata;

   public RibbonServer(String serviceId, Server server) {
      this(serviceId, server, false, Collections.emptyMap());
   }

   public RibbonServer(String serviceId, Server server, boolean secure,
         Map<String, String> metadata) {
      this.serviceId = serviceId;
      this.server = server;
      this.secure = secure;
      this.metadata = metadata;
   }

   @Override
   public String getInstanceId() {
      return this.server.getId();
   }

   @Override
   public String getServiceId() {
      return this.serviceId;
   }

   @Override
   public String getHost() {
      return this.server.getHost();
   }

   @Override
   public int getPort() {
      return this.server.getPort();
   }

   @Override
   public boolean isSecure() {
      return this.secure;
   }

   @Override
   public URI getUri() {
      return DefaultServiceInstance.getUri(this);
   }

   @Override
   public Map<String, String> getMetadata() {
      return this.metadata;
   }

   public Server getServer() {
      return this.server;
   }

   @Override
   public String getScheme() {
      return this.server.getScheme();
   }

   @Override
   public String toString() {
      final StringBuilder sb = new StringBuilder("RibbonServer{");
      sb.append("serviceId='").append(serviceId).append('\'');
      sb.append(", server=").append(server);
      sb.append(", secure=").append(secure);
      sb.append(", metadata=").append(metadata);
      sb.append('}');
      return sb.toString();
   }

}

ServerIntrospector

ServerIntrospector接口基于Server对象确定服务实例是否是HTTPS协议,以及获取服务实例中的元数据信息:

public interface ServerIntrospector {

   // 是否使用HTTPS协议
   boolean isSecure(Server server);
   // 获取元数据信息
   Map<String, String> getMetadata(Server server);

}

各个注册中心都有具体的实现类:

  • Nacos:NacosServerIntrospector

  • Eureka:EurekaServerIntrospector

  • Consul:ConsulServerIntrospector

  • Zookeeper:ZookeeperServerIntrospector

NacosServerIntrospector的实现:

public class NacosServerIntrospector extends DefaultServerIntrospector {

   @Override
   public Map<String, String> getMetadata(Server server) {
      if (server instanceof NacosServer) {
         return ((NacosServer) server).getMetadata();
      }
      return super.getMetadata(server);
   }

   @Override
   public boolean isSecure(Server server) {
      if (server instanceof NacosServer) {
         return Boolean.valueOf(((NacosServer) server).getMetadata().get("secure"));
      }

      return super.isSecure(server);
   }

}

ILoadBalancer

ILoadBalancer接口是Netflix Ribbon用于实现负载均衡的核心接口,其内部有一套完善的机制用于实现负载均衡:

  1. 维护所有的Server列表,可以添加、删除Server或更新Server的状态。

  2. 监听机制。当Server列表(ServerListChangeListener)或Server状态(ServerStatusChangeListener)发送变化的时候,会产生相应事件。

  3. 可自定义的负载均衡策略IRule。

  4. 可自定义的服务实例健康状态检查方式IPing(针对单个Server如何检查是否健康)。

  5. 可自定义的服务实例健康状态检查策略IPingSteategy(针对单个Server如何检查)。

  6. 可自定义的Server列表过滤器(ServerListFilter),可以基于Server列表过滤出新的Server列表。

  7. 可自定义的Server列表获取方式(ServerList),用于获取注册中心服务对应实例。

  8. 可自定义的Server列表更新机制(ServerListUpdater),默认会使用一个调度线程池每30s从注册中心获取一次实例信息。

  9. 负载均衡器中各个服务实例当前的统计信息(LoadBalancerStats)。

Dubbo LoadBalancer负载均衡

Apache Dubbo是一款高性能Java RPC框架,其内部也拥有负载均衡功能。

@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {

    @Adaptive("loadbalance")
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;

}

LoadBalance接口只有一个select方法,会从一堆Invoker列表中根据负载均衡算法得到唯一的Invoker

Dubbo Router接口定义:

public interface Router extends Comparable<Router> {

 	...
    
    <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
	
  	...

}

Spring Cloud与Apache Dubbo在路由和负载均衡侧的功能对比:

功能/框架Spring CloudApache Dubbo
负载均衡ReactiveLoadBalancer(SCL)或IRule(Ribbon)LoadBalance
路由ServiceInstanceListSupplier(SCL)或ILoadBalancer(Ribbon)Router
容错机制ServerStats(Ribbon)&ILoadBalancerCluster
服务实例刷新机制DiscoveryClient(SCL)或ServerListUpdater(Ribbon)NotifyListener
健康检查Iping(Ribbon)不处理(注册中心实现),可以依靠fault tolerant机制过滤不健康的实例

服务调用

OpenFeign:声明式Rest客户端

RestTemplate的构造可以通过@LoadBalanced注解,使其拥有基于服务名进行服务调用的能力。 如果一个复杂的系统设计上百个服务名,如果使用RestTemplate发起服务,服务信息维护会非常麻烦。 OpenFeign是一种声明式接口方式的Rest客户端,只需定义接口和对应的方式及注解,底层就会生成动态代理,发起Rest请求并获得最终结果。

概述

若要使用OpenFeign,需要在pom文件里加入对应的依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

引入依赖之后,启动类需要添加@EnableFeignClients注解。

@SpringBootApplication
@EnableDiscoveryClient(autoRegister = false)
@EnableFeignClients
public class NacosConsumer {
  ...
}

然后定义接口并使用@FeignClient注解修饰。@FeignClient注解需要指定name属性指明调用哪个服务,接口的方法声明映射服务对外暴露的方法:

@FeignClient(name = "nacos-provider")
public interface EchoService {

    @GetMapping("/")
    String echo();

}

最后通过@Autowired注解直接注入定义的接口,发起服务调用即可。

@EnableFeignClients注解的作用是扫描被@FeignClient注解修饰的类。比如basePackages:String[]、basePackageClasses:Class[]和clients:Class[],这些属性的目的只有一个,就是找出需要扫描的包路径,然后根据包名在扫描的类里找出被@FeignClient注解修饰的类。

@EnableFeignClients注解对外还暴露了一个defaultConfiguration:Class<?>[]属性,这个属性表示默认配置类。

@FeignClient注解对外也暴露一个configuration:Class<?>[]属性。这套加载机制跟@LoadBalancerClient@LoadBalancerClients注解,以及@RibbonClient@RibbonClients注解的作用是一样的,解决的都是配置类的优先级问题。

对JAX-RS的支持

@FeignClient注解接口内部的方法都是被SpringMVC相关参数所修饰的,OpenFeign接口声明的定义还支持JAX-RS注解,若要使用JAX-RS注解修饰OpenFeign接口,需要在pom中加上依赖:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-jaxrs</artifactId>
</dependency>
<dependency>
    <groupId>javax.ws.rs</groupId>
    <artifactId>jsr311-api</artifactId>
</dependency>

@FeignClient修饰的接口中的方法使用JAX-RS注解:

@FeignClient(name = "nacos-provider", configuration = MyLoadBalancerConfiguration.class, contextId = "jaxrs")
public interface EchoServiceJAXRS {

    @GET
    @Path("/")
    String echo();

}

这里MyLoadBalancerConfiguration配置类内部的Contract实现类为JAXRSContract:

@Bean
public Contract myFeignContract(){
    return new JAXRSContract();
}

OpenFeign底层执行原理

OpenFeign声明的接口不论是SpringMVC还是JAX-RS,其底层都会把接口解析成方法元数据(MethodMetadata),再通过动态代理生成接口的代理,并基于MethodMetadata进行Rest调用。

@EnableFeignClients注解提供的包名与@FeignClient注解修饰的接口找到所有的接口,并给予这些接口构造FeignClientFactoryBean这个FactoryBean

FactoryBean内部真正构造的对象是一个Proxy,这个Proxy是通过Targeter#target构造出来的,Targeter内部构造通过Feign.Builder#build方法完成,build方法返回的是一个Feign对象。默认情况下返回的是ReflectiveFeign这个Feign对象的子类:

public class ReflectiveFeign extends Feign {

  ...
  @SuppressWarnings("unchecked")
  @Override
  public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else if (Util.isDefault(method)) {
        DefaultMethodHandler handler = new DefaultMethodHandler(method);
        defaultMethodHandlers.add(handler);
        methodToHandler.put(method, handler);
      } else {
        methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
      }
    }
    InvocationHandler handler = factory.create(target, methodToHandler);
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);

    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }
  
  ...
}

从这段代码可以看到,InvocationHandlerProxy这些JDK内置的动态代理类完成了这个操作。

Dubbo Spring Cloud:服务调用的新选择

Dubbo Spring Cloud是Spring Cloud Alibaba项目内部提供的一个可以使用Spring Cloud客户端RestTemplate或OpenFeign调用Dubbo服务的模块。

Apache Dubbo和Spring Cloud是两套架构完全不同的开发框架。

  • Apache Dubbo暴露的服务都是接口级别的,

  • Spring Cloud暴露的服务是应用级别的。

RestTemplate或OpenFeign发起调用服务都会有对应的URL Path、Query Parameter、Header等内容(这是HTTP协议调用),如何让这些内容关联Dubbo服务呢?

针对上述问题Dubbo Spring Cloud实现了以应用为粒度的注册机制,每个Dubbo应用注册到注册中心后有且仅有一个服务。

Dubbo Spring Cloud定义了DubboMetadataService元数据服务的概念。这是一个专门存储Dubbo服务的元数据接口。DubboMetadataService接口定义如下:

public interface DubboMetadataService {

   String VERSION = "1.0.0";

   String getServiceRestMetadata();

   Set<String> getAllServiceKeys();

   Map<String, String> getAllExportedURLs();

   String getExportedURLs(String serviceInterface, String group, String version);

}

核心方法getServiceRestMetadata获取Dubbo服务的Rest元数据是指:当一个Dubbo服务同时也被SpringMVC相关注解修饰时,SpringMVC相关注解修饰的内容就是这些Rest元数据,这些Rest元数据由RestMethodMetadata类修饰。当Dubbo服务自身也暴露Rest协议的时候,这些JAX-RS相关注解修饰的内容也会被解析成Rest元数据。

调用流程

使用RestTemplate或OpenFeign调用Dubbo服务会经历一下过程:

  1. 根据服务名得到注册中心的Dubbo服务DubboMetadataService

  2. 使用DubboMetadataService里提供的getServiceRestMetadata方法获取要使用的Dubbo服务和对应的Rest元数据。

  3. 基于Dubbo服务和Rest元数据构造GenericService

  4. 服务调用过程中使用GenericService发起泛化调用。

开发步骤

  1. 引入spring-cloud-starter-dubbo依赖。

    <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-dubbo</artifactId>
    </dependency>
    
  2. Provider端接口加上SpringMVC相关注解或使用JAX-RS暴露Rest协议。

  3. 加上SpringMVC相关注解

    @Service(version = "1.0.0")
    @RestController
    public class OrderServiceImpl implements OrderService {
    
    private static List<Order> orderList = new ArrayList<>();
    
    static {
        orderList.add(Order.generate("jim"));
        orderList.add(Order.generate("jim"));
        orderList.add(Order.generate("test"));
    }
    
    @GetMapping("/allOrders")
    @Override
    public List<Order> getAllOrders(@RequestParam("userId") final String userId) {
        return orderList.stream().filter(
            order -> order.getUserId().equals(userId)
        ).collect(Collectors.toList());
    }
    
    @GetMapping("/findOrder")
    @Override
    public Order findOrder(@RequestParam("orderId") String orderId) {
        return orderList.stream().filter(
            order -> order.getId().equals(orderId)
        ).findFirst().orElseGet(Order::error);
    }
    }
    
  4. 使用JAX-RS暴露Rest协议

    配置文件暴露rest协议

    dubbo.protocols.rest.name=rest
    dubbo.protocols.rest.port=9090
    dubbo.protocols.rest.server=netty
    

    接口使用JAX-RS注解修饰:

    @Service(version = "1.0.0", protocol = {"dubbo", "rest"})
    @Path("/')
    public class OrderServiceImpl implements OrderService {
    
    private static List<Order> orderList = new ArrayList<>();
    
    static {
        orderList.add(Order.generate("jim"));
        orderList.add(Order.generate("jim"));
        orderList.add(Order.generate("test"));
    }
    
    @Path("/allOrders")
    @GET
    @Override
    public List<Order> getAllOrders(@RequestParam("userId") final String userId) {
        return orderList.stream().filter(
            order -> order.getUserId().equals(userId)
        ).collect(Collectors.toList());
    }
    
    @Path("/findOrder")
    @GET
    @Override
    public Order findOrder(@RequestParam("orderId") String orderId) {
        return orderList.stream().filter(
            order -> order.getId().equals(orderId)
        ).findFirst().orElseGet(Order::error);
    }
    
    }
    
  5. Consumer客户端加上@DubboTransported注解。

    RestTemplate和OpenFeign客户端都支持@DubboTransported注解。

          @Bean
      @LoadBalanced
          @DubboTransported
      public RestTemplate restTemplate() {
          RestTemplate restTemplate = new RestTemplate();
          return restTemplate;
      }
    
    @FeignClient("sc-dubbo-provider")
    @DubboTransported(protocol = "dubbo")
    public interface DubboFeignOrderService {
    
      @GetMapping("/allOrders")
      List<Order> getAllOrders(@RequestParam("userId") final String userId);
    
      @GetMapping("/findOrder")
      Order findOrder(@RequestParam("orderId") String orderId);
    
    }
    
  6. 使用RestTemplate和OpenFeign调用Dubbo服务。