Ribbon服务负载

发布于 2022-04-18  7.23k 次阅读


一,Ribbon的简介

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

Ribbon提供的主要功能:

  1. 客户端的软负载均衡
  2. 服务调用

Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等,简单的说就是在配置文件中列出Load Balancer(简称LB)的所有服务端,Ribbon会自动的帮助开发者基于某种规则(如简单轮询,随机连接等)去连接这些机器

Ribbon的模块:

  • 功能区:在其他功能区模块和Hystrix之上集成负载平衡、容错、缓存/批处理的 API
  • ribbon-loadbalancer:可以独立使用或与其他模块一起使用的负载平衡器 API
  • ribbon-eureka:使用Eureka 客户端为云提供动态服务器列表的API
  • 功能区传输:使用具有负载平衡功能的RxNetty支持 HTTP、TCP 和 UDP 协议的传输客户端
  • Ribbon-httpclient:基于 Apache HttpClient 构建的 REST 客户端,集成了负载均衡器(已弃用并被功能区模块取代)

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

Ribbon本地负载均衡和Nginx负载均衡的区别:

  1. Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求,即负载均衡是由服务端实现的(集中式的负载均衡)
  2. Ribbon本地负载均衡,在调用微服务接口的时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程调服务(进程内的负载均衡)

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

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

注:Ribbon默认和Eureka做了整合,所以在引入Eureka Client或Eureka Server时就默认带了Ribbon

Ribbon在维护中:

二,Ribbon负载均衡机制

一,Ribbon的负载过程

Ribbon执行分为两步:

  1. 先选择EurekaServer,它优先选择在同一个区域负载较少的server
  2. 根据用户指定的负载均衡策略,在从server取到的服务注册列表中选择一个地址,通过RestTemplate进行访问

Ribbon只是一个负载均衡的服务工具,具体的服务调用需要依赖于RestTemplate(底层Httpclient)进行调用

一般使用Ribbon+RestTemplate进行服务调用

二,Ribbon负载均衡机制和规则替换

一,Ribbon负载均衡机制

Ribbon负载均衡算法通过IRule接口实现,如:轮询算法,随机算法都实现了IRule

IRule接口源码:

public interface IRule {

    //服务名称
    Server choose(Object var1);

    //设置负载均衡算法
    void setLoadBalancer(ILoadBalancer var1);

    //获取负载均衡算法
    ILoadBalancer getLoadBalancer();
}

IRule的实现类:

 

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

二,Ribbon负载均衡的规则替换

官方给出的自定义规则替换的警告:自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则自定义的配置类就会被所有的Ribbon客户端所共享,达不到特殊定制化的目的

注:@ComponentScan所扫描的包就是SpringBoot启动类的包,所有在springboot项目中Ribbon自定义不能和SpringBoot启动类同包

① 父工程依赖

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>SpringCloud-Dome</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <junit.version>4.12</junit.version>
        <lombok.version>1.18.10</lombok.version>
        <log4j.version>1.2.17</log4j.version>
        <mysql.version>5.1.31</mysql.version>
        <druid.version>1.1.16</druid.version>
        <mybatis.spring.boot.version>2.1.1</mybatis.spring.boot.version>
    </properties>


    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.2.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.1.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

② maven依赖

<dependencies>
    <dependency>
        <groupId>com.cloud.commons</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-actuator</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
    </dependency>

    <!--eureka服务端,eureka内置了Ribbon依赖不需要单独引入-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.2</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>com.cloud.commons</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
</dependencies>

③ application配置

server:
  port: 8084
spring:
  application:
    name: cloud-ribbon-client
eureka:
  client:
    fetch-registry: true #是否抓取已有的注册信息,默认为true,单节点无所谓,集群时必须设置为true才能配合Ribbon使用负载均衡
    register-with-eureka: true #是否将自己注册进Eureka Server
    service-url:
      defaultZone: http://eureka1.com:9090/eureka/,http://eureka2.com:9091/eureka/

④ 自定义IRule(这个包不能为主启动类的子包)

@Configuration
public class MySelfRlue {

    @Bean
    public IRule Myrule(){
        return new RandomRule(); //定义随机
    }
}

⑤ 启动类

@SpringBootApplication
@EnableDiscoveryClient
@EnableEurekaClient
//name位服务名,configuration位自定义Rule的class
@RibbonClient(name = "CLOUD-PAYMENT-SERVER",configuration = MySelfRlue.class)
public class RibbonMain {
    public static void main(String[] args) {

        SpringApplication.run(RibbonMain.class,args);}

}

⑥ controller

@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerController {

    public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVER";

    @Autowired
    private DiscoveryClient discoveryClient;

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping(value = "/payment/create",method = RequestMethod.GET)
    public CommonResult<payment> create(payment payments){

       return  restTemplate.postForObject(PAYMENT_URL+"/payment/create",payments,CommonResult.class);
}

    @RequestMapping("/payment/select/{id}")
    public CommonResult<payment> select(@PathVariable("id") int id){

        return  restTemplate.getForObject(PAYMENT_URL+"/payment/select/"+id,CommonResult.class);
    }

测试:

三,Ribbon默认负载轮询算法原理

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

List<Servicelnstance> instances = discpveryClient.getInstances("CLOUD-PAYMENT-SERVICE");

如:List[0] instance = 127.0.0.1:8002     List[1] instance = 127.0.0.1:8001

8001+8002组合成为集群,它们共计两台机器,集群总数为2

按照轮询算法原理:

  • 当总请求数为1时: 1%2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001
  • 当总请求数位2时: 2%2=0对应下标位置为0,则获得服务地址为127.0.0.1:8002
  • 当总请求数位3时:3%2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001
  • 当总请求数位4时: 4%2=0对应下标位置为0,则获得服务地址为127.0.0.1:8002
  • 如此类推.…………

源码:

public class RoundRobinRule extends AbstractLoadBalancerRule {
    private AtomicInteger nextServerCyclicCounter;
    private static final boolean AVAILABLE_ONLY_SERVERS = true;
    private static final boolean ALL_SERVERS = false;
    private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);

    public RoundRobinRule() {
        this.nextServerCyclicCounter = new AtomicInteger(0);
    }


    public RoundRobinRule(ILoadBalancer lb) {
        this();
        this.setLoadBalancer(lb);
    }


    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) {
                if (server == null && count++ < 10) {
                    List<Server> reachableServers = lb.getReachableServers();
                    List<Server> allServers = lb.getAllServers();
                    int upCount = reachableServers.size();
                    int serverCount = allServers.size();
                    if (upCount != 0 && serverCount != 0) {
                        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;
        do {
            current = this.nextServerCyclicCounter.get();
            next = (current + 1) % modulo;
        } while(!this.nextServerCyclicCounter.compareAndSet(current, next));


        return next;
    }


    public Server choose(Object key) {
        return this.choose(this.getLoadBalancer(), key);
    }


    public void initWithNiwsConfig(IClientConfig clientConfig) {
    }
}

路漫漫其修远兮,吾将上下而求索