springcloud-loadbalancer-01

806 阅读8分钟

loadbalancer.drawio.png ​ Springcloud自放弃了Netfix全家桶后,目前已经出到了2021版本,本章简单介绍了2021版本下的负载均衡策略。

​ Spring官方将loadbalance放到了spring-cloud-common工程下,目测官方想一统cloud组件的决心。言归正传,读者看完本章后可以有获得的收获如下:

  • 了解Spring-cloud-load-balancer的运行原理
  • 掌握如何定制负载均衡算法 负载均衡按道理一篇文章就结束了,奈何字数限制了,只有分为5个部分🙄

👉自动配置

从自动配置开始看起

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration,\
org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration,\
org.springframework.cloud.loadbalancer.config.LoadBalancerCacheAutoConfiguration,\
org.springframework.cloud.loadbalancer.security.OAuth2LoadBalancerClientAutoConfiguration,\
org.springframework.cloud.loadbalancer.config.LoadBalancerStatsAutoConfiguration

本节主要关注LoadBalancerAutoConfigurationBlockingLoadBalancerClientAutoConfiguration

👉LoadBalancerAutoConfiguration

LoadBalancerAutoConfiguration.png 图中可以看出LoadBalancerAutoConfiguration中创建了LoadBalancerClientFactory,并且包含有LoadBalancerClientSpecification。该类的构造方法会搜集Spring容器中的ObjectProvider<List<LoadBalancerClientSpecification>并将其注入搭配该类的成员变量configurations中。该configurations代表了负载均衡的配置。从该构造器中我们可以猜到,如果我们自定义负载均衡配置,它同样会被注入到configurations中,这里是一个扩展点。收集到的配置会被用来创建LoadBalancerClientFactory。当然LoadBalancerClientFactory的创建还少不了配置信息LoadBalancerClientsProperties。该配置会因为@EnableConfigurationProperties(LoadBalancerClientsProperties.class)被加载到容器,并自动注入到loadBalancerClientFactory方法的参数中。loadBalancerClientFactory顾名思义它就是用来创建loadBalancerClient的工厂bean,在详细介绍LoadBalancerClientFactory之前我们详细的看一下它的入参。

@Configuration(proxyBeanMethods = false)
@LoadBalancerClients
@EnableConfigurationProperties(LoadBalancerClientsProperties.class)
@AutoConfigureBefore({ ReactorLoadBalancerClientAutoConfiguration.class,
		LoadBalancerBeanPostProcessorAutoConfiguration.class })
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.enabled", havingValue = "true", matchIfMissing = true)
public class LoadBalancerAutoConfiguration {

   //收集应用中所有的loadBalancer配置,包括被@LoadBalancerClient和@LoadBalancerClients注解
   //标注的配置类
   private final ObjectProvider<List<LoadBalancerClientSpecification>> configurations;

   public LoadBalancerAutoConfiguration(ObjectProvider<List<LoadBalancerClientSpecification>> configurations) {
      this.configurations = configurations;
   }

   @Bean
   @ConditionalOnMissingBean
   //获取spring.cloud.loadbalancer.zone配置的zone区域,如果是eureka中也被治疗zone,该
   //配置将会被覆盖
   public LoadBalancerZoneConfig zoneConfig(Environment environment) {
      return new LoadBalancerZoneConfig(environment.getProperty("spring.cloud.loadbalancer.zone"));
   }

   @ConditionalOnMissingBean
   @Bean
   //创建了一个LoadBalancerClientFactory,并传入了loadBalancer的属性和配置
   //利用getInstance方法,来创建配置隔离的loadBalancer客户端
   public LoadBalancerClientFactory loadBalancerClientFactory(LoadBalancerClientsProperties properties) {
      LoadBalancerClientFactory clientFactory = new LoadBalancerClientFactory(properties);
      clientFactory.setConfigurations(this.configurations.getIfAvailable(Collections::emptyList));
      return clientFactory;
   }

}

LoadBalancerClientsProperties继承自LoadBalancerProperties并标注了@ConfigurationProperties("spring.cloud.loadbalancer")注解,LoadBalancerProperties中主要包括了健康检查的相关配置、重试策略的配置等。因此我们可以通过"spring.cloud.loadbalancer"来配置相关属性。

LoadBalancerClientSpecification类如下:

public class LoadBalancerClientSpecification implements NamedContextFactory.Specification {

   //配置的name
   private String name;
   //具体的配置,可以配置相应的负载均衡策略,调用时间等
   private Class<?>[] configuration;
   ...
}

该类的name是配置的服务名,假设我们使用的是Eureka的客户端,并且使用RestTemplate进行远程调用,那么该name可以表示的形式如http://HELLO-SERVICE/hello中的HELLO-SERVICE所示,如果是我们自定义的配置,那么该name会从environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);中获取。

👉接下来主要介绍下LoadBalancerClientFactory

LoadBalancerClientFactory.png

从实现的角度来看LoadBalancerClientFactory具体实现了ReactiveLoadBalancer.Factory接口,并实现了获取配置属性和实例的方法。它还继承了NamedContextFactory<LoadBalancerClientSpecification>抽象类,因此LoadBalancerClientFactory主要功能依赖于NamedContextFactory<LoadBalancerClientSpecification>实现。

public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
      implements DisposableBean, ApplicationContextAware {
	 // Property source name for load balancer.
   private final String propertySourceName;
	 // Property for client name within the load balancer namespace
   private final String propertyName;
	 // Used to save AnnotationConfigApplicationContext
   private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
	 // Used to save the configuration of different clients
   private Map<String, C> configurations = new ConcurrentHashMap<>();
	 // Spring container
   private ApplicationContext parent;
	 // default config type is LoadBalancerClientConfiguration.class
   private Class<?> defaultConfigType;

   public NamedContextFactory(Class<?> defaultConfigType, String propertySourceName, String propertyName) {
      this.defaultConfigType = defaultConfigType;
      this.propertySourceName = propertySourceName;
      this.propertyName = propertyName;
   }

   @Override
   public void setApplicationContext(ApplicationContext parent) throws BeansException {
      this.parent = parent;
   }

   public ApplicationContext getParent() {
      return parent;
   }

   public void setConfigurations(List<C> configurations) {
      for (C client : configurations) {
         this.configurations.put(client.getName(), client);
      }
   }

   public Set<String> getContextNames() {
      return new HashSet<>(this.contexts.keySet());
   }

   @Override
   public void destroy() {
      Collection<AnnotationConfigApplicationContext> values = this.contexts.values();
      for (AnnotationConfigApplicationContext context : values) {
         // This can fail, but it never throws an exception (you see stack traces
         // logged as WARN).
         context.close();
      }
      this.contexts.clear();
   }

   protected AnnotationConfigApplicationContext getContext(String name) {
      //如果 map 中不存在,则创建
      if (!this.contexts.containsKey(name)) {
         //防止并发创建多个
         synchronized (this.contexts) {
            //再次判断,防止有多个等待锁
            if (!this.contexts.containsKey(name)) {
               this.contexts.put(name, createContext(name));
            }
         }
      }
      return this.contexts.get(name);
   }

   //根据名称创建对应的 context
   protected AnnotationConfigApplicationContext createContext(String name) {
      AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
      //如果 configurations 中有对应名称的配置类,则注册之
      if (this.configurations.containsKey(name)) {
         for (Class<?> configuration : this.configurations.get(name).getConfiguration()) {
            context.register(configuration);
         }
      }
      //如果 configurations 中有名称开头为 default. 的配置类,则注册之
      for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
         if (entry.getKey().startsWith("default.")) {
            for (Class<?> configuration : entry.getValue().getConfiguration()) {
               context.register(configuration);
            }
         }
      }

      //注册 PropertyPlaceholderAutoConfiguration,这样可以解析 spring boot 相关的 application 配置
      //注册默认的配置类 defaultConfigType
      context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType);
      //将当前 context 的名称,放入对应的属性中,在配置类中可能会用到
      //我们上面举得例子,就是通过 environment.getProperty() 获取了这个属性
      context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(this.propertySourceName,
            Collections.<String, Object>singletonMap(this.propertyName, name)));
      if (this.parent != null) {
         // Uses Environment from parent as well as beans
         context.setParent(this.parent);
         // jdk11 issue
         // https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
         //spring boot 可以打包成一种 fatjar 的形式,将依赖的 jar 包都打入同一个 jar 包中
         //fatjar 中的依赖,通过默认的类加载器是加载不正确的,需要通过定制的类加载器
         //由于 JDK 11 LTS 相对于 JDK 8 LTS 多了模块化,通过 ClassUtils.getDefaultClassLoader() 有所不同
         //在 JDK 8 中获取的就是定制的类加载器,JDK 11 中获取的是默认的类加载器,这样会有问题
         //所以,这里需要手动设置当前 context 的类加载器为父 context 的类加载器
         context.setClassLoader(this.parent.getClassLoader());
      }
      //生成展示名称
      context.setDisplayName(generateDisplayName(name));
      context.refresh();
      return context;
   }

   protected String generateDisplayName(String name) {
      return this.getClass().getSimpleName() + "-" + name;
   }

   /**
    * 获取某个named的ApplicationContext里面的某个Bean
    * @param name ApplicationContext的名称
    * @param type 类型
    * @param <T> Bean类型
    * @return
    */
   public <T> T getInstance(String name, Class<T> type) {
      //获取或者创建对应名称的ApplicationContext
      AnnotationConfigApplicationContext context = getContext(name);
      try {
         //从对应ApplicationContext中获取Bean
         return context.getBean(type);
      }
      catch (NoSuchBeanDefinitionException e) {
         // ignore
      }
      return null;
   }

   public <T> ObjectProvider<T> getLazyProvider(String name, Class<T> type) {
      return new ClientFactoryObjectProvider<>(this, name, type);
   }

   public <T> ObjectProvider<T> getProvider(String name, Class<T> type) {
      AnnotationConfigApplicationContext context = getContext(name);
      return context.getBeanProvider(type);
   }

   public <T> T getInstance(String name, Class<?> clazz, Class<?>... generics) {
      ResolvableType type = ResolvableType.forClassWithGenerics(clazz, generics);
      return getInstance(name, type);
   }

   /**
    * 获取某个named的ApplicationContext里面的某个Bean
    * @param name ApplicationContext的名称
    * @param type 类型
    * @param <T> Bean类型
    * @return
    */
   @SuppressWarnings("unchecked")
   public <T> T getInstance(String name, ResolvableType type) {
      //获取或者创建对应名称的ApplicationContext
      AnnotationConfigApplicationContext context = getContext(name);
      String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type);
      if (beanNames.length > 0) {
         for (String beanName : beanNames) {
            if (context.isTypeMatch(beanName, type)) {
               //从对应ApplicationContext中获取Bean
               return (T) context.getBean(beanName);
            }
         }
      }
      //没找到返回null
      return null;
   }

   public <T> Map<String, T> getInstances(String name, Class<T> type) {
      AnnotationConfigApplicationContext context = getContext(name);

      return BeanFactoryUtils.beansOfTypeIncludingAncestors(context, type);
   }

   /**
    * Specification with name and configuration.
    */
   public interface Specification {

      String getName();

      Class<?>[] getConfiguration();

   }

}

这段代码可能比较长,慢慢梳理。

👉先从getInstance方法开始,先查看contexts是否存在对应nameAnnotationConfigApplicationContext,如果存在存在直接返回,倘若不存在调用createContext方法创建。

createContext方法首先创建了AnnotationConfigApplicationContext,接着从所有的configurations中查询是否有包含该name

。如果存在则从configurations中获取该配置,并将配置注册到新创建的AnnotationConfigApplicationContext中。其次如果 configurations 中有名称开头为 default. 的配置类,同样也注册它。接着就是配置environment了,key=propertyName,value=name。这样后续使用environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);就可以获取该name值了,最后会返回AnnotationConfigApplicationContext。可能说的有点抽象,我们用图表示一下。

NameContextFactory.drawio.png

​ 上述图片很清楚的展示了NameContextFactory可以配置以name作为边界,并创建了不同客户端配置的隔离环境,并且这些隔离环境共用一个父容器ApplicationContext。这样配置的好处体现在当我们进行远程调用的时候,针对不同的客户端往往需要不同的配置(负载均衡策略、重试、超时等),此时就可以自定义配置,并为不同的客户端配置设置不同的name,这样在进行远程调用的时候,使用的是该客服端对应的配置。简单的例子如下:

@LoadBalancerClient(name = "HELLO-SERVICE", configuration = CustomLoadBalancerConfiguration.class)
public class MyConfiguration {
.....
}

上述配置就是以name = "HELLO-SERVICE"为边界建立的配置。

当然如果仅仅给出了上边的例子,对于读者来说可能还是有些迷茫。接下来,我们用官方给出的NamedContextFactory测试类来说明下。

/**
 * @author Spencer Gibb
 */
public class NamedContextFactoryTests {

   @Test
   public void testChildContexts() {
      AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
      parent.register(BaseConfig.class);
      parent.refresh();
      TestClientFactory factory = new TestClientFactory();
      factory.setApplicationContext(parent);
      factory.setConfigurations(Arrays.asList(getSpec("foo", FooConfig.class), getSpec("bar", BarConfig.class)));

      Foo foo = factory.getInstance("foo", Foo.class);
      then(foo).as("foo was null").isNotNull();

      Bar bar = factory.getInstance("bar", Bar.class);
      then(bar).as("bar was null").isNotNull();

      then(factory.getContextNames()).as("context names not exposed").contains("foo", "bar");

      Bar foobar = factory.getInstance("foo", Bar.class);
      then(foobar).as("bar was not null").isNull();

      Baz fooBaz = factory.getInstance("foo", Baz.class);
      then(fooBaz).as("fooBaz was null").isNotNull();

      Object fooContainerFoo = factory.getInstance("foo", Container.class, Foo.class);
      then(fooContainerFoo).as("fooContainerFoo was null").isNotNull();

      Object fooContainerBar = factory.getInstance("foo", Container.class, Bar.class);
      then(fooContainerBar).as("fooContainerBar was not null").isNull();

      Object barContainerBar = factory.getInstance("bar", Container.class, Bar.class);
      then(barContainerBar).as("barContainerBar was null").isNotNull();

      Map<String, Baz> fooBazes = factory.getInstances("foo", Baz.class);
      then(fooBazes).as("fooBazes was null").isNotNull();
      then(fooBazes.size()).as("fooBazes size was wrong").isEqualTo(1);

      Map<String, Baz> barBazes = factory.getInstances("bar", Baz.class);
      then(barBazes).as("barBazes was null").isNotNull();
      then(barBazes.size()).as("barBazes size was wrong").isEqualTo(2);

      // get the contexts before destroy() to verify these are the old ones
      AnnotationConfigApplicationContext fooContext = factory.getContext("foo");
      AnnotationConfigApplicationContext barContext = factory.getContext("bar");

      then(fooContext.getClassLoader()).as("foo context classloader does not match parent")
            .isSameAs(parent.getClassLoader());

      Assertions.assertThat(fooContext).hasFieldOrPropertyWithValue("customClassLoader", true);

      factory.destroy();

      then(fooContext.isActive()).as("foo context wasn't closed").isFalse();

      then(barContext.isActive()).as("bar context wasn't closed").isFalse();
   }

   private TestSpec getSpec(String name, Class<?> configClass) {
      return new TestSpec(name, new Class[] { configClass });
   }

   static class TestClientFactory extends NamedContextFactory<TestSpec> {

      TestClientFactory() {
         super(TestSpec.class, "testfactory", "test.client.name");
      }

   }

   static class TestSpec implements NamedContextFactory.Specification {

      private String name;

      private Class<?>[] configuration;

      TestSpec() {
      }

      TestSpec(String name, Class<?>[] configuration) {
         this.name = name;
         this.configuration = configuration;
      }

      @Override
      public String getName() {
         return this.name;
      }

      public void setName(String name) {
         this.name = name;
      }

      @Override
      public Class<?>[] getConfiguration() {
         return this.configuration;
      }

      public void setConfiguration(Class<?>[] configuration) {
         this.configuration = configuration;
      }

   }

   static class BaseConfig {

      @Bean
      Baz baz1() {
         return new Baz();
      }

   }

   static class Baz {

   }

   static class FooConfig {

      @Bean
      Foo foo() {
         return new Foo();
      }

      @Bean
      Container<Foo> fooContainer() {
         return new Container<>(new Foo());
      }

   }

   static class Foo {

   }

   static class BarConfig {

      @Bean
      Bar bar() {
         return new Bar();
      }

      @Bean
      Baz baz2() {
         return new Baz();
      }

      @Bean
      Container<Bar> barContainer() {
         return new Container<>(new Bar());
      }

   }

   static class Bar {

   }

   static class Container<T> {

      private final T item;

      Container(T item) {
         this.item = item;
      }

      public T getItem() {
         return this.item;
      }

   }

}

乍看这个测试代码可能需要时间去理解它,我们先分析一下

png1.png

UML类图一目了然,以上测试方法可以表示的图如下:

loadbalancer-test2.drawio.png ​ 这样看是不是很清晰,根据这张图在看测试方法中的断言,需要知道的是,BaseConfig中创建了Baz作为其他隔离容器的parent,其它容器也可以获取到它,因此then(fooBaz).as("fooBaz was null").isNotNull();成立。此外还可以注意到在bar容器中存在两个Bazthen(barBazes.size()).as("barBazes size was wrong").isEqualTo(2);这是因为BarConfig创建的一个,BaseConfig中也创建了一个。

​ 至此我想读者应该掌握了NameContextFactory实现客户端隔离的原理了,现在我们再看下目前已经介绍的图:

png2.png 上述表红圈的就是目前已经介绍的内容,接下来我们看下它的默认配置。