文章目录
- 1.Ribbon负载均衡简介
- 2.Ribbon实例
- 3.Ribbon源码分析
- 1.LoadBalancerInterceptor
- 2.RibbonLoadBalancerClient
- 3.ZoneAwareLoadBalancer
- 4.BaseLoadBalancer
- 5.默认的RoundRobinRule何时被覆盖?
- 6.PredicateBasedRule的choose
- 7.AbstractServerPredicate的chooseRoundRobinAfterFiltering
- 8.进行过滤器过滤
- 9.Predicates
- 10.ZoneAvoidancePredicate区域过滤器
- 11.AvailabilityPredicate可用性过滤器
- 12.执行过滤之后
- 13.常见的负载均衡算法
- 4.自定义Ribbon负载均衡策略
- 4.总结
1.Ribbon负载均衡简介
1.1Ribbon概述
1.1.1.Ribbon是什么
SpringCloud Ribbon是基于Netflix Ribbon实现的一套==客户端负载均衡==的工具。
简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出LoadBalanCer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。
1.1.2.Ribbon主要职责
LB(负载均衡)
LB,即负载均衡( Load Balance ),在微服务或分布式集群中经常用的一种应用。
负载均衡简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA。
常见的负载均衡有软件nginx , LVS,硬件F5等。
相应的在中间件,例如:dubbo和 SpringCloud中均给我们提供了负载均衡,SpringCloud的负载均衡算法可以自定义。
LB又分为两种,集中式LB和进程内LB
集中式LB(偏硬件)
即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如nginx ) ,由该设施负责把访问请求通过某种策略转发至服务的提供方;
进程内LB(偏软件)
将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服各提供方的地址。
1.1.3 官方资料
https://github.com/Netflix/ribbon/wiki
2.Ribbon实例
上一篇的案例中,我们启动了一个springcloud-demo,然后通过DiscoveryClient来获取服务实例信息,然后获取ip和端口来访问。
但是实际环境中,我们往往会开启很多个user-service的集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢?
一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。
接下来,我们就来使用Ribbon实现负载均衡。
2.1.Ribbon架构说明
Ribbon 在工作时分成两步:
第一步先选择 EurekaServer,它优先选择在同一个区域内负载较少的server。
第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。
其中Ribbon 提供了多种策略:比如轮询、随机和根据响应时间加权。
2.2.启动两个服务实例
首先我们启动两个springcloud-demo实例,一个80,一个81。
Eureka监控面板:
2.3.开启负载均衡
因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖
加入Ribbon的配置
# EurekaServer地址
eureka.client.service-url.defaultZone=http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
在RestTemplate的配置方法上添加@LoadBalanced
注解:
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}
修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:
@RestController
@RequestMapping("consumer")
public class ConsumerController {
//改成下面这行
private static final String REST_URL_PREFIX = "http://SPRINGCLOUD-DEMO-SERVICE";
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/test")
public List<User> consumerTest(){
return this.restTemplate.getForObject(REST_URL_PREFIX+"/all",List.class);
}
}
测试:
Ribbon整合后可以直接调用微服务而不再关心地址和端口!!!
2.4.源码跟踪
为什么我们只输入了service名称就可以访问了呢?之前还要获取ip和端口。显然有人帮我们根据service名称,获取到了服务实例的ip和端口。它就是LoadBalancerInterceptor
我们进行源码跟踪:
继续跟入execute方法:发现获取了8001端口的服务
org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient
如果再跟下一次,发现获取的是8002
2.5.负载均衡策略
Ribbon默认的负载均衡策略是简单的轮询,我们可以测试一下:
编写测试类,在刚才的源码中我们看到拦截中是使用RibbonLoadBalanceClient
来进行负载均衡的,其中有一个choose
方法,是这样介绍的:
现在这个就是负载均衡获取实例的方法。我们对注入这个类的对象,然后对其测试:
import com.bruceliu.SpringcloudDemoConsumerApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringcloudDemoConsumerApplication.class)
public class LoadBalanceTest {
@Autowired
RibbonLoadBalancerClient client;
@Test
public void test1(){
for (int i = 0; i <10 ; i++) {
ServiceInstance instance = this.client.choose("SPRINGCLOUD-DEMO-SERVICE");
System.out.println(instance.getHost() + ":" + instance.getPort());
}
}
}
运行结果:
符合了我们的预期推测,确实是轮询方式。
我们是否可以修改负载均衡的策略呢?继续跟踪源码,发现这么一段代码:
我们看看这个rule是谁:
这里的rule默认值是一个RoundRobinRule
,看类的介绍:
这不就是轮询的意思嘛。
我们注意到,这个类其实是实现了接口IRule的,查看一下:
定义负载均衡的规则接口。
它有以下实现:
SpringBoot也帮我们提供了修改负载均衡规则的配置入口:
SPRINGCLOUD-DEMO-SERVICE:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
格式是:{服务名称}.ribbon.NFLoadBalancerRuleClassName
,值就是IRule的实现类。
再次测试,发现结果变成了随机:
2.6.重试机制
我们现在关闭一个springcloud-demo-8001实例
因为服务剔除的延迟,consumer并不会立即得到最新的服务列表,此时再次访问你会得到错误提示:
但是此时,8002服务其实是正常的。
因此Spring Cloud 整合了Spring Retry 来增强RestTemplate的重试能力,当一次服务调用失败后,不会立即抛出一次,而是再次重试另一个服务。
只需要简单配置即可实现Ribbon的重试:
spring:
cloud:
loadbalancer:
retry:
enabled: true # 开启Spring Cloud的重试功能
SPRINGCLOUD-DEMO-SERVICE:
ribbon:
ConnectTimeout: 250 # Ribbon的连接超时时间
ReadTimeout: 1000 # Ribbon的数据读取超时时间
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数
根据如上配置,当访问到某个服务超时后,它会再次尝试访问下一个服务实例,如果不行就再换一个实例,如果不行,则返回失败。切换次数取决于MaxAutoRetriesNextServer
参数的值
引入spring-retry依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
我们重启springcloud-demo-consumer,测试,发现即使springcloud-demo-8001宕机,也能通过另一台服务实例获取到结果!
2.7 Ribbon核心组件IRule(面试题)
策略名称 | 策略对应的类名 | 实现原理 |
---|---|---|
轮询策略(默认) | RoundRobinRule | 轮询策略表示每次都顺序取下一个 provider,比如一共有 5 个provider,第 1 次取第 1 个,第 2次取第 2 个,第 3 次取第 3 个,以此类推 |
权重轮询策略 | WeightedResponseTimeRule | 1.根据每个 provider 的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性越低。2.原理:一开始为轮询策略,并开启一个计时器,每 30 秒收集一次每个 provider 的平均响应时间,当信息足够时,每个 provider附上一个权重,并按权重随机选择provider,高权越重的 provider会被高概率选中。 |
随机策略 | RandomRule | 从 provider 列表中随机选择一个provider |
最少并发数策略 | BestAvailableRule | 选择正在请求中的并发数最小的 provider,除非这个provider 在熔断中。 |
在“选定的负载均衡策略”基础上进行重试机制 | RetryRule | 1.“选定的负载均衡策略”这个策略是轮询策略RoundRobinRule 2.该重试策略先设定一个阈值时间段,如果在这个阈值时间段内当选择 provider 不成功,则一直尝试采用“选定的负载均衡策略:轮询策略”最后选择一个可用的provider |
可用性敏感策略 | AvailabilityFilteringRule | 过滤性能差的 provider,有 2种:第一种:过滤掉在 eureka 中处于一直连接失败 provider 第二种:过滤掉高并发的 provider |
区域敏感性策略 | ZoneAvoidanceRule | 1.以一个区域为单位考察可用性,对于不可用的区域整个丢弃,从剩下区域中选可用的provider2.如果这个 ip 区域内有一个或多个实例不可达或响应变慢,都会降低该 ip 区域内其他 ip 被选中的权重。机架! |
Ribbon的负载均衡算法内置了7种,如果还不能满足实际的要求,那么你需要自定义负载均衡算法~~~~~
写一个类实现IRUle接口,重写其中的方法
然后把自己的规则类,注入到系统中.....
2.8 修改访问服务的算法方式
方式一:修改代码更换负载均衡策略
在启动类中添加实例负载均衡的实例,则默认的轮询策略就会失效,具体如下:
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RibbonConfig {
/**
* 显示实例化 负载均衡的策略对象,那么默认的轮询策略就会失效
* @return
*/
@Bean
public IRule createRule(){
//修改为随机算法!
//return new RandomRule();
return new RoundRobinRule();
}
}
@SpringBootApplication
@EnableEurekaClient//开启客户端
@RibbonClient(name = "USER-SERVICE",configuration = RibbonConfig.class)
public class UserApp9090 {
public static void main(String[] args) {
SpringApplication.run(UserApp9090.class,args);
}
}
方式二:修改配置文件更换负载均衡策略
第二种方式在application.properties中设置分配的策略
#设置负载均衡策略 eureka-ribbon-provider 为调用的服务的名称
eureka-ribbon-provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
'eureka-ribbon-provider’是调用的服务的名称,后面的组成部分是固定的。
同时注释掉方式一中的内容
3.Ribbon源码分析
1.LoadBalancerInterceptor
为什么我们只输入了service
名称就可以访问了呢?之前还要获取ip和端口。显然有人帮我们根据service名称,获取到了服务实例的ip和端口。它就是LoadBalancerInterceptor
org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient
2.RibbonLoadBalancerClient
3.ZoneAwareLoadBalancer
调用父类的chooseServer
4.BaseLoadBalancer
内部规则是ZoneAvoidanceRule
,就是前面说的把默认的RoundRobinRule
给覆盖了,但是复合策略里面默认的又是RoundRobinRule
,只是加了一些过滤器,根据一些条件过滤服务器。
5.默认的RoundRobinRule何时被覆盖?
5.1.RibbonClientConfiguration
5.2.何时替换了RoundRobinRule
6.PredicateBasedRule的choose
最终到父类的choose方法,先获取负载均衡器lb,然后获取所有的服务器,再获取预测器getPredicate
,里面是个复合的AbstractServerPredicate
,可以当做过滤器,满足一定条件过滤服务器的,最后进行chooseRoundRobinAfterFiltering
轮询:
7.AbstractServerPredicate的chooseRoundRobinAfterFiltering
必须先走过滤器,过滤一遍,然后再轮询出服务器。
8.进行过滤器过滤
内部最终会进行ZoneAvoidancePredicate
和AvailabilityPredicate
两个过滤器的过滤,只要有一个不满足就被过滤掉了。
9.Predicates
10.ZoneAvoidancePredicate区域过滤器
我们一般默认就一个区域defaultzone,多个区域就需要有判断了,比如断电情况,负载这些,具体可以看ZoneAvoidancePredicate
的apply方法。一般情况只有一个区域,就返回true了:
11.AvailabilityPredicate可用性过滤器
这个是用来过滤掉不可用的服务器,不如连接不上,还有并发太多的
如果检测出断路器问题或者是服务器连接数超过限制了,就要过滤掉了。
12.执行过滤之后
最后过滤出来的服务器集合再进行轮询算法,选出一个。
核心的incrementAndGetModulo(int)
方法
老版本源码
private int incrementAndGetModulo(int modulo) {
int current;
int next;
do {
current = this.nextServerCyclicCounter.get(); //nextServerCyclicCounter是AtomicInteger对象,默认值0,可保证线程安全性
next = (current + 1) % modulo; //每次往后移一位,取集合中的下一个server。这里要注意的是从1开始,即数组中的第二个server会被第一个调用。
} while(!this.nextServerCyclicCounter.compareAndSet(current, next)); //操作完成后用CAS操作将next赋值给nextServerCyclicCounter
return next;
}
新版本源码
13.常见的负载均衡算法
IRule是以下七种负载均衡算法的父接口
说明:
4.自定义Ribbon负载均衡策略
官方文档指出:自定义的负载均衡配置类不能放在 @componentScan 所扫描的当前包下及其子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,也就是说我们达不到特殊化定制的目的了;
要求自定义的算法:依旧是轮询策略,但是每个服务器被调用5次后轮到下一个服务,即以前是每个服务被调用1次,现在是每个被调用5次。
1、自定义算法类必须继承 AbstractLoadBalanceRule 类
启动类在com.bruce.springcloud 包下,所以我们新建一个包: con.bruce.myrule,并在该包下新建一个类:CustomeRule
public class CustomeRule extends AbstractLoadBalancerRule
{
/*
total = 0 // 当total==5以后,我们指针才能往下走,
index = 0 // 当前对外提供服务的服务器地址,
total需要重新置为零,但是已经达到过一个5次,我们的index = 1
*/
private int total = 0; // 总共被调用的次数,目前要求每台被调用5次
private int currentIndex = 0; // 当前提供服务的机器号
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
if (Thread.interrupted()) {
return null;
}
List<Server> upList = lb.getReachableServers(); //当前存活的服务
List<Server> allList = lb.getAllServers(); //获取全部的服务
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
//int index = rand.nextInt(serverCount);
//server = upList.get(index);
if(total < 5)
{
server = upList.get(currentIndex);
total++;
}else {
total = 0;
currentIndex++;
if(currentIndex >= upList.size())
{
currentIndex = 0;
}
}
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
// Shouldn't actually happen.. but must be transient or a bug.
server = null;
Thread.yield();
}
return server;
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
2、配置类中增加自定义规则
@Configuration
public class ConfigBean
{
@Bean
@LoadBalanced //Ribbon 是客户端负载均衡的工具;
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
@Bean
public IRule myRule()
{
return new CustomeRule(); //自定义负载均衡规则
}
}
3、主启动类添加 @RibbonClient 注解,name和configuration参数很重要;
在启动该微服务的时候就能去加载我们自定义的Ribbon配置类,从而使配置生效:
@RibbonClient(name=“microservicecloud-dept”, configuration=ConfigBean.class)
name指定针对哪个服务 进行负载均衡,而configuration指定负载均衡的算法具体实现类。
4、测试
启动该消费者服务,然后访问:http://localhost/consumer/dept/get/1,可以看到先访问生产者1服务5次,然后访问生产者2服务5次…