侧边栏壁纸
博主头像
coydone博主等级

记录学习,分享生活的个人站点

  • 累计撰写 306 篇文章
  • 累计创建 51 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Ribbon

coydone
2022-08-03 / 0 评论 / 0 点赞 / 326 阅读 / 9,287 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-05-03,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

概述

Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。

简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。

简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。

Github - Ribbon:Ribbon目前也进入维护模式,Ribbon未来可能被Spring Cloud LoadBalacer替代。

LB负载均衡(Load Balance):简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。

常见的负载均衡有软件Nginx,LVS,硬件F5等。,例如:Dubbo和SpringCloud中均给我们提供了负载均衡,SpringCloud的负载均衡算法可以自定义。

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

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

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

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

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

Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

Ribbon的LB和Rest调用

Ribbon其实就是一个软负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用,和Eureka结合只是其中的一个实例。

架构说明

Ribbon在工作时分成两步:

  1. 先选择Eureka Server,它优先选择在同一个区域内负载较少的Server。

  2. 再根据用户指定的策略,在从Server取到的服务注册列表中选择一个地址。

其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。

基本使用

POM,在消费者工程中引入Ribbon。

先前工程项目没有引入spring-cloud-starter-ribbon也可以使用ribbon。这是因为spring-cloud-starter-netflix-eureka-client自带了spring-cloud-starter-ribbon引用。

<dependency>
    <groupld>org.springframework.cloud</groupld>
    <artifactld>spring-cloud-starter-netflix-ribbon</artifactid>
</dependency>

RestTemplate的使用

官方使用文档:RestTemplate Java Doc

getForObject()/getForEntity():GET请求方法

  • getForObject():返回对象为响应体中数据转化成的对象,基本上可以理解为Json。

  • getForEntity():返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。

postForObject()/postForEntity():POST请求方法

@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id) {
    ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
    if(entity.getStatusCode().is2xxSuccessful()){
        return entity.getBody();//getForObject()
    }else{
        return new CommonResult<>(444,"操作失败");
    }
}

通过SpringCloud Ribbon的封装,我们在微服务架构中使用客户端负载均衡调用非常简单,只需要如下两步:

  • 服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心。

  • 服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用。

这样,我们就可以将服务提供者的高可用以及服务消费者的负载均衡调用一起实现了。

Ribbon核心组件IRule

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

在Ribbon中实现了非常多的选择策略,其中也包含了RoudRobinRule和ZoneAvoidanceRule。

负载均衡策略:

AbstractLoadBalancerRule

负载均衡策略的抽象类,在该抽象类中定义了负载均衡器ILoadBalancer对象,该对象能够在具体实现选择服务策略时,获取到一些负载均衡器中维护的信息来作为分配依据,并以此设计一些算法来实现针对特定场景的高效策略。

RandomRule

该策略实现了从服务实例清单中随机选择一个服务实例的功能。具体的选择逻辑在一个while(server==null)循环之内,而根据选择逻辑的实现,正常情况下每次选择都应该选出一个服务实例,如果出现死循环获取不到服务实例,如果出现死循环获取不到服务实例时,则很有可能存在并发的Bug。

RoundRobinRule

该策略实现了按照线性轮询的方式的方式一次选择每个服务实例的功能。它的具体实现如下,其详细结构与RandomRule非常类似。除了循环条件不同外,就是从可用列表中获取所谓的逻辑不通。从循环条件中,我们可以看到增加了一个count计数变量,该变量会在每次循环之后累加,也就是说,如果一直选择不到server超过10次,那么就会结束尝试,并打印一个警告信息No available alive servers after 10tries from load balancer : … 。

RetryRule

该策略实现了一个具备重试机制的实例选择功能。从下面的实现中我们可以看到,在其内部还定义了一个IRule对象,默认使用了RoudRobinRule实例。而在choose方法中则实现了对内部定义的策略进行反复尝试的策略,若期间能够选择到具体的服务实例就反悔,若选择不到就根据设置结束时间为阀值(maxRetryMillis参数定义的值+choose方法开始执行的时间戳),当超过该阀值就返回null。

WeightedResponseTimeRule

该策略是对RoundRobinRule的扩展,增加了根据实例等运行情况来计算权重,并根据权重来挑选实例,以达到更优的分配效果,它的实现主要有三个核心内容:定时任务、权重计算、实例选择

ClientConfigEnabledRoundRobinRule

该策略较为特殊,我们一般不直接使用它,因为它本身并没有实现什么特殊的处理逻辑,正如下面的源码所示,在它的内部定义了一个RoundRobinRule策略,而choose函数的实现也正是使用了RoundRobinRule的线性轮询机制,所以它实现的功能实际上与RoundRobinRule相同,那么定义它有什么特殊的用处呢?

虽然我们不会直接使用该策略,但是通过继承该策略,默认的choose就实现了线性轮询机制,在子类中做一些高级策略时通常有可能会存在一些无法实施的情况,那么久可以用父类的实现作为备选。在后面中我们将继续介绍的高级策略均是基于ClientConfigEnabledRoundRobinRule的扩展。

BestAvailableRule

该策略继承自ClientConfigEnabledRoundRobinRule,在实现中它注入了负载均衡器的统计对象LoadBalancerStats,同时在具体的choose算法中利用LoadBalancerStats保存的实例统计信息来选择满足要求的实例。从如下源码中我们可以看到,它通过遍历负载均衡器中维护的所有服务实例,会过滤掉故障的实例,并找出并发请求数最小的一个,所以该策略的特性是可选出最空闲的实例。

同时,由于该算法的核心依据是统计对象LoadBalancerStats,当其为空的时候,该策略是无法执行的。所以从源码中我们可以看到,当loadBalancerStats为空的时候,它会采用父类的线性轮询策略,正如我们在介绍ClientConfigEnabledRoundRobinRule时那样,它的子类在无法满足实现高级策略的时候,可以使用线性轮询策略的特性。后面将要介绍的策略因为也都继承自ClientConfigEnabledRoundRobinRule,所以它们都会具有这样的特性。

PredicateBasedRule

这是一个抽象策略,它也继承了ClientConfigEnabledRoundRobinRule,从其命名中可以猜出这是一个基于Predicate实现的策略,Predicate是Google Guava Collections工具对集合进行过滤掉条件接口。

Google Guava Collections是一个对Java Collections Framework增强和扩展的开源项目。虽然Java Collections Framework已经能够满足我们大多数抢空下使用集合的要求,但是当遇到一些特殊情况时,我们的代码会比较冗长且容易出错。Google Guava Collections可以帮助我们让集合操作代码更为简短精炼并大大增强代码的可读性。

AvailabilityFilteringRule

该策略继承自上面介绍的抽象策略PredicateBasedRule,所以它也继承了“先过滤清单,再轮询选择”的的基本处理逻辑,其中过滤条件使用了AvailabilityPredicate。简单地说,该策略通过线性抽样的方式直接尝试寻找可用且较空闲的实例来使用,优化了父类每次都要遍历所有实例的开销。

ZoneAvoidanceRule

它也是PredicateBasedRule的具体实现类,在之前的介绍中主要针对ZoneAvoidanceRule中用于选择Zone区域策略的一些静态函数,比如createSnapshot(LoadBalancerStats lbStats)、getAvailableZones(Map snapshot, double triggeringLoad,double triggeringBlackoutPercentage)。在这里我们将详细看看ZoneAvoidanceRule作为服务实例过滤条件的实现原理。从下面ZoneAvoidanceRule的源码片段中可以看到,它使用了CompositePredicate来进行服务实例清单的过滤。这是一个组合过来条件,在其构造函数中,它以ZoneAvoidanceRule为主过滤条件,AvailabilityPredicate为次过滤条件初始化了组合过滤条件的实例。

Ribbon负载规则替换

我们可以修改cloud-consumer-order80消费者工程。

官方文档明确给出了警告:这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。

因为主启动类有@SpringBootApplication,而此注解下有@ComponentScan注解,所以配置类不能和主启动类同包。

1、我们新建package:com.coydone.myrule在com.coydone.myrule下新建MySelfRule规则类。

package com.coydone.myrule;

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 MySelfRule {
    @Bean
    public IRule myRule(){
        return new RandomRule();
 //定义为随机
    }
}

2、主启动类添加@RibbonClient

//在启动该微服务的时候就能去加载我们的自定义Ribbon配置类,从而使配置生效
@RibbonClient(name="CLOUD-PAYMENT-SERVICE",configuration=MySelfRule.class)

3、测试

开启cloud-eureka-server7001,cloud-consumer-order80,cloud-provider-payment8001,cloud-provider-payment8002。浏览器输入http://localhost/consumer/payment/get/1,返回结果中的serverPort在8001与8002两种间反复横跳。

Ribbon负载均衡算法

原理

默认负载轮询算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务重启动后rest接口计数从1开始。

List<Servicelnstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
如:
List [0] instances = 127.0.0.1:8002
List [1] instances = 127.0.0.1:8001
8001+ 8002组合成为集群,它们共计2台机器,集群总数为2,按照轮询算法原理:
当总请求数为1时:1%2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001
当总请求数位2时:2%2=О对应下标位置为0,则获得服务地址为127.0.0.1:8002
当总请求数位3时:3%2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001
当总请求数位4时:4%2=О对应下标位置为0,则获得服务地址为127.0.0.1:8002
如此类推…

轮询原理:RoundRobinRule(轮询规则)类

public Server choose(ILoadBalancer lb, Object key) {
    ...
	if (server == null && count++ < 10) {
		//获取健康可用的Servers
        List<Server> reachableServers = lb.getReachableServers();
        //获取所有的Servers
		List<Server> allServers = lb.getAllServers();
        int upCount = reachableServers.size();//健康Server的数量
        int serverCount = allServers.size();//所有Server的数量
        if (upCount != 0 && serverCount != 0) {
        	int nextServerIndex = this.incrementAndGetModulo(serverCount);
        	server = (Server)allServers.get(nextServerIndex);//下一个的Server角标
	...
}
//增加并取模
private int incrementAndGetModulo(int modulo) {
	int current;
	int next;
    do {
	    current = this.nextServerCyclicCounter.get();
        next = (current + 1) % modulo;
 //取模操作
    } while(!this.nextServerCyclicCounter.compareAndSet(current, next));
    return next;
}

手写轮询算法

自己试着写一个类似RoundRobinRule的本地负载均衡器。

7001/7002集群启动

1、8001/8002微服务改造:controller

@RestController
@Slf4j
public class PaymentController{
    ...
	@GetMapping("/payment/lb")
    public String getPaymentLB() {
        return serverPort;//返回服务接口
    }
    ...
}

2、80订单微服务改造

(1)ApplicationContextConfig类中去掉注解@LoadBalanced,OrderMain80去掉注解@RibbonClient。

(2)创建LoadBalancer接口及其实现类。

package com.coydone.springcloud.lb;

import org.springframework.cloud.client.ServiceInstance;

import java.util.List;

public interface LoadBalancer {
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
package com.coydone.springcloud.lb;

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);
    public final int getAndIncrement() {
        int current;
        int 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;
    }

    //负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标  ,每次服务重启动后rest接口计数从1开始。
    @Override
    public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
        int index = getAndIncrement() % serviceInstances.size();
        return serviceInstances.get(index);
    }
}

(3)OrderController

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import com.coydone.springcloud.lb.LoadBalancer;

@Slf4j
@RestController
public class OrderController {
    //public static final String PAYMENT_URL = "http://localhost:8001";
    public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
	...
    @Resource
    private LoadBalancer loadBalancer;

    @Resource
    private DiscoveryClient discoveryClient;
	...
    @GetMapping("/consumer/payment/lb")
    public String getPaymentLB() {
        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();
        return restTemplate.getForObject(uri+"/payment/lb",String.class);
    }
}

(4)测试,不停地刷新http://localhost/consumer/payment/lb,可以看到8001/8002交替出现。

0

评论区