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<CommonResult> 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)。

Spring Cloud笔记-Ribbon负载均衡服务调用(八)_spring

  • 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 {
    @Bean
    public 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的server
                List<Server> reachableServers = lb.getReachableServers();
                // 获取所有server
                List<Server> 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<ServiceInstance> 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);

    @Override
    public ServiceInstance instances(List<ServiceInstance> 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<ServiceInstance> 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,可以看到端口号的轮询变化,此时我们自己的负载均衡轮询策略生效。