SpringCloud入门(七) ——Ribbon负载均衡服务调用

    技术2022-07-11  86

    目录

    概述LB(负载均衡)Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别集中式LB进程内LBNginx负载均衡和Ribbon负载均衡的区别? 架构说明使用Ribbon实现负载均衡RestTemplate的使用 Ribbon核心组件IRule_负载均衡策略Ribbon自带的实现如何替换负载均衡策略配置类:主启动类添加@RibbonClient测试 Ribbon负载均衡算法轮询原理轮询RoundRobinRule源码分析自定义本地负载均衡策略

    概述

    Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端,负载均衡的工具。 简单说,ribbon是Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。ribbon客户端组件提供一系列完善的配置项,如超时连接,重试等。就是在配置文件中列出Load Balancer后面所有的机器,Ribbon会自动的帮助你基于某种规则(如:轮询,随机连接)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。

    官网:https://github.com/Netflix/ribbon/wiki/Getting-Started Ribbon目前也进入维护模式。

    LB(负载均衡)

    简单说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。 常见的负载均衡软件:Nginx,LVS,硬件F5等。

    Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别

    Nginx是服务器负载均衡,客户端所有请求都会交给Nginx,然后由Nginx实现转发请求。即负载均衡是由服务端实现。

    Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术

    集中式LB

    即在服务的消费方和提供方之间使用独立的LB设备(可以是硬件,如F5,也可以是软件,如Nginx),由该设施负责将访问请求通过某种策略转发至服务的提供方。

    进程内LB

    将LB逻辑集成到消费方,消费方从服务注册中心获取哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。

    Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。 一句话:ribbon就是负载均衡+RestTemplate调用

    Nginx负载均衡和Ribbon负载均衡的区别?

    假如一个程序你分别部署了至少两个实例,这个时候可以在nginx配置一下负载均衡策略,这里的策略会有多种可选,具体百度一下吧,配置完后,一个请求进来先到nginx,这时候根据之前配置的策略,会把请求转到多个实例中的其中一个,假如程序中使用了springCloud作了分布式部署,在请求的相应方法中会调用其他服务实例,调用的方式是ribbon,那么当被调用的服务是至少两个以上,这个时候ribbon会根据默认的均衡策略选择其中的一个调用,这就是整个流程。总的来说,都是负载均衡,只不过起作用的位置不一样而已。

    例如:一个请求来到nginx,nginx通过负载均衡策略,将请求转发给网关(肯定是集群的),网关接受到请求,做一些处理,处理完毕后,发送请求到微服务,假设是商品的微服务(肯定也是集群的),gateway网关底层使用的就是ribbon,所以也是负载均衡的把请发送到商品服务。

    架构说明

    使用Ribbon实现负载均衡

    在学习Eureka的时候已经用过这个功能,服务提供者集群搭建并实现负载均衡功能:

    https://blog.csdn.net/weixin_42412601/article/details/107030441

    为啥不用引用Ribbon的依赖?

    RestTemplate的使用

    官网:https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html

    getForObject方法/getForEntity方法:

    postForObject/postForEntity:

    Ribbon核心组件IRule_负载均衡策略

    IRule:根据特定算法从服务列表中选取一个要访问的服务。

    类图:

    Ribbon自带的实现

    com.netflix.loadbalancer.RoundRobinRule:轮询。默认使用轮询com.netflix.loadbalancer.RandomRule:随机com.netflix.loadbalancer.RetryRule:先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试WeightedResponseTimeRule :对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择BestAvailableRule :会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务AvailabilityFilteringRule :先过滤掉故障实例,再选择并发较小的实例ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server的可用性选择服务器

    如何替换负载均衡策略

    服务消费者修改。

    配置类:

    把轮询改为随机

    @Configuration public class MySelfRule { @Bean public IRule myRule(){ //默认轮询,现在改为随机策略 return new RandomRule(); } }

    注意配置类放的位置,因为启动类注解SpringBootApplication自带@ComponentScan,并且扫描的是当前包和子包,所以自定义配置类放到springcloud包的外面。

    主启动类添加@RibbonClient

    启动类添加注解:

    @RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)

    配置类:

    @Configuration public class ApplicationContextConfig { // 不加@LoadBalanced会出现:nested exception is java.net.UnknownHostException: CLOUD-PAYMENT-SERVICE @LoadBalanced @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } }

    测试

    启动Eureka服务端,服务提供者(至少两),服务消费者

    http://localhost/consumer/payment/get/1

    多次访问,发现端口随机变化

    Ribbon负载均衡算法

    轮询原理

    假设Eureka上,注册了服务提供者CLOUD-PAYMENT-SERVICE集群,且两个节点

    轮询RoundRobinRule源码分析

    public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { log.warn("no load balancer"); return null; } Server server = null; int count = 0; while (server == null && count++ < 10) { //获取可用的服务提供者节点,如服务提供者CLOUD-PAYMENT-SERVICE下的可用节点 List<Server> reachableServers = lb.getReachableServers(); //获取所有的提供者节点,如服务提供者CLOUD-PAYMENT-SERVICE下的所有节点 List<Server> allServers = lb.getAllServers(); //可用节点的数量 int upCount = reachableServers.size(); //所有节点的数量 int serverCount = allServers.size(); if ((upCount == 0) || (serverCount == 0)) { log.warn("No up servers available from load balancer: " + lb); return null; } //通过自旋锁获取节点的下标index int nextServerIndex = incrementAndGetModulo(serverCount); //根据index获取服务节点 server = allServers.get(nextServerIndex); if (server == null) { /* Transient. */ Thread.yield(); continue; } if (server.isAlive() && (server.isReadyToServe())) { return (server); } // Next. server = null; } if (count >= 10) { log.warn("No available alive servers after 10 tries from load balancer: " + lb); } return server; }

    通过自旋锁获取节点的下标index:

    //原子类,默认值为0 private AtomicInteger nextServerCyclicCounter; public RoundRobinRule() { nextServerCyclicCounter = new AtomicInteger(0); } private int incrementAndGetModulo(int modulo) { //自旋锁 for (;;) { //获取原子类的值 int current = nextServerCyclicCounter.get(); //(值+1)取余所有节点的数量,即2 int next = (current + 1) % modulo; //如果当前值和内存地址里的值相同就把地址内存值改为next if (nextServerCyclicCounter.compareAndSet(current, next)) return next; } }

    说明:第一次发起请求时,nextServerCyclicCounter值为0,(0+1)%2=1,将nextServerCyclicCounter的内存值改为1; 第二次发起请求时,nextServerCyclicCounter值为1,(1+1)%2=0,将nextServerCyclicCounter内存值改为0; 第三次发起情求,nextServerCyclicCounter为0,(0+1)%2=1,将nextServerCyclicCounter的内存值改为1; 。。。 通过看源码发现这里的实现和上面说的轮询原理并不相同,上面说的是: rest接口第几次请求数%服务器集群总数量=实际调用服务器下标,但是源码实际是通过一个(原子类+1)%服务器集群总数量=实际调用服务器下标来获取实际调用服务器下标。可能是版本变更原因。

    自定义本地负载均衡策略

    1.启动Eureka集群。 2.服务提供者,改造: 用于测试

    @GetMapping(value = "/payment/lb") public String getPaymentLB(){ return serverPort; }

    3.服务消费者,修改: 3.1 配置类去掉 @LoadBalanced注解

    @Configuration public class ApplicationContextConfig { // @LoadBalanced @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } }

    3.2定义接口LoadBalancer

    public interface LoadBalancer { //收集服务器总共有多少台能够提供服务的机器,并放到list里面 ServiceInstance instances(List<ServiceInstance> serviceInstances); }

    3.2 自定义负载均衡策略 还是轮询,不过这里使用的原理和上面的原理一致。

    import org.springframework.cloud.client.ServiceInstance; import org.springframework.stereotype.Component; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @Component public class MyLB implements LoadBalancer { //原子类,可以保证原子性,底层直接和操作系统挂钩,在内存中修改值 private AtomicInteger atomicInteger = new AtomicInteger(0); //获取当前接口的访问次数 private final int getAndIncrement(){ int current; int next; do { current = this.atomicInteger.get(); next = current >= 2147483647 ? 0 : current + 1; }while (!this.atomicInteger.compareAndSet(current,next)); //第一个参数是期望值,第二个参数是修改值是 System.out.println("*******第几次访问,次数next: "+next); return next; } @Override public ServiceInstance instances(List<ServiceInstance> serviceInstances) { //得到机器的列表 int index = getAndIncrement() % serviceInstances.size(); //得到服务器的下标位置 return serviceInstances.get(index); } }

    4.服务消费者测试controller

    @Resource private DiscoveryClient discoveryClient; @Resource private LoadBalancer loadBalancer; @GetMapping(value = "/consumer/payment/lb") public String getPaymentLB(){ //获取服务提供者CLOUD-PAYMENT-SERVICE有几台能够提供服务的机子 List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE"); if (instances == null || instances.size() <= 0){ return null; } //使用自定义的负载均衡策略选择机子提供服务 ServiceInstance serviceInstance = loadBalancer.instances(instances); URI uri = serviceInstance.getUri(); System.out.println("URI地址:"+uri.toString()); return restTemplate.getForObject(uri+"/payment/lb",String.class); }

    5.测试 访问:http://localhost/consumer/payment/lb

    Processed: 0.017, SQL: 9