乐趣区

为何一个LoadBalanced注解就能让RestTemplate拥有负载均衡的能力享学Spring-Cloud

每篇一句

你应该思考:为什么往往完成比完美更重要?

前言

Spring Cloud 微服务应用体系中,远程调用都应负载均衡。我们在使用 RestTemplate 作为远程调用客户端的时候,开启负载均衡极其简单:一个 @LoadBalanced 注解就搞定了
相信大家大都使用过 RibbonClient 端 的负载均衡,也许你有和我一样的感受:Ribbon 虽强大但不是特别的好用 。我研究了一番,其实根源还是我们对它内部的原理不够了解,导致对一些现象无法给出合理解释,同时也影响了我们对它的 定制和扩展 。本文就针对此做出梳理,希望大家通过本文也能够对Ribbon 有一个较为清晰的理解(本文只解释它 @LoadBalanced 这一小块内容)。

开启客户端负载均衡只需要一个注解即可,形如这样:

@LoadBalanced // 标注此注解后,RestTemplate 就具有了客户端负载均衡能力
@Bean
public RestTemplate restTemplate(){return new RestTemplate();
}

Spring 是 Java 界最优秀、最杰出的重复发明轮子作品一点都不为过。本文就代领你一探究竟,为何开启 RestTemplate 的负载均衡如此简单。

说明:本文建立在你已经熟练使用 RestTemplate,并且了解RestTemplate 它相关组件的原理的基础上分析。若对这部分还比较模糊,强行推荐你 参看我前面这篇文章:RestTemplate 的使用和原理你都烂熟于胸了吗?【享学 Spring MVC】

RibbonAutoConfiguration

这是 Spring Boot/Cloud 启动 Ribbon 的入口自动配置类,需要先有个大概的了解:

@Configuration
// 类路径存在 com.netflix.client.IClient、RestTemplate 等时生效
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class) 
// // 允许在单个类中使用多个 @RibbonClient
@RibbonClients 
// 若有 Eureka,那就在 Eureka 配置好后再配置它~~~(如果是别的注册中心呢,ribbon 还能玩吗?)@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})
// 加载配置:ribbon.eager-load --> true 的话,那么项目启动的时候就会把 Client 初始化好,避免第一次惩罚
@EnableConfigurationProperties({RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class})
public class RibbonAutoConfiguration {

    @Autowired
    private RibbonEagerLoadProperties ribbonEagerLoadProperties;
    // Ribbon 的配置文件们~~~~~~~(复杂且重要)@Autowired(required = false)
    private List<RibbonClientSpecification> configurations = new ArrayList<>();

    // 特征,FeaturesEndpoint 这个端点 (`/actuator/features`) 会使用它 org.springframework.cloud.client.actuator.HasFeatures
    @Bean
    public HasFeatures ribbonFeature() {return HasFeatures.namedFeature("Ribbon", Ribbon.class);
    }


    // 它是最为重要的,是一个 org.springframework.cloud.context.named.NamedContextFactory  此工厂用于创建命名的 Spring 容器
    // 这里传入配置文件,每个不同命名空间就会创建一个新的容器(和 Feign 特别像)设置当前容器为父容器
    @Bean
    public SpringClientFactory springClientFactory() {SpringClientFactory factory = new SpringClientFactory();
        factory.setConfigurations(this.configurations);
        return factory;
    }

    // 这个 Bean 是关键,若你没定义,就用系统默认提供的 Client 了~~~
    // 内部使用和持有了 SpringClientFactory。。。@Bean
    @ConditionalOnMissingBean(LoadBalancerClient.class)
    public LoadBalancerClient loadBalancerClient() {return new RibbonLoadBalancerClient(springClientFactory());
    }
    ...
}

这个配置类最重要的是完成了 Ribbon 相关组件的自动配置,有了 LoadBalancerClient 才能做负载均衡(这里使用的是它的唯一实现类RibbonLoadBalancerClient


@LoadBalanced

注解本身及其简单(一个属性都木有):

// 所在包是 org.springframework.cloud.client.loadbalancer
// 能标注在字段、方法参数、方法上
// JavaDoc 上说得很清楚:它只能标注在 RestTemplate 上才有效
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {}

它最大的特点:头上标注有 @Qualifier 注解,这是它生效的最重要因素之一,本文后半啦我花了大篇幅介绍它的生效时机。
关于 @LoadBalanced 自动生效的配置,我们需要来到这个自动配置类:LoadBalancerAutoConfiguration

LoadBalancerAutoConfiguration

// Auto-configuration for Ribbon (client-side load balancing).
// 它的负载均衡技术依赖于的是 Ribbon 组件~
// 它所在的包是:org.springframework.cloud.client.loadbalancer
@Configuration
@ConditionalOnClass(RestTemplate.class) // 可见它只对 RestTemplate 生效
@ConditionalOnBean(LoadBalancerClient.class) // Spring 容器内必须存在这个接口的 Bean 才会生效(参见:RibbonAutoConfiguration)@EnableConfigurationProperties(LoadBalancerRetryProperties.class) // retry 的配置文件
public class LoadBalancerAutoConfiguration {
    
    // 拿到容器内所有的标注有 @LoadBalanced 注解的 Bean 们
    // 注意:必须标注有 @LoadBalanced 注解的才行
    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();    
    // LoadBalancerRequestTransformer 接口:允许使用者把 request + ServiceInstance --> 改造一下
    // Spring 内部默认是没有提供任何实现类的(匿名的都木有)@Autowired(required = false)
    private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

    // 配置一个匿名的 SmartInitializingSingleton 此接口我们应该是熟悉的
    // 它的 afterSingletonsInstantiated()方法会在所有的单例 Bean 初始化完成之后,再调用一个一个的处理 BeanName~
    // 本处:使用配置好的所有的 RestTemplateCustomizer 定制器们,对所有的 `RestTemplate` 定制处理
    // RestTemplateCustomizer 下面有个 lambda 的实现。若调用者有需要可以书写然后扔进容器里既生效
    // 这种定制器:若你项目中有多个 RestTempalte,需要统一处理的话。写一个定制器是个不错的选择
    //(比如统一要放置一个请求拦截器:输出日志之类的)@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);
                }
            }
        });
    }
    
    // 这个工厂用于 createRequest()创建出一个 LoadBalancerRequest
    // 这个请求里面是包含 LoadBalancerClient 以及 HttpRequest request 的
    @Bean
    @ConditionalOnMissingBean
    public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
    }
    
    // ========= 到目前为止还和负载均衡没啥关系 ==========
    // ========= 接下来的配置才和负载均衡有关(当然上面是基础项)==========

    // 若有 Retry 的包,就是另外一份配置,和这差不多~~
    @Configuration
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    static class LoadBalancerInterceptorConfig {、// 这个 Bean 的名称叫 `loadBalancerClient`,我个人觉得叫 `loadBalancerInterceptor` 更合适吧(虽然 ribbon 是唯一实现)// 这里直接使用的是 requestFactory 和 Client 构建一个拦截器对象
        // LoadBalancerInterceptor 可是 `ClientHttpRequestInterceptor`,它会介入到 http.client 里面去
        // LoadBalancerInterceptor 也是实现负载均衡的入口,下面详解
        // Tips: 这里可没有 @ConditionalOnMissingBean 哦~~~~
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }
    
        
        // 向容器内放入一个 RestTemplateCustomizer 定制器
        // 这个定制器的作用上面已经说了:在 RestTemplate 初始化完成后,应用此定制化器在 ** 所有的实例上 **
        // 这个匿名实现的逻辑超级简单:向所有的 RestTemplate 都塞入一个 loadBalancerInterceptor 让其具备有负载均衡的能力
        
        // Tips:此处有注解 @ConditionalOnMissingBean。也就是说如果调用者自己定义过 RestTemplateCustomizer 类型的 Bean,此处是不会执行的
        // 请务必注意这点:容易让你的负载均衡不生效哦~~~~
        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
            return restTemplate -> {List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
        }
    }
    ...
}

这段配置代码稍微有点长,我把流程总结为如下几步:

  1. LoadBalancerAutoConfiguration要想生效类路径必须有 RestTemplate,以及 Spring 容器内必须有LoadBalancerClient 的实现 Bean

        1. `LoadBalancerClient` 的唯一实现类是:`org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient`
  2. LoadBalancerInterceptor是个 ClientHttpRequestInterceptor 客户端请求拦截器。它的作用是在客户端发起请求之前拦截,进而实现客户端的负载均衡
  3. restTemplateCustomizer()返回的匿名定制器 RestTemplateCustomizer 它用来给所有的 RestTemplate 加上负载均衡拦截器(需要注意它的 @ConditionalOnMissingBean 注解~)

不难发现,负载均衡实现的核心就是一个拦截器,就是这个拦截器让一个普通的 RestTemplate 逆袭成为了一个具有负载均衡功能的请求器

LoadBalancerInterceptor

该类唯一被使用的地方就是 LoadBalancerAutoConfiguration 里配置上去~

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

    // 这个命名都不叫 Client 了,而叫 loadBalancer~~~
    private LoadBalancerClient loadBalancer;
    // 用于构建出一个 Request
    private LoadBalancerRequestFactory requestFactory;
    ... // 省略构造函数(给这两个属性赋值)@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, this.requestFactory.createRequest(request, body, execution));
    }
}

此拦截器拦截请求后把它的 serviceName 委托给了 LoadBalancerClient 去执行,根据 ServiceName 可能对应 N 多个实际的 Server,因此就可以从众多的 Server 中运用均衡算法,挑选出一个最为合适的Server 做最终的请求(它持有真正的请求执行器ClientHttpRequestExecution)。


LoadBalancerClient

请求被拦截后,最终都是委托给了 LoadBalancerClient 处理。

// 由使用负载平衡器选择要向其发送请求的服务器的类实现
public interface ServiceInstanceChooser {

    // 从负载平衡器中为指定的服务选择 Service 服务实例。// 也就是根据调用者传入的 serviceId,负载均衡的选择出一个具体的实例出来
    ServiceInstance choose(String serviceId);
}

// 它自己定义了三个方法
public interface LoadBalancerClient extends ServiceInstanceChooser {
    
    // 执行请求
    <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
    <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
    
    // 重新构造 url:把 url 中原来写的服务名 换掉 换成实际的
    URI reconstructURI(ServiceInstance instance, URI original);
}

它只有一个实现类 RibbonLoadBalancerClient ServiceInstanceChooser 是有多个实现类的~)。

RibbonLoadBalancerClient

首先我们应当关注它的 choose() 方法:

public class RibbonLoadBalancerClient implements LoadBalancerClient {
    
    @Override
    public ServiceInstance choose(String serviceId) {return choose(serviceId, null);
    }
    // hint:你可以理解成分组。若指定了,只会在这个偏好的分组里面去均衡选择
    // 得到一个 Server 后,使用 RibbonServer 把 server 适配起来~~~
    // 这样一个实例就选好了~~~ 真正请求会落在这个实例上~
    public ServiceInstance choose(String serviceId, Object hint) {Server server = getServer(getLoadBalancer(serviceId), hint);
        if (server == null) {return null;}
        return new RibbonServer(serviceId, server, isSecure(server, serviceId),
                serverIntrospector(serviceId).getMetadata(server));
    }

    // 根据 ServiceId 去找到一个属于它的负载均衡器
    protected ILoadBalancer getLoadBalancer(String serviceId) {return this.clientFactory.getLoadBalancer(serviceId);
    }

}

choose 方法 :传入 serviceId,然后通过SpringClientFactory 获取负载均衡器 com.netflix.loadbalancer.ILoadBalancer,最终委托给它的chooseServer() 方法选取到一个 com.netflix.loadbalancer.Server 实例,也就是说真正完成 Server 选取的是ILoadBalancer

ILoadBalancer以及它相关的类是一个较为庞大的体系,本文不做更多的展开,而是只聚焦在我们的流程上

LoadBalancerInterceptor执行的时候是直接委托执行的 loadBalancer.execute() 这个方法:

RibbonLoadBalancerClient:// hint 此处传值为 null:一视同仁
    // 说明:LoadBalancerRequest 是通过 LoadBalancerRequestFactory.createRequest(request, body, execution)创建出来的
    // 它实现 LoadBalancerRequest 接口是用的一个匿名内部类,泛型类型是 ClientHttpResponse
    // 因为最终执行的显然还是执行器:ClientHttpRequestExecution.execute()
    @Override
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {return execute(serviceId, request, null);
    }
    // public 方法(非接口方法)public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
        // 同上:拿到负载均衡器,然后拿到一个 serverInstance 实例
        ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
        Server server = getServer(loadBalancer, hint);
        if (server == null) { // 若没找到就直接抛出异常。这里使用的是 IllegalStateException 这个异常
            throw new IllegalStateException("No instances available for" + serviceId);
        }

        // 把 Server 适配为 RibbonServer  isSecure:客户端是否安全
        // serverIntrospector 内省  参考配置文件:ServerIntrospectorProperties
        RibbonServer ribbonServer = new RibbonServer(serviceId, server,
                isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server));

        // 调用本类的重载接口方法~~~~~
        return execute(serviceId, ribbonServer, request);
    }

    // 接口方法:它的参数是 ServiceInstance --> 已经确定了唯一的 Server 实例~~~
    @Override
    public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
    
        // 拿到 Server)(说白了,RibbonServer 是 execute 时的唯一实现)Server server = null;
        if (serviceInstance instanceof RibbonServer) {server = ((RibbonServer) serviceInstance).getServer();}
        if (server == null) {throw new IllegalStateException("No instances available for" + serviceId);
        }

        // 说明:执行的上下文是和 serviceId 绑定的
        RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId);
        ... 
        // 真正的向 server 发送请求,得到返回值
        // 因为有拦截器,所以这里肯定说执行的是 InterceptingRequestExecution.execute()方法
        // so 会调用 ServiceRequestWrapper.getURI(),从而就会调用 reconstructURI()方法
            T returnVal = request.apply(serviceInstance);
            return returnVal;
        ... // 异常处理
    }

returnVal是一个 ClientHttpResponse,最后交给handleResponse() 方法来处理异常情况(若存在的话),若无异常就交给提取器提值:responseExtractor.extractData(response),这样整个请求就算全部完成了。

使用细节

针对 @LoadBalanced 下的 RestTemplate 的使用,我总结如下细节供以参考:

  1. 传入的 String 类型的 url 必须是绝对路径(http://...),否则抛出异常:java.lang.IllegalArgumentException: URI is not absolute
  2. serviceId不区分大小写(http://user/... 效果同 http://USER/...
  3. serviceId后请不要跟 port 端口号了~~~

最后,需要特别指出的是:标注有 @LoadBalancedRestTemplate只能书写 serviceId 而不能再写 IP 地址 / 域名 去发送请求了。若你的项目中两种 case 都有需要,请定义多个 RestTemplate 分别应对不同的使用场景~

本地测试

了解了它的执行流程后,若需要本地测试(不依赖于注册中心),可以这么来做:

// 因为自动配置头上有 @ConditionalOnMissingBean 注解,所以自定义一个覆盖它的行为即可
// 此处复写它的 getServer()方法,返回一个固定的(访问百度首页)即可,方便测试
@Bean
public LoadBalancerClient loadBalancerClient(SpringClientFactory factory) {return new RibbonLoadBalancerClient(factory) {
        @Override
        protected Server getServer(ILoadBalancer loadBalancer, Object hint) {return new Server("www.baidu.com", 80);
        }
    };
}

这么一来,下面这个访问结果就是百度首页的 html 内容喽。

@Test
public void contextLoads() {String obj = restTemplate.getForObject("http://my-serviceId", String.class);
    System.out.println(obj);
}

此处 my-serviceId 肯定是不存在的,但得益于我上面自定义配置的LoadBalancerClient

什么,写死 return 一个 Server 实例不优雅?确实,总不能每次上线前还把这部分代码给注释掉吧,若有多个实例呢?还得自己写负载均衡算法吗?很显然 Spring Cloud 早早就为我们考虑到了这一点:脱离 Eureka 使用配置 listOfServers 进行客户端负载均衡调度(<clientName>.<nameSpace>.listOfServers=<comma delimited hostname:port strings>

对于上例我只需要在主配置文件里这么配置一下:

# ribbon.eureka.enabled=false # 若没用 euraka,此配置可省略。否则不可以
my-serviceId.ribbon.listOfServers=www.baidu.com # 若有多个实例请用逗号分隔

效果完全同上。

Tips:这种配置法不需要是完整的绝对路径,http://是可以省略的(new Server()方式亦可)

自己添加一个记录请求日志的拦截器可行吗?

显然是可行的,我给出示例如下:

@LoadBalanced
@Bean
public RestTemplate restTemplate() {RestTemplate restTemplate = new RestTemplate();
    List<ClientHttpRequestInterceptor> list = new ArrayList<>();
    list.add((request, body, execution) -> {System.out.println("当前请求的 URL 是:" + request.getURI().toString());
        return execution.execute(request, body);
    });
    restTemplate.setInterceptors(list);
    return restTemplate;
}

这样每次客户端的请求都会打印这句话:当前请求的 URI 是:http://my-serviceId,一般情况(缺省情况)自定义的拦截器都会在负载均衡拦截器前面执行(因为它要执行最终的请求)。若你有必要定义多个拦截器且要控制顺序,可通过 Ordered 系列接口来实现~


最后的最后,我抛出一个非常非常重要的问题:

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

@Autowired + @LoadBalanced能把你配置的 RestTemplate 自动注入进来拿来定制呢???核心原理是什么?

提示:本原理内容属于 Spring Framwork 核心技术,建议深入思考而不囫囵吞枣。有疑问的可以给我留言,我也将会在下篇文章给出详细解答(建议先思考)


推荐阅读

RestTemplate 的使用和原理你都烂熟于胸了吗?【享学 Spring MVC】
@Qualifier 高级应用 — 按类别批量依赖注入【享学 Spring】

总结

本文以大家熟悉的 @LoadBalancedRestTemplate为切入点介绍了 Ribbon 实现负载均衡的执行流程,当然此部分对 Ribbon 整个的核心负载体系知识来说知识冰山一角,但它作为敲门砖还是很有意义的,希望本文能勾起你对 Ribbon 体系的兴趣,深入了解它~

== 若对 Spring、SpringBoot、MyBatis 等源码分析感兴趣,可加我 wx:fsx641385712,手动邀请你入群一起飞 ==
== 若对 Spring、SpringBoot、MyBatis 等源码分析感兴趣,可加我 wx:fsx641385712,手动邀请你入群一起飞 ==

退出移动版