一、概述
1.1 什么是Ribbon
SpringCloud Ribbon 是基于 Netflix Ribbon 实现的一套客户端负载均衡工具。
Ribbon 是 Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon 客户端组件提供一系列完善的配置项,如连接超时、重试等,简单的说,就是在配置文件中列出 Load Balance(简称: LB) 后所有的机器,Ribbon 会自动的帮助你基于某种规则(如简单轮询、随机连接等) 去连接这些机器,我们很容易使用 Ribbon 来实现自定义的负载均衡算法。
1.2 Ribbon 官网路径
https://github.com/Netflix/ribbon/wiki/Getting-Started
Ribbon 目前也进入了维护模式。
1.3 Ribbon 用途
1.3.1 负载均衡
负载均衡(Load Balance) 就是将用户请求平均分配到多个服务上,从而达到系统的高可用(HA),常用的负载均衡软件有:Nginx、LVS、硬件F5等。
1.3.2 Ribbon 本地负载均衡客户端 和 Nginx 服务的负载均衡区别
Nginx 是服务器负载均衡,客户端所有请求都会交给 Nginx,然后有 Nginx 实现转发请求,即 Nginx 的负载均衡是在服务端实现的。
Ribbon 是本地负载均衡,在调用微服务接口的时候,会在注册中心上获取注册信息服务列表,并缓存到本地,从而在本地实现RPC远程服务调用技术。
Nginx 的负载均衡是针对服务器层面的,比如多台服务器,而 Ribbon 的负载均衡是针对具体调用微服务层面的,比如微服务集群
1.3.3 集中式负载均衡 和 进程内的负载均衡
集中式的负载均衡:即在服务的消费方和提供方之间使用独立的负载均衡设施,这些设施可以是硬件:比如 F5,也可以是软件:Nginx,由该设施负责把访问请求通过某种策略转发至服务的提供方。
进程内的负载均衡:将负载均衡逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon 就属于此类负载均衡,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务通过方的地址。
总的来说,Ribbon就是 负载均衡 + RestTemplate 调用。
二、Ribbon负载均衡演示
2.1 Ribbon 负载均衡说明
Ribbon 其实就是一个软负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用,和 Eureka 结合只是其中的一个实例。
Ribbon 在工作时分成两步:
1. 先选中 Eureka Server,它优先选择在同一个区域内负载较少的 Server;
2. 再根据用户指定的策略,在从 Server 取到的服务注册列表中选择一个地址。
其中 Ribbon 提供了多种策略:比如 轮询、随机和根据响应时间加权等
2.2 pom.xml 相关讲解
从之前文章和例子中,我们可以得知,其实订单子模块(cloud-consumer-order80) 已经实现了负载均衡,但我们在 cloud-consumer-order80 子模块的pom.xml 中并未引入 Ribbon 的相关 maven 信息,那这边的负载均衡又是哪边来的呢?
通过我们对 Eureka 引入的jar包分析可知,当前版本的 Eureka 客户端中已经集成了 Ribbon ,如下图:
从而我们可以不用在订单子模块中再次引入 Ribbon 也可以正常使用 Ribbon。
2.3 RestTemplate 中 *ForEntity 相关用法
2.3.1 官网链接
2.3.2 getForEntity 和 getForObject 区别
getForEntity: 返回对象为 ResponseEntity 对象,包含了响应中的一些重要信息,如:响应头、响应状态码、响应体等;
getForObject: 返回对象为响应体中数据转化成对象,基本可以理解为JSON对象;
2.3.3 demo 演示
执行结果:
三、Ribbon 核心组件 IRule
IRule:根据特地的算法,从服务列表中选取一个要访问的服务。
Ribbon 自带的常见负载均衡算法:
序号 | 算法实现 | 说明 |
1 | com.netflix.loadbalancer.RoundRobinRule | 轮询【默认算法】 |
2 | com.netflix.loadbalancer.RandomRule | 随机 |
3 | com.netflix.loadbalancer.RetryRule | 先按照 RoundRobinRule 的策略获取服务,如果获取服务失败,则在指定的时间内重试,获取可用的服务 |
4 | WeightedResponseTimeRule | 对 RoundRobinRule 的扩展,响应速度越快的实例选择权重就越大,越容易被选择 |
5 | BestAvailableRule | 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。 |
6 | AvailabilityFilteringRule | 先过滤掉故障实例,再选择并发较小的实例。 |
7 | ZoneAvoidanceRule | 复合判断 Server 所在区域的性能和 Server 的可用性选择服务器 |
四、替换Ribbon 默认的负载均衡规则
Ribbon 的默认负载均衡规则是轮询算法,我们可以通过新建配置类修改负载均衡规则。因为 Ribbon 是客户端负载均衡工具,所以我们本次修改需要操作的是 order-consumer-order80 子模块。
1. 新建自定义负载均衡规则类 MySelfRule.java
根据 Ribbon 官网信息,我们 Ribbon 的自定义配置类不能放在 @ComponentScan 所能扫描到的当前包及子包下,即 @SpringBootApplication 注解所在的包及子包下,所以我们这次新建的负载均衡自定义配置类 MySelfRule.java 放在 com.atguigu.ribbonselfrule 下。
package com.atguigu.ribbonselfrule;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @auther He
* @date 2022-05-24 20:07
*/
@Configuration
public class MySelfRule {
@Bean
public IRule myCustomRule(){
return new RandomRule(); //定义为随机算法
}
}
特别注意:
新建的包名和MySelfRule.java 中的方法名不能为 myrule,否则调用接口时会报错。
2. 修改启动类 OrderMain80.java
修改order-consumer-order80 订单子模块的启动类 OrderMain80.java 增加 @RibbonClient 注解
package com.atguigu.springcloud;
import com.atguigu.ribbonselfrule.MySelfRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
/**
* @auther He
* @date 2022-04-26 22:10
*/
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
3. 重启测试
重启后,访问接口 http://localhost/consumer/payment/get/1 ,注意观察 返回内容中的端口号
因为我们现在在当前接口中增加返回当前访问的端口信息,所以在返回信息中会显示对于的端口信息,通过观察可以发下,原来Ribbon 负载均衡算法为轮询时,返回的端口号为 8001和8002 交替出现,而经过我们上述修改,返回的端口号已经变成 8001 和 8002 随机返回了,也就代表是随便访问其中的一个端口。
五、负载均衡轮询算法原理
六、模拟自定义轮询算法
为加深对负载均衡算法的理解,跟着源码,模拟自定义实现轮询算法,仅为学习,非实际生产应用。
6.1 修改支付子模块中的controller
分别修改支付子模块 (cloud-provider-payment8001 和 cloud-provider-payment8002) 中的 PaymentController.java,增加如下请求接口,作为修改完成后,校验是否修改成功的标记。
/**
* 获取当前端口
* @return
*/
@GetMapping("/payment/lb")
public String getPaymentLB() {
return serverPort;
}
6.2 修改 订单模块启动类 OrderMain80.java
注释原先增加的 @RibbonClient 注解,结果如下:
package com.atguigu.springcloud;
import com.atguigu.ribbonselfrule.MySelfRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
/**
* @auther He
* @date 2022-04-26 22:10
*/
@SpringBootApplication
@EnableEurekaClient
//@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
6.3 修改 订单模块(cloud-consumer-order80)中RestTemplate 配置类 ApplicationContextBean.java
修改 订单模块(cloud-consumer-order80)中RestTemplate 配置类 ApplicationContextBean.java,去除 @LoadBalanced 注解,让修改完后,订单模块中的负载均衡算法只能由我们自定义的轮询算法实现。
package com.atguigu.springcloud.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
/**
* @auther He
* @date 2022-04-26 22:19
*/
@Component
public class ApplicationContextConfig {
@Bean
// @LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
6.4 新增我们自定义的负载均衡算法 LoadBalancer 接口
package com.atguigu.springcloud.lb;
import org.springframework.cloud.client.ServiceInstance;
/**
* @auther He
* @date 2022-05-26 22:37
*/
public interface LoadBalancer {
/**
* 获取 Eureka 中的一个服务对象
* @return
*/
public ServiceInstance getInstance();
}
6.5 增加上述自定义负载均衡接口的实现类 MyLB
package com.atguigu.springcloud.lb.impl;
import com.atguigu.springcloud.lb.LoadBalancer;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @auther He
* @date 2022-05-26 22:41
*/
@Component
public class MyLB implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
@Resource
private DiscoveryClient discoveryClient;
/**
* 获取 Eureka 中的一个服务对象
*
* @return
*/
@Override
public ServiceInstance getInstance() {
List<ServiceInstance> instanceList = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if (null == instanceList || instanceList.size() == 0) {
return null;
}
//将获得的取模后获得返回服务的下标
int index = this.getAndIncrement() % instanceList.size();
return instanceList.get(index); //根据返回的服务下标获得对应的服务实例
}
/**
* CAS(自旋锁)保证原子性,获得下一个访问的请求数
* @return
*/
public final int getAndIncrement() {
int current, next;
do {
current = this.atomicInteger.get(); //获取当前初始值
next = current > Integer.MAX_VALUE ? 0 : current + 1; //防止累加溢出
} while (!this.atomicInteger.compareAndSet(current, next));
System.out.println(">>> 第几次访问, 次数next: " + next);
return next;
}
}
核心在于:getAndIncrement() 方法,通过自旋锁保证操作原子性,并获得下一个访问的请求数。
6.5 修改 订单模块 中的 OrderController,增加测试自定义负载均衡的的请求接口
/**
* 测试自定义负载均衡算法
* @return
*/
@GetMapping(value = "/consumer/payment/lb")
public String getPaymentLB() {
ServiceInstance serviceInstance = loadBalancer.getInstance();
if (null == serviceInstance) {
return "未找到有效的服务地址";
} else {
URI url = serviceInstance.getUri();
return restTemplate.getForObject(url + "/payment/lb", String.class);
}
}
6.6 重启服务,并测试
重启完后,循环访问 http://localhost/consumer/payment/lb ,若修改成功,会依次返回端口号 8001、8002
------------------------------------------------------ 我是分割线 ------------------------------------------------------
至此,Ribbon 的基础学习终于结束了,最近事情又杂又多,烦得很,没什么时间也静不下心来学习,只能尽量抽空逼自己坐下来学习,Ribbon 的学习时间大大超出了我先前的预期,希望后面能尽量避免这种情况发生。
加油!
让我们一起学习,一起成长,一起飞