简介
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端 、负载均衡的工具。主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。
初步配置
前面一直强调,Ribbon 是基于客户端的负载均衡,所以们需要对消费者端做手脚,而不是服务提供端。在之前的博客上已经配置好了消费者服务,现在对其进行更改。
第一步
在消费者的pom.xml中添加相关依赖,因为Ribbon要和Eureka整合才能发挥强大的功能,所以在引入 Ribbon 依赖的同时,也要引入 Eureka 的依赖。此时这个消费者,也是一个 Eureka 的客户端了,但是它只需要消费,不需要注册到注册中心。
<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>
第二步
修改消费者微服务的 application.properties
配置文件,需要加上注册中心的地址。现在消费者不直接去调用服务提供者了,而是去注册中心找注册了的服务,再通过路径匹配,Ribbon转发到相应的请求,最终返回数据。Eureka集群已经在之前的博客中配好了。
配置如下:
server.port=80
# 消费者消费即可,不用注册到注册中心
eureka.client.register-with-eureka=false
# 注册中心地址
eureka.client.service-url.defaultZone=http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka
第三步
集成了Ribbon之后,我们访问微服务,可以不再去关心 ip + port了,我们可以直接通过微服务名称去调用。之前在建立消费者模块的时候,采用的是 RestTemplate
来实现不同微服务之间的调用,我们需要做如下设置:
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
在注入Bean的方法上加上 @LoadBalanced
注解。随后在消费者的controller中,将原来的URL替换成微服务的名称。这个注解也是开启负载均衡效果的注解,下面再说。
// private static final String REST_URL = "http://localhost:8001";之前的
// 使用Ribbon之后,可以直接通过微服务的名称进行访问
private static final String REST_URL = "http://SPRINGCLOUDSERVICE-USER";
因为消费者也集成了 eureka-client,消费者也是一个eureka的客户端。所以要在主启动类上加上 @EnableEurekaClient
注解。
@SpringBootApplication
@EnableEurekaClient
public class UserConsumer80_App {
public static void main(String[] args) {
SpringApplication.run(UserConsumer80_App.class, args);
}
}
第四步
启动eureka集群,启动服务提供者,启动消费者服务。进行测试。
访问 http://localhost/user/get 路径,成功得到数据。
Ribbon和Eureka整合后Consumer可以直接调用服务而不用再关心地址和端口号。
负载均衡
在初步配置中,实现了直接通过微服务的名字来访问服务的微服务的提供者,而不用再去关心提供者的 ip + port,这也是整合Ribbon带来的一个好处。接下来要将负载均衡的效果体现出来。那么此时一个provider就不行了,因为只有一个,无论多少请求过来,都会找到这个 provider 服务。所以再创建两个provider微服务。
创建两个微服务 springcloudservice-provider-user-8002、springcloudservice-provider-user-8003,provider的搭建在之前的博客搭建好了,这两个微服务的配置文件大致与 springcloudservice-provider-user-8001 相同,可以拷贝过来进行一定的修改。
第一步
配置 pom.xml 文件。
springcloudservice-provider-user-8002 & springcloudservice-provider-user-8003的 pom.xml 如下:
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>springcloudservice-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<!--使用内嵌的jetty-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<!-- actuator监控信息完善 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
第二步
为每个微服务都创建一个对应的数据库。
以前的单体架构可能一个项目就是一个数据库,里面多张表。而现在微服务架构,每个微服务都可以拥有自己独立的数据库。
建库建表语句
CREATE DATABASE cloudDB02;
USE cloudDB02;
CREATE TABLE `user`(
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(5),
`db_source` VARCHAR(20)
);
INSERT INTO `user`(`name`,`db_source`) VALUES('张三',DATABASE());
INSERT INTO `user`(`name`,`db_source`) VALUES('李四',DATABASE());
INSERT INTO `user`(`name`,`db_source`) VALUES('王五',DATABASE());
INSERT INTO `user`(`name`,`db_source`) VALUES('赵六',DATABASE());
INSERT INTO `user`(`name`,`db_source`) VALUES('老七',DATABASE());
SELECT * FROM `user`;
第三步
修改各自的 application.properties 配置文件
server.port=8002
spring.datasource.url=jdbc:mysql://120.77.41.74/cloudDB01?characterEncoding=utf8&useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
mybatis.config-location=classpath:/mybatis/mybatis.cfg.xml
mybatis.type-aliases-package=com.zxb.springcloud.bean
mybatis.mapper-locations=classpath:/mybatis/mapper/*.xml
spring.application.name=springcloudservice-user
eureka.client.service-url.defaultZone=http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka
eureka.instance.instance-id=springcloudservice-user
eureka.instance.prefer-ip-address=true
info.app.name=springcloudservice2
info.company.name=www.zxb.com
info.build.artifactId=${project.artifactId}
info.build.version=${project.version}
第四步
测试
启动 Eureka-Server 集群,然后再启动刚刚配置的两个微服务,访问接口,看是否连通。
8002正常:
8003正常
第五步
测试负载均衡功能
上面提到过,Ribbon 是基于 客户端 的负载均衡工具,所以要将消费者微服务和Ribbon整合。消费者去调用服务提供者的时候,使用了 RestTemplate
来进行微服务之间的调用。而要实现负载均衡功能,要在创建 RestTemplate 的方法上加上 @LoadBalance
注解。有了这个注解,调用微服务可以直接通过服务名来访问,并且实现了负载均衡的效果。
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
配置完毕后,启动 Eureka Server 集群。启动三个服务提供者,启动消费者,进行最终的测试。
先来到注册中心页面:
此时相当于我们的一个微服务,有三个实例。
接下来测试消费者来调用接口,因为消费者端配置了负载均衡,看看效果。
一直发送同一个请求:
第一次发送
第二次发送
第三次发送
第四次发送结果又变成 cloudDB01。这就是 Ribbon 提供的负载均衡效果。
我们发送请求,由于我们是带了客户端的负载均衡, @LoadBalance
,所以会去使用默认的轮询算法去访问 provider 微服务。SpringCloud帮我们做到了一个注解就实现了负载均衡的效果,这比 Nginx 的配置文件写起来方便多了。
修改轮询算法
切换Ribbon自带的轮询算法
之前负载均衡采用的是默认的轮询算法,是Ribbon出厂自带的,而Ribbon出厂自带了7中算法。
RoundRobinRule | 轮询 |
---|---|
RandomRule | 随机 |
AvailabilityFilteringRule | 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问 |
WeightedResponseTimeRule | 根据平均响应时间计算所有服务的权重,响应时间越快服务权重越大被选中的概率越高。刚启动时如果统计信息不足,则使用RoundRobinRule策略,等统计信息足够,会切换到WeightedResponseTimeRule |
RetryRule | 先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务 |
BestAvailableRule | 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务 |
ZoneAvoidanceRule | 默认规则,复合判断server所在区域的性能和server的可用性选择服务器 |
而我们要切换轮询算法,需要用到 Ribbon 中的核心组件 IRule
。
IRule 是一个接口。 位于com.netflix.loadbalancer包下。
在我这个版本中,该接口中共有三个方法:
public interface IRule{
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
IRule 接口根据特定算法中从服务列表中选取一个要访问的服务。这个接口中,有一个getLoadBalancer()方法,返回是一个ILoadBalancer
实现类。而这个ILoadBalancer
,就是定义软件负载均衡操作的接口,它有一个实现类就是 AbstractLoadBalancerRule
,这七大负载均衡算法接继承了 AbstractLoadBalancerRule
类。
想要切换Ribbon的负载均衡算法,可以写一个配置类,创建一个 IRule 类型的bean,返回对应的轮询算法实现类即可。
@Bean
public IRule myRule() {
return new RandomRule(); // 使用我随机选择算法替换默认的轮询算法
}
此时重启消费者服务,然后访问同一接口,发现就是随机的了,不再是按照轮询的方式。
自定义轮询算法
如果不想使用Ribbon出厂自带的负载均衡算法,我们也可以自定义轮询算法。自定义轮询算法,必须将我们写的类去继承 AbstractLoadBalancerRule
抽象类。
假设现在有一个需求:依旧是轮询策略,但是加上新需求,每个服务器要求被调用五次。也即以前是轮询时每台机子一次,现在改为每台机子五次五次的轮询。就像是每人值日一天轮着来,现在是每人值日一周,轮着来。
那么我们就可以先去查看下源码,去学习借鉴下。以下是随机负载均衡的算法:
package com.netflix.loadbalancer;
import java.util.List;
import java.util.Random;
import com.netflix.client.config.IClientConfig;
public class RandomRule extends AbstractLoadBalancerRule {
Random rand;
public RandomRule() {
rand = new Random();
}
/**
* Randomly choose from all living servers
*/
@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE")
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 (server == null) {
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
server = null;
Thread.yield();
}
return server;
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
可以看到,真正起作用的,还是 choose 方法,那么我们就可以将源码复制过来,再按照我们的业务逻辑进行修改。
代码如下:
package com.zxb.myrule;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.List;
import java.util.Random;
/**
* 我自定义的负载均衡算法,需求就是每台机子轮流调用五次
*/
public class RandomRule_ZXB extends AbstractLoadBalancerRule {
// 当 total到5时,需要重新置为0,达到过一次5次,index就要加一,轮到下一个服务
private int total = 0; // 总共被调用的次数,要求每台被调用 5 次
private int currentIndex = 0; // 当前提供服务的机器号
// ILoadBalancer 表示负载均衡算法
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(); // 微服务所有的实例(包括down的)
int serverCount = allList.size();
if (serverCount == 0) { // 判断个数
return null;
}
// 算法逻辑
if (total < 5) {
server = upList.get(currentIndex);
total++;
} else {
total = 0;
currentIndex++;
if (currentIndex >= upList.size()) { // 注意是 >=
currentIndex = 0;
}
// server = upList.get(currentIndex); // 不用写这个,因为是 while 循环,
// 如果 server == null 成立,仍会进入if 判断
}
if (server == null) {
/*
* The only time this should happen is if the server list were
* somehow trimmed. This is a transient condition. Retry after
* yielding.
*/
Thread.yield(); // 休息一会
continue; // 继续
}
if (server.isAlive()) {
return (server);
}
server = null;
Thread.yield();
}
return server;
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
// TODO Auto-generated method stub
}
}
自定义负载均衡算法完成后,要将我们自定义的类加入到IOC容器当中,但是我们写的类,不能和启动类放在同一目录下!
官方文档也给出了警告:
所以目录结构应该这样:
然后在配置类中将我们写的自定义类加入到IOC容器当中。
@Configuration
public class RuleConfig {
@Bean
public RandomRule_ZXB myRule() {
return new RandomRule_ZXB();
}
}
然后,在消费者的主启动类上加上 @RibbonClient
注解,并给出提供者的微服务名以及配置类。
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "SPRINGCLOUDSERVICE-USER",configuration = {RuleConfig.class})
public class UserConsumer80_App {
public static void main(String[] args) {
SpringApplication.run(UserConsumer80_App.class, args);
}
}
而只有加了 @RibbonClient
注解,那么在启动该微服务的时候,就能去加载我们自定义的Ribbon配置类,从而使配置类生效。
这样我们去访问接口,就会按照我们的算法来了。