Ribbon

1.Ribbon 概述

Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。通过 Spring Cloud 的封装,可以让我们轻松地将面向服务的 REST 模版请求自动转换成客户端负载均衡的服务调用。 轮询 hash 权重 ...

简单的说 Ribbon 就是 netfix 公司的一个开源项目,主要功能是提供客户端负载均衡算法和服务调用。Ribbon 客户端组件提供了一套完善的配置项,比如连接超时,重试等。

在 Spring Cloud 构建的微服务系统中, Ribbon 作为服务消费者的负载均衡器,有两种使用方式,一种是和 RestTemplate 相结合,另一种是和 OpenFeign 相结合。OpenFeign 已经默认集成了 Ribbon,Ribbon 有很多子模块,但很多模块没有用于生产环境。

RestTemplate官网说明

2.负载均衡

负载均衡,英文名称为 Load Balance(LB)http://、lb://(负载均衡协议) ,其含义就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,例如 Web 服务器、企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务。

负载均衡构建在原有网络结构之上,它提供了一种透明且廉价有效的方法扩展服务器和网络设备的带宽、加强网络数据处理能力、增加吞吐量、提高网络的可用性和灵活性。

2.1 服务器的负载均衡

3.Ribbon 快速入门

3.1 本次调用设计图

3.2 项目搭建

consumer 和 provider-1 和 provider-2 都是 eureka-client

注意这三个依赖是 eureka-client
注意 provider-1 和 provider-2 的 spring.application.name=provider
注意启动类的注解和配置文件的端口以及服务名称

3.3 创建 provider-1 和 provider-2

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.hguo</groupId>
<artifactId>ribbon-provider-1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-server</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--eureka-client eureka客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 依赖管理,cloud 的依赖-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

配置文件

server:
port: 8001 #provider-1端口8001,provider-2端口8002
spring:
application:
name: provider # 两个ribbon-provider服务都叫同一个服务名称
eureka:
client:
service-url: #eureka 服务端和客户端的交互地址
defaultZone: http://localhost:8761/eureka/
instance:
hostname: localhost
prefer-ip-address: true
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author leizi
* @create 2023-04-15 19:56
*/
@RestController
public class ProviderController {
@GetMapping("/info")
public String info() {
return "我是provider-1";
}
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author leizi
* @create 2023-04-15 19:56
*/
@RestController
public class ProviderController {
@GetMapping("/info")
public String info() {
return "我是provider-2";
}
}

3.4创建 consumer

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hguo</groupId>
<artifactId>ribbon-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ribbon-server</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<!--ribbon依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

配置文件

server:
port: 8003
spring:
application:
name: reibbon-server
eureka:
client:
service-url: #eureka 服务端和客户端的交互地址
defaultZone: http://localhost:8761/eureka/
instance:
hostname: localhost
prefer-ip-address: true
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

3.5编写RestTemplate配置类

package com.hguo.ribbonserver.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* @author leizi
* @create 2023-04-15 21:34
*/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

3.6 编写 consumer 的 TestController

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Random;
/**
* @author leizi
* @create 2023-04-15 21:37
*/
@RestController
public class TestController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
static Random random = new Random();
@RequestMapping("/testBalance")
public String testBalance(String serviceId) {
//获取服务列表
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
if (ObjectUtils.isEmpty(instances)) {
return "服务列表为空";
}
//如果服务列表不为空,先自己做一个负载均衡
ServiceInstance serviceInstance = loadBalance(instances);
String host = serviceInstance.getHost();
int port = serviceInstance.getPort();
String url = "http://" + host + ":" + port + "/info";
System.out.println("本次我调用的是" + url);
String forObject = restTemplate.getForObject(url, String.class);
System.out.println(forObject);
return forObject;
}
private ServiceInstance loadBalance(List<ServiceInstance> instances) {
//拼接 url 去调用 ip:port 先自己实现不用 ribbon
ServiceInstance serviceInstance = instances.get(random.nextInt(instances.size()));
return serviceInstance;
}
}

3.7 启动测试

首选确保都注册上去了

然后访问调用

http://localhost:8003/testBalance?serviceId=provider

3.8使用 Ribbon 改造

只需要对 consumer 改造即可,改造restTemplet配置类,改造 controller。

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* @author leizi
* @create 2023-04-15 21:34
*/
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced //ribbon 的负载均衡注解
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @author leizi
* @create 2023-04-15 21:37
*/
@RestController
public class TestController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/testRibbonBalance")
public String testRibbonBalance(String serviceId) {
// 直接用服务名称替换 ip:port
String url = "http://" + serviceId + "/info";
String forObject = restTemplate.getForObject(url, String.class);
System.out.println(forObject);
return forObject;
}
}

📢说明:

使用ribbon改造后 String url = "http://" + serviceId + "/info";,访问地址改成这个是不能直接访问的,正常的请求是 http://ip:port/info。这里将ip和port换成了serviceId服务名称是因为RestTemplate在配置类中使用了Ribbon注解被Ribbon接管,ribbon根据服务名称通过eureka服务发现找到serviceId对应的服务注册信息,如IP和端口。再重构请求的url去请求到provider。

ribbon默认使用轮询负载均衡算法。

3.9 改造后测试效果

http://localhost:8003/testRibbonBalance?serviceId=provider

4.Ribbon 源码分析

4.1 Ribbon 要做什么事情?

先通过 "http://" + serviceId + "/info" 我们思考 ribbon 在真正调用之前需要做什么?

restTemplate.getForObject("http://provider/info", String.class);

想要把上面这个请求执行成功,我们需要以下几步:

1、拦截该请求;

2、获取该请求的 URL 地址:http://provider/info

3、截取 URL 地址中的 provider

4、从服务列表中找到 key 为 provider 的服务实例的集合(服务发现)

5、根据负载均衡算法选出一个符合的实例

6、拿到该实例的 host 和 port,重构原来 URL 中的 provider

7、真正的发送 restTemplate.getForObject(“http://ip:port/info”,String.class);

4.2 Ribbon 负载均衡的测试

新增 controller

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @author leizi
* @create 2023-04-15 21:37
*/
@RestController
public class TestController {
@Autowired
private LoadBalancerClient loadBalancerClient;
@RequestMapping("/testChoose")
public String testChoose(String serviceId) {
ServiceInstance choose = loadBalancerClient.choose(serviceId);
System.out.println(choose.getHost() + ":" + choose.getPort());
return choose.toString();
}
}

访问:http://localhost:8003/testChoose?serviceId=provider

4.3 从 choose 方法入手,查看 Ribbon 负载均衡的源码

走进 getServer()方法

chooseServer()里面得到 rule 是哪个对象

发现当前的 ruleZoneAvoidanceRule 对象,而他只有一个父类 PredicateBasedRule

最终进入 PredicateBasedRule 类的 choose()方法

com.netflix.loadbalancer.AbstractServerPredicate#incrementAndGetModulo

4.4 负载均衡之前的服务列表是从何而来呢?

  • Ribbon 里面有没有服务列表?

Ribbon 只做负载均衡和远程调用

  • 服务列表从哪来?

从 eureka 获取

Ribbon 有一个核心接口 ILoadBalance(承上(eureka) 启下(Rule))
我们发现在负载均衡之前,服务列表已经有数据了。

重点接口 ILoadBalancer

Ribbon 没有服务发现的功能,但是 eureka 有,所以 ribbon 和 eureka 完美结合。

首先关注这两个集合,就是存放从 eureka 服务端拉取的服务列表然后缓存到本地

我们去看 DynamicServerListLoadBalancer 类如何获取服务列表,然后放在 ribbon 的缓存里面

ServerList<T extends Server> 实现类(DiscoveryEnabledNIWSServerList)

再回到 BaseLoadBalancer 中真正的存放服务列表

最后我们得知,只有在初始化 DynamicServerListLoadBalancer 类时,去做了服务拉取和缓存。

也就是说并不是服务一启动就拉取了服务列表缓存起来,流程图如下:

4.5 Ribbon 把 serverList 缓存起来,脏读怎么处理?

根据上面缓存服务列表我们得知,ribbon 的每个客户端都会从 eureka-server 中把服务列表缓存起来。
主要的类是 BaseLoadBalancer,那么有新的服务上线或者下线,这么保证缓存及时同步呢

Ribbon 中使用了一个 PING 机制,从 eureka 中拿到服务列表,缓存到本地,ribbon 搞了个定时任务,隔一段时间就去循环 ping一下每个服务节点是否存活。

我们查看 IPing 这个接口

我们就想看 NIWSDiscoveryPing

跟着 isAlive 一直往上找,看哪里去修改本地缓存列表

查看 notifyServerStatusChangeListener 发现只是一个空壳的接口,并没有对缓存的服务节点做出是实际操作,那么到底在哪里修改了缓存列表的值呢?我们发现在 ribbon 的配置类中 RibbonClientConfiguration 有一个更新服务列表的方法。

定时任务在哪里开始执行的呢?我们查找 doUpdate()方法

解决脏读机制的总结:

1、Ping
2、更新机制

都是为了解决脏读的现象而生的

测试发现:更新机制和 ping 有个重回,而且在 ping 的时候不能运行更新机制,在更新的时候不能运行 ping 机制,导致我们很难测到 ping 失败的现象!Ping 机制做不了事情

4.6 Ribbon 负载均衡的实现和几种算法【重点】

在 ribbon 中有一个核心的负载均衡算法接口 IRule

1.RoundRobinRule--轮询 请求次数 % 机器数量
2.RandomRule--随机
3.权重
4.iphash
5.AvailabilityFilteringRule --会先过滤掉由于多次访问故障处于断路器跳闸状态的服
务,还有并发的连接数量超过阈值的服务,然后对于剩余的服务列表按照轮询的策略进行访问
6.WeightedResponseTimeRule--根据平均响应时间计算所有服务的权重,响应时间越快服
务权重越大被选中的概率越大。刚启动时如果同统计信息不足,则使用轮询的策略,等统计信
息足够会切换到自身规则
7.RetryRule-- 先按照轮询的策略获取服务,如果获取服务失败则在指定的时间内会进行重
试,获取可用的服务
8.BestAvailableRule --会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后
选择一个并发量小的服务
9.ZoneAvoidanceRule -- 默认规则,复合判断 Server 所在区域的性能和 Server 的可用行选择服务器。

Ribbon 默认使用哪一个负载均衡算法:
ZoneAvoidanceRule :区间内亲和轮询的算法!通过一个 key 来区分

负载均衡算法:随机、轮询、权重、iphash(响应时间最短算法,区域内亲和(轮训)算法)

5.如何修改默认的负载均衡算法

5.1 修改 yml 配置文件(指定某一个服务使用什么算法)

# provider是提供者的服务名称
provider: # 配置提供者的服务名称,那么访问该服务的时候就会按照自定义的负载均衡算法
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #几种算法的全限定类名

5.2 测试调用该服务(这里使用随机规则)

5.3 配置此消费者调用任何服务都用某种算法

@Bean
public IRule myRule() {
//指定调用所有的服务都用此算法
return new RandomRule();
}

6.Ribbon 的配置文件和常用配置

Ribbon 有很多默认的配置,查看 DefaultClientConfigImpl

7.Ribbon 总结

Ribbon 是客户端实现负载均衡的远程调用组件,用法简单。

Ribbon 源码核心:
ILoadBalancer 接口:起到承上启下的作用

承上:从 eureka 拉取服务列表
启下:使用 IRule 算法实现客户端调用的负载均衡

设计思想:每一个服务提供者都有自己的 ILoadBalancer
userService —> 客户端有自己的 ILoadBalancer
TeacherService —> 客户端有自己的 ILoadBalancer

posted @   Lz_蚂蚱  阅读(34)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起