一、负载均衡概述
1、Ribbon 简介
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。
2、负载均衡简介
LB,即负载均衡(Load Balance),在微服务或分布式集群中经常用的一种应用。负载均衡简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA。常见的负载均衡有软件Nginx,LVS,硬件 F5等。相应的在中间件,例如:dubbo和SpringCloud中均给我们提供了负载均衡,SpringCloud的负载均衡算法可以自定义。
(1)集中式LB:
在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5, 也可以是软件,如nginx), 该设施负责把访问请求通过某种策略转发至服务的提供方;
(2)进程内LB:
将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
二、Ribbon配置初步
1、修改微服务 microservicecloud-consumer-dept-80
pom.xml 添加 Ribbon 相关依赖
<!-- Ribbon相关 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
application.yml 添加 eureka 的服务注册地址
eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
创建 RestTemplate 时添加 @LoadBalanced 注解,第12行为新增的
1 package com.atguigu.springcloud.cfgbeans; 2 3 import org.springframework.cloud.client.loadbalancer.LoadBalanced; 4 import org.springframework.context.annotation.Bean; 5 import org.springframework.context.annotation.Configuration; 6 import org.springframework.web.client.RestTemplate; 7 8 @Configuration 9 public class ConfigBean 10 { 11 @Bean 12 @LoadBalanced 13 public RestTemplate getRestTemplate() 14 { 15 return new RestTemplate(); 16 } 17 }
80微服务启动类 DeptConsumer80_App.java 添加 @EnableEurekaClient 注解(第 8 行)
1 package com.atguigu.springcloud; 2 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 import org.springframework.cloud.netflix.eureka.EnableEurekaClient; 6 7 @SpringBootApplication 8 @EnableEurekaClient 9 public class DeptConsumer80_App 10 { 11 public static void main(String[] args) 12 { 13 SpringApplication.run(DeptConsumer80_App.class, args); 14 } 15 }
修改DeptController_Consumer客户端访问类,将 http://localhost:8001 换成微服务名 MICROSERVICECLOUD-DEPT
//private static final String REST_URL_PREFIX = "http://localhost:8001"; private static final String REST_URL_PREFIX = "http://MICROSERVICECLOUD-DEPT";
依次启动 7001、7002、7003、8001、80 并验证下列地址均能正常访问。
http://localhost/consumer/dept/get/1
http://localhost/consumer/dept/list
http://localhost/consumer/dept/add?dname=大数据部
注意:Ribbon和Eureka整合后Consumer可以直接调用服务而不用再关心地址和端口号
三、Ribbon负载均衡
1、架构说明及工作步骤
Ribbon在工作时分成两步:
(1)先选择 EurekaServer ,它优先选择在同一个区域内负载较少的server.
(2)再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。
其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。
2、参考微服务8001新建微服务8002、8003
8001、8002、8003连各自的数据库,创建数据库的脚本如下:

DROP DATABASE IF EXISTS cloudDB02; CREATE DATABASE cloudDB02 CHARACTER SET UTF8; USE cloudDB02; CREATE TABLE dept ( deptno BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT, dname VARCHAR(60), db_source VARCHAR(60) ); INSERT INTO dept(dname,db_source) VALUES('开发部',DATABASE()); INSERT INTO dept(dname,db_source) VALUES('人事部',DATABASE()); INSERT INTO dept(dname,db_source) VALUES('财务部',DATABASE()); INSERT INTO dept(dname,db_source) VALUES('市场部',DATABASE()); INSERT INTO dept(dname,db_source) VALUES('运维部',DATABASE()); SELECT * FROM dept;

DROP DATABASE IF EXISTS cloudDB03; CREATE DATABASE cloudDB03 CHARACTER SET UTF8; USE cloudDB03; CREATE TABLE dept ( deptno BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT, dname VARCHAR(60), db_source VARCHAR(60) ); INSERT INTO dept(dname,db_source) VALUES('开发部',DATABASE()); INSERT INTO dept(dname,db_source) VALUES('人事部',DATABASE()); INSERT INTO dept(dname,db_source) VALUES('财务部',DATABASE()); INSERT INTO dept(dname,db_source) VALUES('市场部',DATABASE()); INSERT INTO dept(dname,db_source) VALUES('运维部',DATABASE()); SELECT * FROM dept;
8001、8002、8003不同的地方:pom.xml中artifactId;application.yml中port、datasource.url、instance-id;启动类名称。注意微服务名一样。
3、依次启动eureka集群 7001、7002、7003 和 服务实例 8001、8002、8003,并验证,以下可以正常访问
http://localhost:8001/dept/list
http://localhost:8002/dept/list
http://localhost:8003/dept/list
4、启动80微服务,输入:http://localhost/consumer/dept/list 测试,每次访问的是不同的服务实例,依次轮询实现负载均衡
第一次
第二次
第三次
... ...
总结:Ribbon其实就是一个软负载均衡的客户端组件,他可以和其他所需请求的客户端结合使用,和eureka结合只是其中的一个实例。
四、Ribbon 负载均衡自定义策略
1、Ribbon 默认的7种负载策略
Ribbon核心组件是IRule,根据特定算法从服务列表中选取一个要访问的服务,Ribbon提供了7种负载策略
(1)RoundRobinRule,轮询(默认的负载策略)
(2)RandomRule,随机
(3)AvailabilityFilteringRule,会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进访问
(4)WeightedResponseTimeRule,根据平均响应时间计算所有服务的权重,响应时间越快,服务权重越大,被选中的概率越高。刚启动时如果统计信息不足,则使用 RoundRobinRule策略,等统计信息足够多,会切换到WeightedResponseTimeRule
(5)RetryRule,先按照RoundRobinRule的策略获取服务,如果获取服务失败,则在指定的时间内进行重试,获取可用的服务
(6)BestAvailableRule,会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
(7)ZoneAvoidanceRule,复合判断server所在区域的性能和server的可用性选择服务器
2、测试微服务的其他负载均衡策略
使用 Ribbon 提供的其他负载均衡策略,只需要修改80微服务的ConfigBean.java类,创建合适的IRule实例即可。
输入地址 http://localhost/consumer/dept/get/1 分别测试下面两种随机策略。
(1)测试随机策略(测试成功)
@Bean public IRule myRule(){ return new RandomRule();//随机算法 }
(2)测试RetryRule策略(测试失败,测试了几十次(或许次数不够?)依然会访问实例A,页面报 “Whitelabel Error Page” )
@Bean public IRule myRule(){ return new RetryRule();//所有的实例都正常就是轮询算法,假如A实例宕掉了,访问几次A实例报错后,就不会访问A实例了 }
3、自定义负载均衡策略
现在实现一个这样的负载均衡:依旧轮询策略,但是加上新需求,每个服务器要求被调用5次。也即以前是每台机器一次,现在是每台机器5次。
(1)修改80微服务 在启动类DeptConsumer80_App.java上添加注解
@RibbonClient(name="MICROSERVICECLOUD-DEPT",configuration=MySelfRule.class)
该注解的意思是:在启动 MICROSERVICECLOUD-DEPT(必须是大写) 微服务的时候去加载自定义的Ribbon配置类 MySelfRule,由它实现负载均衡。
(2)新建类 com.atguigu.myrule.MySelfRule
package com.atguigu.myrule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.netflix.loadbalancer.IRule; import com.netflix.loadbalancer.RandomRule; @Configuration public class MySelfRule{ @Bean public IRule myRule(){ return new RandomRule();//随机 } }
注:官方文档明确给出了警告,这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,也就是说我们达不到特殊化定制的目的了。
测试,此时启动微服务,发现已经采用了随机的策略了,说明配置是正确的,只需要创建一个自己的类替换掉 RandomRule 即可
(3)创建访问策略类 RandomRule_ZY

1 package com.atguigu.myrule; 2 3 import java.util.List; 4 import java.util.concurrent.ThreadLocalRandom; 5 6 import com.netflix.client.config.IClientConfig; 7 import com.netflix.loadbalancer.AbstractLoadBalancerRule; 8 import com.netflix.loadbalancer.ILoadBalancer; 9 import com.netflix.loadbalancer.Server; 10 11 public class RandomRule_ZY extends AbstractLoadBalancerRule { 12 13 private int total = 0;//总共被调用的次数,目前要求每台被调用的5次 14 private int currentIndex = 0;//当前提供服务的机器号 15 16 public Server choose(ILoadBalancer lb, Object key) { 17 if (lb == null) { 18 return null; 19 } 20 Server server = null; 21 22 while (server == null) { 23 if (Thread.interrupted()) { 24 return null; 25 } 26 List<Server> upList = lb.getReachableServers(); 27 List<Server> allList = lb.getAllServers(); 28 29 int serverCount = allList.size(); 30 if (serverCount == 0) { 31 /* 32 * No servers. End regardless of pass, because subsequent passes 33 * only get more restrictive. 34 */ 35 return null; 36 } 37 38 if(total < 5){ 39 server = upList.get(currentIndex); 40 total ++; 41 }else{ 42 total = 0; 43 currentIndex++; 44 if(currentIndex >= upList.size()){ 45 currentIndex = 0; 46 } 47 } 48 49 if (server == null) { 50 /* 51 * The only time this should happen is if the server list were 52 * somehow trimmed. This is a transient condition. Retry after 53 * yielding. 54 */ 55 Thread.yield(); 56 continue; 57 } 58 59 if (server.isAlive()) { 60 return (server); 61 } 62 63 // Shouldn't actually happen.. but must be transient or a bug. 64 server = null; 65 Thread.yield(); 66 } 67 68 return server; 69 70 } 71 72 protected int chooseRandomInt(int serverCount) { 73 return ThreadLocalRandom.current().nextInt(serverCount); 74 } 75 76 @Override 77 public Server choose(Object key) { 78 return choose(getLoadBalancer(), key); 79 } 80 81 @Override 82 public void initWithNiwsConfig(IClientConfig clientConfig) { 83 // TODO Auto-generated method stub 84 } 85 }
注意第38-47行是添加的逻辑
启动微服务访问 http://localhost/consumer/dept/get/1 测试修改的策略已经成功,大功告成。