服务负载均衡:Ribbon

目录


配套资料,免费下载
链接:https://pan.baidu.com/s/1la_3-HW-UvliDRJzfBcP_w
提取码:lxfx
复制这段内容后打开百度网盘手机App,操作更方便哦

第一章 Ribbon介绍

1.1、什么是Ribbon

Ribbon 是一个基于 HTTP 和 TCP 的客服端负载均衡工具,它是基于 Netflix Ribbon 实现的。它不像 Spring Cloud 服务注册中心、配置中心、API 网关那样独立部署,但是它几乎存在于每个 Spring Cloud 微服务中。包括 Feign 提供的声明式服务调用也是基于该 Ribbon 实现的。

Ribbon 默认提供很多种负载均衡算法,例如轮询、随机等等,甚至包含自定义的负载均衡算法。

1.2、为啥用Ribbon

Ribbon 提供了一套微服务的负载均衡解决方案。目前业界主流的负载均衡方案可分成两类:

  • 集中式负载均衡(服务器负载均衡),即在 consumer 和 provider 之间使用独立的负载均衡设施(可以是硬件,如 F5,也可以是软件,如 nginx),由该设施负责把访问请求通过某种策略转发至 provider;
  • 进程内负载均衡(客户端负载均衡),将负载均衡逻辑集成到 consumer,consumer 从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的 provider。Ribbon 属于后者,它只是一个类库,集成于 consumer 进程,consumer 通过它来获取 provider 的地址。

第二章 Ribbon入门案例

2.1、项目准备

我们接下来的所有操作均是在Eureka最后完成的工程上进行操作,相关代码请到配套资料中寻找。

服务负载均衡:Ribbon

2.2、添加服务提供者

既然我们要做服务的负载均衡,那肯定需要有很多服务,一个服务再怎么负载均衡还是他自己,因此,我们需要在原工程的基础上在添加一个服务提供者,配置基本和service-provider8001差不多,只不过端口不一样而已,接下来会给出具体不一样的配置。

(1)修改启动类为ServiceProvider8002Application

@EnableEurekaClient
@SpringBootApplication
public class ServiceProvider8002Application {
    public static void main(String[] args) {
        SpringApplication.run(ServiceProvider8002Application.class);
    }
}

(2)修改配置文件application.yaml

server:
  port: 8002

spring:
  application:
    #该名称在集群模式下应该保持一致
    name: service-provider

#度量指标监控与健康检查
management:
  endpoints:
    web:
      exposure:
        #开启 shutdown 端点访问
        include: shutdown
  endpoint:
    #开启 shutdown 实现优雅停服
    shutdown:
      enabled: true

eureka:
  instance:
    #是否使用 ip 地址注册
    prefer-ip-address: true
    #该实例注册到服务中心的唯一ID
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  client:
    #设置服务注册中心地址
    service-url:
      defaultZone: http://root:123456@eureka-server7001.com:7001/eureka/,http://root:123456@eureka-server7002.com:7002/eureka/

(3)分别修改service-provider8001service-provider8002的控制层ProductController 代码,都加入一段新的控制器方法,代码如下:

service-provider8001的ProductController

@RequestMapping("/provider/product/findByPid")
public String findByPid(@RequestParam("pid") Integer pid) {
    return "8001 findByPid ...";
}

service-provider8002的ProductController

@RequestMapping("/provider/product/findByPid")
public String findByPid(@RequestParam("pid") Integer pid) {
    return "8002 findByPid ...";
}

(4)启动注册中心

  1. 启动eureka-server7001
  2. 启动eureka-server7002

(5)启动服务提供者

  1. 启动service-provider8001
  2. 启动service-provider8002

(6)查看服务是否注册到注册中心,登录账户为root,登录密码为123456

在浏览器打开地址:http://localhost:7001/

服务负载均衡:Ribbon

2.3、修改服务消费者

2.3.1、方式一:LoadBalancerClient

(1)修改service-consumer9001ProductController控制器代码,在里边新增以下代码

// Ribbon 负载均衡器
// import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
@Autowired
private LoadBalancerClient loadBalancerClient;

@RequestMapping("/consumer/product/findByPid")
public String findByPid(@RequestParam("pid") Integer pid) {
    //从注册中心获取一个SERVICE-PROVIDER服务的实例对象
    ServiceInstance si = loadBalancerClient.choose("SERVICE-PROVIDER");
    if (si == null) {
        return null;
    }

    String findByPidUrl = si.getUri() + "/provider/product/findByPid" + "?pid=" + pid;
    String product = restTemplate.getForObject(findByPidUrl, String.class);
    return product;
    //这里只是做模拟返回了一个字符串,当然你也可以返回一个商品对象,这取决于你
}

(2)启动service-consumer9001,然后打开浏览器输入地址访问:http://localhost:9001/consumer/product/findByPid?pid=1

服务负载均衡:Ribbon

2.3.2、方式二:@LoadBalanced

(1)修改service-consumer9001ServiceConsumer9001Application启动类代码,在里边新增以下代码

@EnableEurekaClient
@SpringBootApplication
public class ServiceConsumer9001Application {
    public static void main(String[] args) {
        SpringApplication.run(ServiceConsumer9001Application.class);
    }

    @Bean
    @LoadBalanced // Ribbbon 负载均衡注解,加上这个注解,restTemplate默认就有负载均衡的能力了
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

(2)修改service-consumer9001ProductController控制器代码,在里边新增以下代码

@RequestMapping("/consumer/product/findByPid2")
public String findByPid2(@RequestParam("pid") Integer pid) {
    String findByPidUrl = "http://SERVICE-PROVIDER/provider/product/findByPid" + "?pid=" + pid;
    String product = restTemplate.getForObject(findByPidUrl, String.class);
    System.out.println(product);
    return product;
}

(3)重启service-consumer9001,然后打开浏览器输入地址访问:http://localhost:9001/consumer/product/findByPid2?pid=1

服务负载均衡:Ribbon

第三章 Ribbon负载均衡策略

3.1、轮询策略(默认)

策略对应类名:RoundRobinRule

实现原理:轮询策略表示每次都顺序取下一个 provider,比如一共有 5 个 provider,第 1 次取第 1 个,第 2 次取第 2 个,第 3 次取第 3 个,以此类推。

3.2、权重轮询策略

策略对应类名:WeightedResponseTimeRule

实现原理:

  • 根据每个 provider 的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性越低。
  • 原理:一开始为轮询策略,并开启一个计时器,每 30 秒收集一次每个 provider 的平均响应时间,当信息足够时,给每个 provider 附上一个权重,并按权重随机选择 provider,高权越重的 provider 会被高概率选中。

3.3、随机策略

策略对应类名:RandomRule

实现原理:从 provider 列表中随机选择一个。

3.4、最少并发数策略

策略对应类名:BestAvailableRule

实现原理:选择正在请求中的并发数最小的 provider,除非这个 provider 在熔断中。

3.5、重试策略

策略对应类名:RetryRule

实现原理:其实就是轮询策略的增强版,轮询策略服务不可用时不做处理,重试策略服务不可用时会重新尝试集群中的其他节点。

3.6、可用性敏感策略

策略对应类名:AvailabilityFilteringRule

实现原理:过滤性能差的 provider

  • 第一种:过滤掉在 Eureka 中处于一直连接失败的 provider。
  • 第二种:过滤掉高并发(繁忙)的 provider。

3.7、区域敏感性策略

策略对应类名:ZoneAvoidanceRule

实现原理:

  • 以一个区域为单位考察可用性,对于不可用的区域整个丢弃,从剩下区域中选可用的 provider。
  • 如果这个 ip 区域内有一个或多个实例不可达或响应变慢,都会降低该 ip 区域内其他 ip 被选中的权
    重。

第四章 Ribbon负载均衡设置

4.1、全局替换

在启动类或配置类中注入负载均衡策略对象,所有服务请求均使用该策略。

@EnableEurekaClient
@SpringBootApplication
public class ServiceConsumer9001Application {
    public static void main(String[] args) {
        SpringApplication.run(ServiceConsumer9001Application.class);
    }

    @Bean
    @LoadBalanced // Ribbbon 负载均衡注解,加上这个注解,restTemplate默认就有负载均衡的能力了
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

    @Bean
    public RandomRule randomRule() {
        return new RandomRule();
    }
}

重启消费者然后多次访问:http://localhost:9001/consumer/product/findByPid2?pid=1,结果如下:

服务负载均衡:Ribbon

4.2、局部替换

请注释掉刚才的全局设置,避免影响结果,修改配置文件指定服务的负载均衡策略。格式:服务应用名.ribbon.NFLoadBalancerRuleClassName

# 负载均衡策略
# service-provider 为调用的服务的名称
service-provider:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

重启消费者然后多次访问:http://localhost:9001/consumer/product/findByPid2?pid=1,结果如下:

服务负载均衡:Ribbon

第五章 Ribbon手写负载均衡

5.1、负载均衡算法

服务负载均衡:Ribbon

5.2、轮询策略源码

核心:自旋锁,private int incrementAndGetModulo(int modulo)方法用到了自旋锁

定义:
自旋锁(spin lock)是一种非阻塞锁,也就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁。线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。

作用:
自旋锁避免了进程上下文的调度开销。线程一直处于用户态,减少了用户态到内核态的开销与损耗(减少了上下文切换)。

适用场景:
1、多线程
2、使用者占有锁的时间短

轮询源码:

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) {
    }
}

5.3、手写负载均衡

(1)创建一个插件包名字叫:com.caochenlei.plugin

(2)在这个插件包中新建类:MyLoadBalanced

(3)手写负载均衡器来替代默认的负载均衡器注解@LoadBalanced,源码如下:

@Component
public class MyLoadBalanced {
    private AtomicInteger atomicInteger = new AtomicInteger(0);

    //获取服务提供者实例下标
    public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
        int index = getAndIncrement() % serviceInstances.size();
        return serviceInstances.get(index);
    }

    //使用自旋锁进行累加操作
    private 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);
        return next;
    }
}

(4)在控制器类ProductController中新增如下测试代码:

@Autowired
private MyLoadBalanced loadBalanced;

@RequestMapping("/consumer/product/findByPid3")
public String findByPid3(@RequestParam("pid") Integer pid) {
    //获取注册中心服务列表
    List<ServiceInstance> instances = discoveryClient.getInstances("SERVICE-PROVIDER");
    if (CollectionUtils.isEmpty(instances)) {
        return null;
    }

    //使用自定义负载均衡器
    ServiceInstance si = loadBalanced.instances(instances);
    String findByPidUrl = si.getUri() + "/provider/product/findByPid" + "?pid=" + pid;
    String product = restTemplate.getForObject(findByPidUrl, String.class);
    System.out.println(product);
    return product;
}

(5)注释配置文件中配置的随机策略

# 负载均衡策略
# SERVICE-PROVIDER 为调用的服务的名称
#SERVICE-PROVIDER:
#  ribbon:
#    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

(6)注释Ribbon默认的负载均衡器注解

@EnableEurekaClient
@SpringBootApplication
public class ServiceConsumer9001Application {
    public static void main(String[] args) {
        SpringApplication.run(ServiceConsumer9001Application.class);
    }

    @Bean
    //@LoadBalanced // Ribbbon 负载均衡注解,加上这个注解,restTemplate默认就有负载均衡的能力了
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

//    @Bean
//    public RandomRule randomRule() {
//        return new RandomRule();
//    }
}

5.4、测试最终效果

重启消费者然后多次访问:http://localhost:9001/consumer/product/findByPid3?pid=1,结果如下:

服务负载均衡:Ribbon

注意:此时如果你访问http://localhost:9001/consumer/product/findByPid2?pid=1,则会报错,因为这个方法中使用的是默认的负载均衡器,刚才我们已经注释掉了。

上一篇:Spring Cloud中使用Feign实现负载均衡--RandomRule


下一篇:深入剖析ribbon源码