你应该思考:为什么往往完成比完美更重要?
在 Spring Cloud
微服务应用体系中,远程调用都应负载均衡。我们在使用 RestTemplate
作为远程调用客户端的时候,开启负载均衡极其简单:
一个 @LoadBalanced
注解就搞定了
。
相信大家大都使用过 Ribbon
做 Client端
的负载均衡,也许你有和我一样的感受: Ribbon虽强大但不是特别的好用
。我研究了一番,其实根源还是我们对它内部的原理不够了解,导致对一些现象无法给出合理解释,同时也影响了我们对它的 定制和扩展
。本文就针对此做出梳理,希望大家通过本文也能够对 Ribbon
有一个较为清晰的理解(本文只解释它 @LoadBalanced
这一小块内容)。
开启客户端负载均衡只需要一个注解即可,形如这样:
@LoadBalanced // 标注此注解后,RestTemplate就具有了客户端负载均衡能力 @Bean public RestTemplate restTemplate(){ return new RestTemplate(); }
说 Spring
是Java界最优秀、最杰出的重复发明轮子作品一点都不为过。本文就代领你一探究竟,为何开启 RestTemplate
的负载均衡如此简单。
说明:本文建立在你已经熟练使用 RestTemplate
,并且了解 RestTemplate
它相关组件的原理的基础上分析。若对这部分还比较模糊,强行推荐你 先
参看我前面这篇文章: RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
这是 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
)
注解本身及其简单(一个属性都木有):
// 所在包是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
// 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); }; } } ... }
这段配置代码稍微有点长,我把流程总结为如下几步:
LoadBalancerAutoConfiguration
要想生效类路径必须有 RestTemplate
,以及Spring容器内必须有 LoadBalancerClient
的实现Bean
1. `LoadBalancerClient`的唯一实现类是:`org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient`
LoadBalancerInterceptor
是个 ClientHttpRequestInterceptor
客户端请求拦截器。它的作用是在客户端发起请求之前拦截, 进而实现客户端的负载均衡
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
处理。
// 由使用负载平衡器选择要向其发送请求的服务器的类实现 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
的使用,我总结如下细节供以参考:
String
类型的url必须是绝对路径( http://...
),否则抛出异常: java.lang.IllegalArgumentException: URI is not absolute
serviceId
不区分大小写( http://user/...效果同http://USER/...
) serviceId
后请不要跟port端口号了~~~
最后,需要特别指出的是:标注有 @LoadBalanced
的 RestTemplate
只能书写 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】
本文以大家熟悉的 @LoadBalanced
和 RestTemplate
为切入点介绍了 Ribbon
实现负载均衡的执行流程,当然此部分对 Ribbon
整个的核心负载体系知识来说知识冰山一角,但它作为敲门砖还是很有意义的,希望本文能勾起你对 Ribbon
体系的兴趣,深入了解它~
== 若对Spring、SpringBoot、MyBatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一起飞 ==
== 若对Spring、SpringBoot、MyBatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一起飞 ==