
3.2 Spring Cloud LoadBalancer负载均衡组件
SCL作为新一代Spring Cloud客户端负载均衡的实现,若要使用,需要在pom里加上依赖:

SCL相关的代码在spring-cloud-commons模块中,相关类和接口的关系如图3-1所示。
图3-1中,这些类和定义的含义如下:
·ServiceInstanceChooser:服务实例选择器,根据服务名获取一个服务实例(ServiceInstance)。
·LoadBalancerClient:客户端负载均衡器,继承ServiceInstanceChooser,会根据ServiceInstance和Request请求信息执行最终的结果。
·BlockingLoadBalancerClient:基于Spring Cloud LoadBalancer的LoadBalancerClient默认实现。
·RibbonLoadBalancerClient:基于Netflix Ribbon的LoadBalancerClient实现。

图3-1
1.ServicelnstanceChooser
org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser 服务实例选择器的定义如下:

我们自定义一个 ServiceInstanceChooser 的实现类来完成负载均衡操作(由于客户端负载均衡器 LoadBalancerClient 跟 Request 请求信息有着强耦合的关系,会涉及 RestTemplate 或OpenFeign的一些概念,相关内容将在后面介绍):


接下来在 Controller 里使用 RandomServiceInstanceChooser (内部使用随机算法)获取ServiceInstance,再使用RestTemplate进行服务调用:

执行Shell脚本,调用Controller的customChooser路径,返回结果如下:


我们可以看到,RandomServiceInstanceChooser和 Controller里的 customChooser方法这两段代码内部先使用 DiscoveryClient基于服务名获取这个服务对应的所有 ServiceInstance集合,然后根据负载均衡算法从这个集合中得到一个 ServiceInstance,最后基于获取的 ServiceInstance里的 IP和端口使用 RestTemplate发起 HTTP调用。仔细想想,这两段代码还有更进一步的简化空间:直接使用RestTemplate根据服务名进行调用,屏蔽ServiceInstance的获取细节。

在实际情况下,使用 Spring Cloud 确实可以直接基于服务名进行服务调用。这是因为Spring Cloud扩展了RestTemplate,只需要在定义 RestTemplate Bean时加上@LoadBalanced 注解,就可以基于服务名进行服务调用:

这个神秘的@LoadBalanced注解在底层做了什么事情呢?我们来分析一下。
2.@LoadBalanced
spring-cloud-commons 模块中的 META-INF/spring.factories 文件里存在 LoadBalancerAuto-Configuration 这个自动化配置类,根据工厂加载机制会被 ApplicationContext 加载。该自动化配置类内部的Bean构造代码如下:


上述代码中:
①获取ApplicationContext中所有被@LoadBalanced注解修饰的RestTemplate。
② List<RestTemplateCustomizer>是 ApplicationContext 存在的 RestTemplateCustomizer Bean的集合。
③ 遍历代码①处得到的 RestTemplate 集合,并使用 RestTemplateCustomizer 集合给每个RestTemplate定制。
这个 RestTemplateCustomizer 定制的时候做了哪些操作呢?LoadBalancerAutoConfiguration内部的LoadBalancerInterceptorConfig配置类中定义了RestTemplateCustomizer:


上述代码中:
① 条件注解。LoadBalancerInterceptorConfig配置类只有在ClassLoader不存在 RetryTemplate (Spring Retry框架提供的模板类)时才会生效。
②定义LoadBalancerInterceptor Bean,这个拦截器继承ClientHttpRequestInterceptor,可以被添加到RestTemplate的拦截器列表中。
③ 定义RestTemplateCustomizer Bean,会在LoadBalancerAutoConfiguration里的RestTemplate-Customizer列表中存在。
④LoadBalancerInterceptor参数是代码②处创建的Bean。
⑤ 使用 lambda表达式在 RestTemplate的拦截器列表中添加 LoadBalancerInterceptor拦截器。
提示:如果ClassLoader存在RetryTemplate,会触发另外一个配置类:RetryInterceptorAuto-Configuration。该配置类内部的操作与 LoadBalancerInterceptorConfig配置类唯一的区别就是构造了 RetryLoadBalancerInterceptor 拦截器(跟 LoadBalancerInterceptor 相比,在RestTemplate调用失败的情况下会进行重试操作)。
看到这里,大家应该明白了。@LoadBalanced 直接修饰的 RestTemplate 会被添加一个LoadBalancerInterceptor拦截器。接下来进入 LoadBalancerInterceptor拦截器,看它内部做了哪些操作,其代码如下:

上述代码中:
① LoadBalancerInterceptor 构造器需要 LoadBalancerClient 和 LoadBalancerRequestFactory参数(默认会在 LoadBalancerAutoConfiguration 里被构造,开发者可以进行覆盖)。前者根据负载均衡请求(LoadBalancerRequest)和服务名做真正的服务调用,后者构造负载均衡请求(LoadBalancerRequest),构造过程中会使用LoadBalancerRequestTransformer对请求做一些自定义的转换操作(默认情况下,LoadBalancerRequestTransformer接口无任何实现类,开发者可以根据业务构造Bean进行Request的转换操作)。
②服务名使用URI中的host信息。
③使用LoadBalancerClient客户端负载均衡器做真正的服务调用。
3.LoadBalancerClient
LoadBalancerClient(客户端负载均衡器)会根据负载均衡请求和服务名执行真正的负载均衡操作,在介绍SCL类和接口关系时也提到过这个接口,该接口的具体定义如下:


·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方法。
execute方法的源码如下:

LoadBalancerRequest 表示一次负载均衡请求,会被 LoadBalancerRequestFactory 构造。构造出的负载均衡请求实现类是 ServiceRequestWrapper(内部基于服务实例和请求信息构造出真正的 URI)。然后根据 LoadBalancerRequestTransformer 做二次加工。LoadBalancerRequest 接口定义如下:

LoadBalacerRequestFactory构造负载均衡请求的过程如下:

LoadBalancerClient 默认的实现类为基于 SCL 的 BlockingLoadBalancerClient,其定义如下:



上述代码中:
① BlockingLoadBalancerClient 构造函数依赖 LoadBalancerClientFactory。LoadBalancer-ClientFactory是一个用于创建 ReactiveLoadBalancer的工厂类,LoadBalancerClientFactory内部维护着一个 Map,该 Map用于保存各个服务的 ApplicationContext(Map的 key是服务名)。每个ApplicationContext内部维护对应服务的一些配置和Bean。
② 没有 ServiceInstance参数的 execute方法内部会调用 choose方法获取 ServiceInstance,然后调用另外一个重载的execute方法。
③ 有 ServiceInstance参数的 execute方法把负载均衡的操作直接委托给 LoadBalancerRequest负载均衡请求处理。
④ 根据 URI 和找到的服务实例 ServiceInstance 重新构造一个 URI,这个过程被封装在LoadBalancerUriTools工具类里。
⑤代码②处提到的choose方法会返回服务实例ServiceInstance。choose方法首先会根据服务名和 LoadBalancerClientFactory 得到该服务名所对应的 ReactiveLoadBalancer Bean,然后调用ReactiveLoadBalancer的choose方法得到服务实例ServiceInstance。
4.Spring Cloud LoadBalancer总结
下面对Spring Cloud LoadBalancer内容做个总结。
①@LoadBalanced 注解修饰 RestTemplate 后,会根据 RestTemplateCustomized 注解给RestTemplate 做定制化操作。这个定制化操作一定含有一个添加 LoadBalancerInterceptor 负载均衡拦截器的操作。此外,我们还可以扩展添加符合业务需求的自定义定制化操作。整个过程如图3-2所示。

图3-2
② LoadBalancerInterceptor负载均衡拦截器拦截的背后会通过 LoadBalancerClient的execute方法完成最终的调用。execute 需要两个参数,一个是服务名,请求信息中的 host 即表示服务名;另一个是负载均衡请求(LoadBalancerRequest),通过 LoadBalancer-RequestFactory构造完成。整个过程如图3-3所示。

图3-3
Spring Cloud LoadBalancer还提供了@LoadBalancerClient注解用于进行自定义的配置操作,如自定义负载均衡算法(默认是轮询算法)、自定义 ServiceInstanceListSupplier(默认会缓存服务实例列表)。

@LoadBalancerClient注解有3个属性,分别是value:String、name:String和configuration:Class[]。name 和 value 属性表示同一个含义,即服务名,且只能设置其中一个属性。上述代码中,nacos-provider-lb对应的是服务名,每个服务名拥有单独的自定义配置。
提示:@LoadBalancerClients 注解的 defaultConfiguration 属性表示默认的配置类,所有的BlockingLoadBalancerClient都会使用这些配置类里的配置。
configuration属性表示配置类,配置类中返回的 Bean会替换 LoadBalancerClientConfiguration配置类中已经存在的Bean(前文提到LoadBalancerClientFactory 内部维护着一个Map,用于保存各个服务的 ApplicationContext。每个 ApplicationContext构造的时候都会加上 LoadBalancer-ClientConfiguration 配置类)。比如,LoadBalancerClientConfiguration 配置类中负载均衡策略为默认的轮询策略:

上述代码中:
① 默认的负载均衡策略 Bean 被 ConditionalOnMissingBean 注解修饰,表示开发者配置的配置类优先级更高。
②默认使用轮询负载均衡策略。
我们可以在 MyLoadBalancerConfiguration 配置类中定义一个新的 ReactorLoadBalancer 来覆盖默认的负载均衡策略:

RandomLoadBalancer是一个自定义的实现随机算法的负载均衡策略:

