写作背景
本文是接上一篇《SpringCloud Netflix复习之Eureka》来写的,有了Eureka之后,微服务之间调用可以通过Eureka Server拉取到服务注册表,就知道了要调用的下游服务的访问IP加端口等信息。但是呢,现在一般微服务都是多实例部署,一来防止单点问题,二来可以水平扩展提高并发能力。那么问题来了,服务A调用服务B,服务B是多实例部署的,我服务A怎么知道要调用服务B哪个实例呢?是随机还是轮询,还是有更智能一些的根据负载和机器配置来选择,Ribbon就是来干这件事的。
本文来复习SpringCloud Ribbon的相关知识,书写思路是以下几个方面
- SpringCloud Ribbon是什么,干了什么事情
- Ribbon组件的核心功能
- 上手实战
- 从源码角度来看下Ribbon的实现原理
SpringCloud Ribbon是什么,干了什么事情
Ribbon本身是Netflix公司(类似国内爱奇艺)研发的一个用于客户端负载均衡的组件,SpringCloud官方拿来封装了一下,成为SpringCloud Netflix生态的一员。刚刚说到了客户端的负载均衡,对应的还有个服务端负载均衡,常见的比如Nginx就是服务端负载均衡,相信你已经懂了一个是从客户端自己发起并执行的,一个是请求到达服务器之后再根据负载算法选择目标服务器。
Ribbon组件的核心功能
Ribbon提供了一系列完善的配置,比如超时配置,重试配置。通过ILoadBalancer获取所有服务实例列表ServerList,然后基于IRule实现的某种负载均衡算法,比如随机、轮询等选出一个服务实例,然后通过RestTemplate发起一个Rest请求。
可以说Ribbon这个中间件最核心的组件就是ILoadBalancer,然后服务实例列表的ServerList,以及用于负载算法IRule从ServerList中选出一个服务实例,还有一个用于ping每个服务实例判断其是否存活的IPing组件。
Ribbon内置了哪些负载均衡算法
Ribbon内置了许多负载均衡算法规则,这些规则的顶级接口是com.netflix.loadbalancer.IRule,可以通过实现IRule接口自定义负载均衡算法,但是一般用内置的负载均衡算法就够了。
RandomRule:随机找一个服务实例,这种基本不会用。
RoundRobinRule:轮询,从一堆server list中轮询选择出来一个server,每个server平摊到的这个请求,基本上是平均的,比如你100个请求,然后5个实例,基本每个实例会打到20个请求。有个限制默认超过10次获取的server都不可用,会返回空
AvailabilityFilteringRule:顾名思义,这个规则会考察服务器的可用性。如果3次连接失败,就会等待30秒后再次访问;如果某个服务器的连接数超限也就是并发请求太高了,那么会绕过去,不再访问
WeightedResponseTimeRule:带着权重的,每个服务器可以有权重,权重越高优先访问,如果某个服务器响应时间比较长,那么权重就会降低,减少访问
ZoneAvoidanceRule:根据区域来进行负载均衡,说白了就是优先在同一个机房内的节点轮询选取
BestAvailableRule:最小链接数策略,遍历ServerList从中选出一个可用且连接数最小的Server。
RetryRule:重试,默认继承RoundRobinRule就是通过轮询找到的服务器不可用或者请求失败,可以重新找一个服务器,而且没有RoundRoinRule超过10的限制,只要serverList不挂会不停选取判断。
其中ZoneAvoiddanceRule是默认策略,这个在后面源码中也有体现
上手实战
在SpringCloud里Ribbon实战
Springcloud项目中使用Ribbon只需要注入RestTemplate,然后加一个@LoadBalanced注解就可以了
/**
* Ribbon负载均衡
*
* @return RestTemplate
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
连依赖都不用加,因为Eureka客户端里已经有Ribbon的依赖,所以只要你项目里已经有了eureka客户端的依赖
RestTemplate本身只是一个Http组件,指定一个url发起一个Http请求,它是不具备负载均衡的功能的,但是这里加了@LoadBalanced注解之后,底层就会用Ribbon实现负载均衡。
我这边演示fc-service-portal(端口是8002)调用fc-service-screen服务,其中fc-service-screen启动两个实例,一个端口是8003,一个端口是8004。
下面是fc-service-portal服务里的Controller代码,通过RestTemplate调用fc-service-screen的/getPort接口,主要是查端口,根据端口号来区分调用的是哪个实例
@RestController
public class HelloWorldController {
@Resource
RestTemplate restTemplate;
@GetMapping("/getPort")
public int getPort() {
return restTemplate.getForObject("http://fc-service-screen/getPort", Integer.class);
}
}
fc-service-sceen8003和8004里Controller里的代码都是下面这样的
@RestController
public class HelloWordController {
@Value("${server.port}")
int port;
@GetMapping("/getPort")
public int getPort() {
return port;
}
}
启动所有服务后,看下Eureka的注册情况
可以看到fc-service-screen已经有两个实例了
我们请求http://localhost:8002/getPort 看返回结果情况
可以看的出来是轮询的访问方式
从源码角度看下Ribbon实现原理
SpringCloud与Ribbon整合的原理
从@LoadBalanced注解入手
/**
* Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient.
* @author Spencer Gibb
*/
@Target({
ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
看注释的意思是@LoadBalanced注解的意思是将一个RestTemplate标记为底层采用LoadBalancerClient来执行实际的Http的请求,支持负载均衡。还是老套路找下XXAutoConfiguration类,找到了LoadBalancerAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
//定制restTemplate
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
});
}
可以看到有个restTemplate的列表然后每个restTemplate都被定制了,我们找下RestTemplateCustomizer
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
//往restTemplate里设置了拦截器LoadBalancerInterceptor
restTemplate.setInterceptors(list);
};
}
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new