作者:mobiledu2502858787 | 来源:互联网 | 2023-06-13 07:58
1.概述SpringCloudRibbon是基于NetflixRibbon实现的一套客户端负载均衡工具。Ribbon客户端组件提供了一系列完善的配置项,如连接超时&
1.概述
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡工具。Ribbon客户端组件提供了一系列完善的配置项,如连接超时,重试等。简单地说,就是在配置文件中列出Load Balancer后面所有的机器,Ribbon会自动帮我们基于某种规则(如轮询,随机连接等)去连接这些机器,我们很容易使用Ribbon实现自定义的负载均衡算法。
Ribbon官网:https://github.com/Netflix/ribbon,根据官网信息,可以看到Ribbon现在进入了维护模式,替换方案是Loadbalancer。
负载均衡的表现就是,将用户的请求分摊到多个服务器上,从而达到高可用的目的。常见的负载均衡软件有:Nginx、LVS、硬件F5等。
Nginx是服务器端负载均衡,客户端的请求都发给Nginx,Nginx实现分发,将请求发送到不同的服务器上。
Ribbon是客户端负载均衡,在调用微服务接口的时候,会在注册中心拿到注册信息服务列表缓存到本地JVM,在客户端通过某种规则,确定请求的链接,发送请求进行调用。
集中式LoadBalancer:在服务的消费方和提供方之间使用的独立LoadBalancer设备(可以是硬件,如F5,也可以是软件,如Nginx),由该设施负责把请求通过某种策略转发至服务的提供方。
进程内LoadBalancer:将LoadBalancer集成到消费方,消费者从服务注册中心获取到可用地址,自己再从这些地址中,选择一个作为要访问的服务器。Ribbon就属于进程内LoadBalancer,它只是一个类库,集成消费者进程,消费者通过它获取服务提供方的地址。
2.Ribbon负载均衡演示
之前,我们好像并没有加入Ribbon的依赖,也实现了负载均衡,其实在spring-cloud-starter-netflix-eureka-client坐标下,是引入了spring-cloud-starter-netflix-ribbon的,所以,我们仅仅只需要添加一个@LoadBalanced就可以实现负载均衡。
RestTemplate常见的方法有getForObject()、getForEntity()、postForObject()、postForEntity()方法。其中*ForObject()方法返回对象为响应体中数据转换成的对象,基本理解为JSON。*ForEntity()方法返回对象是ResponseEntity对象,包含了响应中的信息,比如响应头,响应状态码,响应体等。
将cloud-eureka-server7001、cloud-eureka-server7002的配置文件改成集群模式,让7001和7002互相注册,将cloud-provider-payment8001、cloud-provider-payment8002的配置文件改成集群模式,将cloud-consumer-order80的配置文件改成集群模式。在cloud-consumer-order80模块中的OrderController里,添加两个方法,分别调用getForEntity()和postForEntity()方法。
@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult getPaymentById2(@PathVariable("id") Long id) {ResponseEntity entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);System.out.println("status code=" + entity.getStatusCode());System.out.println("headers=" + entity.getHeaders());if (entity.getStatusCode().is2xxSuccessful()) {return entity.getBody();} else {return new CommonResult(404, "查找失败");}
}
@GetMapping("/consumer/payment/create2")
public CommonResult create2(Payment payment) {return restTemplate.postForEntity(PAYMENT_URL + "/payment/create", payment, CommonResult.class).getBody();
}
先启动Eureka注册中心,再启动两个生产者,最后启动消费者,浏览器发送请求来调用*ForEntity()方法进行测试。在浏览器端,可以看到port的值,不断在8001和8002之前进行切换。
3.Ribbon核心组件IRule
IRule是一个接口,它有多个实现类,分别代表不同的负载均衡策略。使用IDEA生成一下它的子类的关系图(点击IRule,按下Ctrl+Alt+B获取所有子类,按下Ctrl+A全选子类,按下Ctrl+Alt+U,选择Java Class Diagrams)。
- com.netflix.loadbalancer.RoundRobinRule:轮询
- com.netflix.loadbalancer.RandomRule:随机
- com.netflix.loadbalancer.RetryRule:先按照RoundRobinRule策略获取服务,如果获取服务失败,在指定时间内重试,获取可用服务
- com.netflix.loadbalancer.WeigthResponseTimeRule:对RoundRobinRule扩展,根据响应速度进行选择,响应速度越快,优先级越高
- com.netflix.loadbalancer.BestAvailableRule:先过滤掉多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
- com.netflix.loadbalancer.AvailabilityFilterRule:先过滤掉故障实例,再选择并发最小的实例
- com.netflix.loadbalancer.ZoneAvoidanceRule:默认规则,符合判断server所在区域的性能和server可用性选择服务器
在这里提到,自定义Ribbon Client的时候,我们不能将自定义的配置类放在@ComponentScan所扫描的包及其子包下,否则我们自定义的配置类会被所有Ribbon Client所共享,达不到特殊化定制的目的了。
新建MyRule配置类,注意包名,这里我放在com.atguigu.rule包下,假设我改成了RandomRule。
package com.atguigu.rule;import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MyRule {@Beanpublic IRule iRule() {return new RandomRule();}
}
告诉主启动类,使用的哪一个Ribbon Client。
package com.atguigu.springcloud;import com.atguigu.rule.MyRule;
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;@SpringBootApplication
@EnableEurekaClient
// 自定义Ribbon规则适用于哪个服务(服务名要大写),自定义Ribbon规则对应的类
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", cOnfiguration= MyRule.class)
public class OrderMain80 {public static void main(String[] args) {SpringApplication.run(OrderMain80.class, args);}
}
通过浏览器进行访问,查看浏览器给出的端口信息,发现自定义的随机规则生效了。
4.Ribbon负载均衡算法
负载均衡算法:REST接口第几次请求数字%服务器集群总数量=实际调用服务器位置下标,每次重启服务器REST接口计数从1开始。找一个IRule接口的实现类,这里选择RoundRobinRule类,查看它的实现。
public Server choose(ILoadBalancer lb, Object key) {if (lb == null) {log.warn("no load balancer");return null;} else {Server server = null;int count = 0;while(true) {// 获取server,如果获取失败,进行重试,如果10次后,还没获取到server,就报错if (server == null && count++ <10) {// 获取所有状态是up的serverList reachableServers = lb.getReachableServers();// 获取所有serverList allServers = lb.getAllServers();int upCount = reachableServers.size();int serverCount = allServers.size();if (upCount != 0 && serverCount != 0) {// 计算出要访问的server下标int nextServerIndex = this.incrementAndGetModulo(serverCount);server = (Server)allServers.get(nextServerIndex);if (server == null) {Thread.yield();} else {if (server.isAlive() && server.isReadyToServe()) {return server;}server = null;}continue;}log.warn("No up servers available from load balancer: " + lb);return null;}if (count >= 10) {log.warn("No available alive servers after 10 tries from load balancer: " + lb);}return server;}}
}private int incrementAndGetModulo(int modulo) {int current;int next;// 利用cas和自旋锁,获取next的值do {current = this.nextServerCyclicCounter.get();next = (current + 1) % modulo;} while(!this.nextServerCyclicCounter.compareAndSet(current, next));return next;
}
下面手写一个本地的负载均衡。
cloud-eureka-server7001和cloud-eureka-server7002集群方式启动。
在cloud-provider-payment8001和cloud-provider-payment8002的controller里,加入如下方法,用于返回访问接口。
@GetMapping("/payment/loadbalance")
public String getLoadBalancePort() {return serverPort;
}
修改cloud-consumer-order80模块。
将配置类中的@LoadBalanced注解去掉,添加LoadBalancer.java接口和MyLoadBalancer.java实现类,同样,在MyLoadBalancer.java里用到cas和自旋锁。
package com.atguigu.springcloud.balance;import org.springframework.cloud.client.ServiceInstance;import java.util.List;public interface LoadBalancer {ServiceInstance instances(List serviceInstanceList);
}
package com.atguigu.springcloud.balance;import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;@Component
public class MyLoadBalancer implements LoadBalancer {private AtomicInteger atomicInteger = new AtomicInteger(0);@Overridepublic ServiceInstance instances(List serviceInstanceList) {int index = getAndIncrement() % serviceInstanceList.size();return serviceInstanceList.get(index);}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;}
}
在OrderController.java里注入MyLoadBalancer实例和DiscoveryClient实例。添加一个请求方法,输出负载均衡后需要访问的端口号,测试负载均衡效果是否有效。
@GetMapping("/consumer/payment/loadbalance")
public String getPaymentLoadBalance() {// 通过discoveryClient,使用服务提供者对应的应用名称大写获取所有服务实例List instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");if (instances == null || instances.size() == 0) {return null;}// instances()方法拿到所有的服务实例,使用访问次数%服务实例数量求得目标服务的下标,返回下标对应的服务实例ServiceInstance instance = myLoadBalancer.instances(instances);URI uri = instance.getUri();// 获取这个实例的uri// /payment/loadbalance请求对应服务提供者controller中新加的映射方法,返回当前服务提供者的serverPort的值return restTemplate.getForObject(uri + "/payment/loadbalance", String.class);
}
启动Eureka注册中心集群,启动服务提供者,启动服务消费者,通过浏览器访问http://localhost/consumer/payment/loadbalance,可以看到端口号的轮询变化,此时我们自己的负载均衡轮询策略生效。