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
本节主要关注LoadBalancerAutoConfiguration和BlockingLoadBalancerClientAutoConfiguration
👉LoadBalancerAutoConfiguration
图中可以看出
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具体实现了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是否存在对应name的AnnotationConfigApplicationContext,如果存在存在直接返回,倘若不存在调用createContext方法创建。
createContext方法首先创建了AnnotationConfigApplicationContext,接着从所有的configurations中查询是否有包含该name
。如果存在则从configurations中获取该配置,并将配置注册到新创建的AnnotationConfigApplicationContext中。其次如果 configurations 中有名称开头为 default. 的配置类,同样也注册它。接着就是配置environment了,key=propertyName,value=name。这样后续使用environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);就可以获取该name值了,最后会返回AnnotationConfigApplicationContext。可能说的有点抽象,我们用图表示一下。
上述图片很清楚的展示了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;
}
}
}
乍看这个测试代码可能需要时间去理解它,我们先分析一下
UML类图一目了然,以上测试方法可以表示的图如下:
这样看是不是很清晰,根据这张图在看测试方法中的断言,需要知道的是,
BaseConfig中创建了Baz作为其他隔离容器的parent,其它容器也可以获取到它,因此then(fooBaz).as("fooBaz was null").isNotNull();成立。此外还可以注意到在bar容器中存在两个Baz,then(barBazes.size()).as("barBazes size was wrong").isEqualTo(2);这是因为BarConfig创建的一个,BaseConfig中也创建了一个。
至此我想读者应该掌握了NameContextFactory实现客户端隔离的原理了,现在我们再看下目前已经介绍的图:
上述表红圈的就是目前已经介绍的内容,接下来我们看下它的默认配置。