Spring cloud负载均衡@LoadBalanced & LoadBalancerClient

 更新时间:2023年11月16日 11:34:29   作者:福  
由于Spring cloud2020之后移除了Ribbon,直接使用Spring Cloud LoadBalancer作为客户端负载均衡组件,我们讨论Spring负载均衡以Spring Cloud2020之后版本为主,学习Spring Cloud LoadBalance

LoadBalance vs Ribbon

由于Spring cloud2020之后移除了Ribbon,直接使用Spring Cloud LoadBalancer作为客户端负载均衡组件,我们讨论Spring负载均衡以Spring Cloud2020之后版本为主,学习Spring Cloud LoadBalance,暂不讨论Ribbon。

两者有什么区别、Spring Cloud为什么移除了Ribbon转向了Spring Cloud LoadBalancer,改日研究。

回顾

上篇文章我们学习了Spring Cloud LoadBalance负载均衡底层原理中的:

@LoadBalanced注解的使用:与@Bean一起作用在RestTemplate上:

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
    return new RestTemplate();
}

可以实现:在注入RestTemplate对象到Spring IoC容器的同时,启用Spring的负载均衡机制。

  • @LoadBalanced注解的底层原理:在LoadBalancerAutoConfiguration初始化的过程中,创建拦截器LoadBalancerInterceptor,对请求进行拦截从而实现负载均衡。
  • LoadBalancerInterceptor拦截器在执行请求前调用其intercept方法,intercept负责负载均衡的实现(具体的实现逻辑尚未研究)

其中第3点,intercept方法是怎么实现负载均衡的,我们还没有深入研究,这是我们今天这篇文章的主要目的。

Spring Cloud负载均衡原理

LoadBalancerClient及ReactorLoadBalancer初始化

LoadBalancerInterceptor拦截器的intercept方法究竟是怎么实现负载均衡的?

拦截方法intercept中会通过LoadBalancerClient对象(从Spring IoC容器中获取)实现负载均衡,LoadBalancerClient对象的注入以及拦截原理这个过程稍微复杂一点,所以我们先用简单的方式描述其实现逻辑,然后再从源码角度进行跟踪。

我们在上一篇文章中说过的spring-cloud-commons包下的自动配置类(如图):

比如对@LoadBalanced注解的解析、LoadBalancerInterceptor的注入等等,就是上面自动配置类LoadBalancerAutoConfiguration完成的。

Spring cloud有两个名字一样的自动配置类LoadBalancerAutoConfiguration,位于不同的包下,上面一个是在spring-cloud-commes包下,下面还要提到的一个是在spring-cloud-loadbalancer包下。

spring-cloud-loadbalancer包下的自动配置类LoadBalancerAutoConfiguration负责注入LoadBalancerClientFactory对象,LoadBalancerClientFactory负责创建子容器(SpringCloud通过子容器来隔离各微服务的访问参数、负载均衡策略等)。创建LoadBalancerClientFactory对象的过程中将LoadBalancerClientConfiguration设置给他的defaultConfigType属性,在子容器初始化的过程中将LoadBalancerClientConfiguration注册为配置类,从而通过LoadBalancerClientConfiguration配置类完成ReactorLoadBalancer的创建并注入子容器中。ReactorLoadBalancer是负载均衡策略接口,默认的负载均衡策略为RoundRobinLoadBalancer。

spring-cloud-loadbalancer包下的另外一个自动配置类BlockingLoadBalancerClientAutoConfiguration负责注入拦截器中的LoadBalancerClient,实际注入的是BlockingLoadBalancerClient对象,BlockingLoadBalancerClient会持有LoadBalancerClientFactory对象。

LoadBalancerInterceptor的intercept方法会转交给BlockingLoadBalancerClient处理,BlockingLoadBalancerClient通过LoadBalancerClientFactory对象向子容器(子容器不存在的话首先创建子容器)获取相关配置以及负载均衡策略RoundRobinLoadBalancer,最终通过RoundRobinLoadBalancer实现负载均衡。

需要注意,子容器不是在系统初始化过程中创建的,而是在处理请求的过程中创建的。

下面分析源码。

LoadBalancerClient

从应用层入手分析,先看上一篇文章的案例中的orderServicede的代码:

@Service
public class OrderService {
    @Autowired
    private RestTemplate restTemplate;
    public String getOrder(){
        //通过userService获取user信息
        String url="http://userservice/user/getUser";
        System.out.println("url"+url);
        User user=restTemplate.getForObject(url,User.class);
        System.out.println(user);
        return user.getName();
    }
}

restTemplate.getForObject最终会调用到LoadBalancerInterceptor的intercept方法:

private LoadBalancerClient loadBalancer;
    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, this.requestFactory.createRequest(request, body, execution));
    }

调用loadBalancer的execute方法,而loadBalancer是LoadBalancerClient 对象、是LoadBalancerInterceptor初始化过程中通过方法参数从SpringIoC容器中注入进来的。

前面提到过,自动配置类BlockingLoadBalancerClientAutoConfiguration负责注入拦截器中的LoadBalancerClient,实际注入的是BlockingLoadBalancerClient对象。为了不影响可读性,我们稍后再看这部分源码。

继续跟踪loadBalancer的execute方法。首先看一下LoadBalancerClient 的类结构;

接口LoadBalancerClient继承自接口ServiceInstanceChooser,接口定义了choose方法及execute方法(包括其重载方法)。其中execute是调用入口、也是模板方法:根据请求的服务serviceId(比如userService)通过调用choose方法获取到最终要调用的服务实例serviceInstance,最终调用到服务实例所提供的服务:

@Override
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        String hint = getHint(serviceId);
        LoadBalancerRequestAdapter<T, TimedRequestContext> lbRequest = new LoadBalancerRequestAdapter<>(request,
                buildRequestContext(request, hint));
        Set<LoadBalancerLifecycle> supportedLifecycleProcessors = getSupportedLifecycleProcessors(serviceId);
        supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
        ServiceInstance serviceInstance = choose(serviceId, lbRequest);
        if (serviceInstance == null) {
            supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
                    new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, new EmptyResponse())));
            throw new IllegalStateException("No instances available for " + serviceId);
        }
        return execute(serviceId, serviceInstance, lbRequest);
    }

继续跟踪choose方法,是在BlockingLoadBalancerClient类中实现的。

BlockingLoadBalancerClient

我们已经知道注入到Spring Ioc容器中的LoadBalancerClient其实是BlockingLoadBalancerClient对象,所以继续跟踪BlockingLoadBalancerClient的choose方法:

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

我们需要重点关注的是两个方法:

第一个是LoadBalancerClientFactory的getInstance方法:通过serviceId从子容器中拿到ReactiveLoadBalancer,参数serviceId(服务Id),指的就是我们注册到Eureka注册中心的服务Id,比如前面案例中的userService。

第二个是ReactiveLoadBalancer的choose方法:根据不同的负载均衡策略,从服务队列中拿到serviceInstance。Spring cloud提供了两种负载均衡策略:随机策略RandomLoadBalancer和循环策略RoundRobinLoadBalancer。

我们先来看第一步:从子容器中获取ReactiveLoadBalancer对象。

子容器的创建

如果是首次调用、子容器不存在的情况下,LoadBalancerClientFactory负责创建子容器。

LoadBalancerClientFactory是reactiveLoadBalancer.Factory<ServiceInstance>的实现类,继承自虚拟类NamedContextFactory,创建子容器的大部分代码都在NamedContextFactory类中。

我们首先看一下这个LoadBalancerClientFactory是怎么初始化的Spring IoC容器中的。其实前面已经说过了,是通过spring-cloud-loadbalancer包下的自动配置类LoadBalancerAutoConfiguration负责注入。

我们从源码角度验证一下,LoadBalancerAutoConfiguration源码:

@ConditionalOnMissingBean
    @Bean
    public LoadBalancerClientFactory loadBalancerClientFactory(LoadBalancerClientsProperties properties) {
        LoadBalancerClientFactory clientFactory = new LoadBalancerClientFactory(properties);
        clientFactory.setConfigurations(this.configurations.getIfAvailable(Collections::emptyList));
        return clientFactory;
    }

看一下LoadBalancerClientFactory的构造方法:

public LoadBalancerClientFactory(LoadBalancerClientsProperties properties) {
        super(LoadBalancerClientConfiguration.class, NAMESPACE, PROPERTY_NAME);
        this.properties = properties;
    }

父类的构造方法:

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

可以看到LoadBalancerClientFactory创建的时候将LoadBalancerClientConfiguration.class赋值给他的父类NamedContextFactory的defaultConfigType属性,在创建子容器的时候LoadBalancerClientConfiguration类会被注册为子容器的配置类、从而通过LoadBalancerClientConfiguration完成ReactorLoadBalancer对象的注入(注入到子容器中)

NamedContextFactory

先对LoadBalancerClientFactory做一个简单的认识。

LoadBalancerClientFactory继承自虚拟类NamedContextFactory,实现了接口DisposableBean和ApplicationContextAware,这两个接口我们并不陌生,在Spring生命周期回调的学习过程中中我们了解过这两个接口,Spring会在容器创建完成后通过ApplicationContextAware的setApplicationContext方法把ApplicationContext送回来、在容器销毁的时候回调DisposableBean接口的destroy方法。LoadBalancerClientFactory实现了这两个接口,所以LoadBalancerClientFactory就可以获取到Spring IoC根容器的applicationContext:

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

检查NamedContextFactory的setApplicatonContext方法,发现他把Spring IoC容器设置为自己的父容器了:这也很好理解,从NamedContextFactory类名称判断,这个类的目的就是要创建“Named”容器、也就是命名容器,其实我们后面会发现就是用serviceId命名的容器,比如我们有userservice,那就会创建一个名字叫userservice的容器。通过ApplicationContextAware回调setApplicationContext方法将Spring Ioc容器设置为命名容器的“父容器”。

继续跟踪LoadBalancerClientFactory的getInstance方法,调用到父类NamedContextFactory的getInstance:

@Override
    public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
        return getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class);
    }
    public <T> T getInstance(String name, Class<T> type) {
        AnnotationConfigApplicationContext context = getContext(name);
        try {
            return context.getBean(type);
        }
        catch (NoSuchBeanDefinitionException e) {
            // ignore
        }
        return null;
    }

最终是向Spring的ApplicationContext获取类型为ReactorServiceInstanceLoadBalancer的bean。其中ApplicationContext通过getContext方法获取:

private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
    protected AnnotationConfigApplicationContext getContext(String name) {
        if (!this.contexts.containsKey(name)) {
            synchronized (this.contexts) {
                if (!this.contexts.containsKey(name)) {
                    this.contexts.put(name, createContext(name));
                }
            }
        }
        return this.contexts.get(name);
    }

首先从contexts查找,contexts是以serviceId为键值的ConcurrentHashMap,缓存创建的ApplicationContext,如果尚未创建,则调用createContext方法创建后缓存到contexts中。

这个名字为contexts的ConcurrentHashMap其实就是NamedContextFactory的核心:创建的ApplicationContext缓存在以serviceId为键值的HashMap中,获取的时候以serviceId到contexts中去查找,查找到则直接返回、查找不到则创建后缓存。

createContext方法:

protected AnnotationConfigApplicationContext createContext(String name) {
        AnnotationConfigApplicationContext context;
        if (this.parent != null) {
            // jdk11 issue
            // https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
            // https://github.com/spring-cloud/spring-cloud-openfeign/issues/475
            DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
            if (parent instanceof ConfigurableApplicationContext) {
                beanFactory.setBeanClassLoader(
                        ((ConfigurableApplicationContext) parent).getBeanFactory().getBeanClassLoader());
            }
            else {
                beanFactory.setBeanClassLoader(parent.getClassLoader());
            }
            context = new AnnotationConfigApplicationContext(beanFactory);
            context.setClassLoader(this.parent.getClassLoader());
        }
        else {
            context = new AnnotationConfigApplicationContext();
        }
        if (this.configurations.containsKey(name)) {
            for (Class<?> configuration : this.configurations.get(name).getConfiguration()) {
                context.register(configuration);
            }
        }
        for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
            if (entry.getKey().startsWith("default.")) {
                for (Class<?> configuration : entry.getValue().getConfiguration()) {
                    context.register(configuration);
                }
            }
        }
        //注意这里会注册this.defaultConfigType到容器中
        context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType);
        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);
        }
        context.setDisplayName(generateDisplayName(name));
        context.refresh();
        return context;
    }

代码比较长但是并不复杂,仔细看一下其实就是Spring IoC容器的初始化过程:

  • 创建DefaultListableBeanFactory
  • 创建AnnotationConfigApplicationContext
  • 加载属于当前serviceId的配置
  • 加载所有的“默认”配置(也就是以default.开头的配置项)
  • 加载配置文件(从配置文件及环境变量中加载),注册配置类 this.defaultConfigType,其实就是LoadBalancerClientConfiguration配置类
  • 设置父容器(Spring Ioc的主容器设置为父容器)
  • 刷新容器
  • 返回容器

一个需要关注的重点就是:子容器创建的过程中,将配置类LoadBalancerClientConfiguration注册到容器中,在容器刷新的时候,这个配置类会被加载。

ReactorLoadBalancer & LoadBalancerClientConfiguration

子容器创建出来之后,我们还是返回到上面的NamedContextFactory的getInstance方法中:

@Override
    public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
        return getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class);
    }

会向子容器获取ReactorServiceInstanceLoadBalancer对象。

所以我们现在两个任务:第一个是了解一下ReactorServiceInstanceLoadBalancer类,第二个是要了解到注入到子容器中的ReactorServiceInstanceLoadBalancer究竟是个什么对象。

第一步:看一眼ReactorLoadBalancer的类结构:

ReactorServiceInstanceLoadBalancer接口继承自ReactorLoadBalancer,Spring Cloud提供了他的两个实现类:随机策略类和轮询策略类。

第二步,注入到子容器中的ReactorServiceInstanceLoadBalancer究竟是个什么对象?就需要研究一下ReactorLoadBalancer的初始化过程。

子容器通过配置类LoadBalancerClientConfiguration实现ReactorLoadBalancer的注入,默认实现类是RoundRobinLoadBalancer:

@Bean
    @ConditionalOnMissingBean
    public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
            LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RoundRobinLoadBalancer(
                loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }

OK!

真相大白了,默认就是轮询策略RoundRobinLoadBalancer。

负载均衡策略的配置

Spring Cloud默认的负载均衡策略是RoundRobinLoadBalancer,我们可以通过配置调整负载均衡策略为随机策略RandomLoadBalancer。

调整方法很简单,官网说了:

余事以后再说吧。

以上就是Spring cloud负载均衡@LoadBalanced &amp; LoadBalancerClient的详细内容,更多关于Spring cloud负载均衡@LoadBalanced LoadBalancerClient的资料请关注脚本之家其它相关文章!

相关文章

  • Java设计模式常用原则解析

    Java设计模式常用原则解析

    这篇文章主要介绍了Java设计模式常用原则解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-05-05
  • Java判断字符串是否是有效的括号(实例详解)

    Java判断字符串是否是有效的括号(实例详解)

    给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效,有效字符串需要满足:左括号必须用相同类型的右括号闭合,这篇文章主要介绍了Java判断字符串是否是有效的括号,需要的朋友可以参考下
    2023-10-10
  • 解决Maven依赖冲突的方法

    解决Maven依赖冲突的方法

    本文主要介绍了解决Maven依赖冲突的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • SpringBoot整合分布式锁redisson的示例代码

    SpringBoot整合分布式锁redisson的示例代码

    这篇文章主要介绍了SpringBoot整合分布式锁redisson,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-02-02
  • Spring mvc 实现用户登录的方法(拦截器)

    Spring mvc 实现用户登录的方法(拦截器)

    这篇文章主要介绍了Spring mvc 实现用户登录的方法(拦截器),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-07-07
  • IntelliJ IDEA 2019.1.1 for MAC 下载和注册码激活教程图解

    IntelliJ IDEA 2019.1.1 for MAC 下载和注

    这篇文章主要介绍了IntelliJ IDEA 2019.1.1 for MAC 下载和注册码激活,教程,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-04-04
  • Spring boot搭建web应用集成thymeleaf模板实现登陆

    Spring boot搭建web应用集成thymeleaf模板实现登陆

    这篇文章主要介绍了Spring boot搭建web应用集成thymeleaf模板实现登陆,页面使用bootstrap,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-12-12
  • IntelliJ IDEA设置Tabs实现同时打开多个文件且分行显示

    IntelliJ IDEA设置Tabs实现同时打开多个文件且分行显示

    今天小编就为大家分享一篇关于IntelliJ IDEA设置Tabs实现同时打开多个文件且分行显示,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2018-10-10
  • Java数据结构之图的路径查找算法详解

    Java数据结构之图的路径查找算法详解

    这篇文章主要为大家详细介绍了java数据结构中图的路径查找算法,文中的示例代码讲解详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-11-11
  • Java发送邮箱验证码、session校验功能

    Java发送邮箱验证码、session校验功能

    本篇主要描述“发送邮箱验证码、session校验”相关前(html\js)后(java)台代码,业务逻辑示例,需要的朋友可以参考下
    2018-02-02

最新评论