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 有很多子模块,但很多模块没有用于生产环境。
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
是哪个对象
发现当前的 rule
是 ZoneAvoidanceRule
对象,而他只有一个父类 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
本文来自博客园,作者:Lz_蚂蚱,转载请注明原文链接:https://www.cnblogs.com/leizia/p/17327854.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步