SpringCloud-Ribbon负载均衡机制、手写轮询算法

Ribbon 内置的负载均衡规则

com.netflix.loadbalancer 包下有一个接口 IRule,它可以根据特定的算法从服务列表中选取一个要访问的服务,默认使用的是「轮询机制」

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

负载规则的替换

如果不想使用 Ribbon 默认使用的规则,我们可以通过自定义配置类的方式,手动指定使用哪一种。

需要注意的是,自定义配置类不能放在 @ComponentScan 所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的 Ribbon 客户端所共享,达不到特殊化定制的目的了。

因此我们需要在 Spring Boot 启动类所在包的外面新建一个包存放自定义配置类

@Configuration
public class MyselfRule {
    
    @Bean
    public IRule rule(){
        //随机
        return new RandomRule();
    }
}

然后在启动类上添加如下注解,指定服务名及自定义配置类

@RibbonClient(value = "CLOUD-PAYMENT-SERVICE", configuration = MyselfRule.class)

Ribbon 默认负载轮询算法的原理

算法概述

rest 接口第几次请求数 % 服务器集群总个数 = 实际调用服务器位置下标,服务每次重启后 rest 请求数变为1

源码

public Server choose(ILoadBalancer lb, Object key) {
    if (lb == null) {
        log.warn("no load balancer");
        return null;
    }

    Server server = null;
    int count = 0;
    //循环获取服务,最多获取10次
    while (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)) {
            log.warn("No up servers available from load balancer: " + lb);
            return null;
        }

        //计算下一个服务的下标
        int nextServerIndex = incrementAndGetModulo(serverCount);
        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;
}

//通过此方法获取服务的下标,使用了 CAS 和自旋锁
private int incrementAndGetModulo(int modulo) {
    for (;;) {
        int current = nextServerCyclicCounter.get();
        int next = (current + 1) % modulo;
        if (nextServerCyclicCounter.compareAndSet(current, next))
            return next;
    }
}

手写轮询算法

在服务提供者写一个方法,返回端口号看效果就行

@GetMapping("/payment/lb")
public String roundLb(){
    return this.serverPort;
}

负载均衡接口

public interface LoadBalancer {
    /**
     * 获取服务实例
     */
    ServiceInstance getInstance(List<ServiceInstance>serviceInstances);
}

算法实现类

@Component
public class MyLb implements LoadBalancer {

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    /**
     * 使用「自旋锁」和「CAS」增加请求次数
     */
    public final int incrementAndGet() {
        int current;
        int next;
        do {
            current = atomicInteger.get();
            //防溢出
            next = current >= Integer.MAX_VALUE ? 0 : current + 1;
        } while (!atomicInteger.compareAndSet(current, next));
        return next;
    }

    @Override
    public ServiceInstance getInstance(List<ServiceInstance> serviceInstances) {
        // 实际调用服务器位置下标 = rest 接口第几次请求数 % 服务器集群总个数
        int index = incrementAndGet() % serviceInstances.size();
        return serviceInstances.get(index);
    }
}

编写服务消费者方法,记得注释 @LoadBalanced 注解,否则不生效

@GetMapping("/consumer/payment/lb")
public String roundLb(){
    List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
    if (instances == null || instances.size() <= 0){
        return null;
    }
    ServiceInstance instance = loadBalancer.getInstance(instances);
    URI uri = instance.getUri();
    return restTemplate.getForObject(uri + "/payment/lb", String.class);
}
posted @ 2020-04-22 14:58  农夫三拳有点疼~  阅读(1351)  评论(0编辑  收藏  举报