05.负载均衡之Ribbon

Posted 潮汐先生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了05.负载均衡之Ribbon相关的知识,希望对你有一定的参考价值。

前言

在上篇文章04.服务间的通信方式之RestTemplate中我们实现了使用RestTemplate实现微服务之间的通信功能,但是这种方式存在以下两种问题

  • 硬编码:服务间的调用地址被写死在代码中
  • 无负载均衡:上篇文章中我们只有一个ORDER服务,如果现在我们按照文章02.服务注册中心之Eureka中介绍的集群搭建方式再启用一个ORDER服务,端口号为8883,按照上篇文章中的代码我们只能二者选一。

针对上面提到的第二个问题–无负载均衡,我们可以手写一个负载均衡方法,为了便于观察,我们将上节课的USERORDER服务代码改造一下

启动consul

win+R启动cmd窗口,在命令行中输入consul agent -dev启动我们的consul

ORDER服务

1.OrderController

package com.christy.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author Christy
 * @Date 2021/6/1 17:29
 **/
@RestController
@RequestMapping("/order")
public class OrderController {

    @Value("${server.port}")
    private int port;

    @GetMapping("/all")
    public String all(){
        System.out.println("开始获取所有订单");

        return "所有运单获取成功,当前服务的端口号:" + port;
    }
}

2.ORDER集群

对于ORDER服务,我们开启88828883组成一个集群,分别启动这两个服务,如下图

在这里插入图片描述

确保两个服务的接口都能正常访问

在这里插入图片描述

我们在consul中也能看到两个ORDER已经注册而且是以集群的方式注册的
在这里插入图片描述

USER服务

上面我们的ORDER服务完成后下面我们开始改造USER服务,我们在UserController中新增我们的负载均衡的代码

1.UserController

/**
 * @Author Christy
 * @Date 2021/6/1 17:04
 **/
@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/orders")
    public String getUserOrders() {
        System.out.println("开始获取用户的所有运单");

        // 使用RestTemplate调用订单服务的接口
        RestTemplate restTemplate = new RestTemplate();

        /** 使用RestTemplate无负载均衡 **/
        /*String result = restTemplate.getForObject("http://localhost:8882/order/all", String.class);*/

        /** 使用RestTemplate+自定义负载均衡 **/
        String result = restTemplate.getForObject(randomHost() + "/order/all", String.class);


        System.out.println("获取用户所有的运单成功,结果是:" + result);
        return "UserController->getUserOrders:result=" + result;
    }

    /**
     * 自定义负载均衡方法
     * @author Christy
     * @date 2021/6/2 11:45
     * @return
     */
    public String randomHost(){
        List<String> hostList = new ArrayList<>();

        hostList.add("http://localhost:8882");
        hostList.add("http://localhost:8883");

        //生成随机数 只能在0-hosts.size()
        int i = new Random().nextInt(hostList.size());
        return hostList.get(i);
    }
}

2.启动USER

USER中不需要额外的编码了,上面UserController准备完毕后我们启动USER服务,将USER注册到consul中

在这里插入图片描述

在这里插入图片描述

测试

上述工作准备完毕后我们访问UserController中/user//orders接口,观察界面输出

在这里插入图片描述

上面我们简单实现了USER端的负载均衡,但是我们仍然存在硬编码的问题。此外我们自己实现的负载均衡功能单一,切没有充分利用服务注册中心这一角色。总和以上因素,我们下面来看下springcloud提供的Ribbon.

Ribbon

1.Ribbon介绍

Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。

2.执行流程

在这里插入图片描述

3.代码实现

1.pom.xml

如果使用的是eureka client或consul client,则无须引入依赖,因为在eureka与consul中默认集成了ribbon组件

在这里插入图片描述

在这里插入图片描述

如果使用的client中没有ribbon依赖需要显式引入如下依赖

<!--引入ribbon依赖-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

2.客户端实现

通过上面我们知道Ribbon是在客户端进行负载均衡的,所以本例中我们只需要改写USER服务中UserController里面的代码就可以了,Ribbon的负载均衡有三种方式

  • 使用discovery client进行客户端调用
  • 使用loadBalanceClient进行客户端调用
  • 使用@loadBalanced注解进行客户端调用
discovery client
UserController

首先我们按照第一种方式改造一下UserController

/**
 * @Author Christy
 * @Date 2021/6/1 17:04
 **/
@RestController
@RequestMapping("/user")
public class UserController {
    private static final Logger log = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("/orders")
    public String getUserOrders() {
        System.out.println("开始获取用户的所有运单");

        // 使用RestTemplate调用订单服务的接口
        RestTemplate restTemplate = new RestTemplate();

        /** 使用RestTemplate无负载均衡 **/
        /*String result = restTemplate.getForObject("http://localhost:8882/order/all", String.class);*/

        /** 使用RestTemplate + 自定义负载均衡 **/
        /*String result = restTemplate.getForObject(randomHost() + "/order/all", String.class);*/

        /**
         * 使用Ribbon + RestTemplate
         * 使用discovery client进行客户端调用
         * 使用loadBalanceClient进行客户端调用
         * 使用@loadBalanced注解进行客户端调用
         **/

        /** discovery client **/
        List<ServiceInstance> instances = discoveryClient.getInstances("CONSUL-ORDER");
        instances.forEach(serviceInstance -> {
            log.info("服务主机: {} 服务端口:{} 服务地址:{}",serviceInstance.getHost(),serviceInstance.getPort(),serviceInstance.getUri());
        });
        String result = restTemplate.getForObject(instances.get(0).getUri() + "/order/all", String.class);

        System.out.println("获取用户所有的运单成功,结果是:" + result);
        return "UserController->getUserOrders:result=" + result;
    }

    /**
     * 自定义负载均衡方法
     * @author Christy
     * @date 2021/6/2 11:45
     * @return
     */
    public String randomHost(){
        List<String> hostList = new ArrayList<>();

        hostList.add("http://localhost:8882");
        hostList.add("http://localhost:8883");

        //生成随机数 只能在0-hosts.size()
        int i = new Random().nextInt(hostList.size());
        return hostList.get(i);
    }
}
测试

首先重新启动我们的USER服务,然后浏览器访问http://localhost:8881/user/orders

在这里插入图片描述

通过上述结果我们可以知道discoveryClient能够发现我们的服务,但是自己没法进行负载均衡

loadBalanceClient
UserController
/**
 * @Author Christy
 * @Date 2021/6/1 17:04
 **/
@RestController
@RequestMapping("/user")
public class UserController {
    private static final Logger log = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private DiscoveryClient discoveryClient;

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @GetMapping("/orders")
    public String getUserOrders() {
        System.out.println("开始获取用户的所有运单");

        // 使用RestTemplate调用订单服务的接口
        RestTemplate restTemplate = new RestTemplate();

        /** 使用RestTemplate无负载均衡 **/
        /*String result = restTemplate.getForObject("http://localhost:8882/order/all", String.class);*/

        /** 使用RestTemplate + 自定义负载均衡 **/
        /*String result = restTemplate.getForObject(randomHost() + "/order/all", String.class);*/

        /**
         * 使用Ribbon + RestTemplate
         * 使用discovery client进行客户端调用
         * 使用loadBalanceClient进行客户端调用
         * 使用@loadBalanced注解进行客户端调用
         **/

        /** discovery client **/
        /*List<ServiceInstance> instances = discoveryClient.getInstances("CONSUL-ORDER");
        instances.forEach(serviceInstance -> {
            log.info("服务主机: {} 服务端口:{} 服务地址:{}",serviceInstance.getHost(),serviceInstance.getPort(),serviceInstance.getUri());
        });
        String result = restTemplate.getForObject(instances.get(0).getUri() + "/order/all", String.class);*/

        /** loadBalanceClient **/
        ServiceInstance instance = loadBalancerClient.choose("CONSUL-ORDER");
        log.info("服务主机: {} 服务端口:{} 服务地址:{}",instance.getHost(),instance.getPort(),instance.getUri());
        String result = restTemplate.getForObject(instance.getUri() + "/order/all", String.class);

        System.out.println("获取用户所有的运单成功,结果是:" + result);
        return "UserController->getUserOrders:result=" + result;
    }

    /**
     * 自定义负载均衡方法
     * @author Christy
     * @date 2021/6/2 11:45
     * @return
     */
    public String randomHost(){
        List<String> hostList = new ArrayList<>();

        hostList.add("http://localhost:8882");
        hostList.add("http://localhost:8883");

        //生成随机数 只能在0-hosts.size()
        int i = new Random().nextInt(hostList.size());
        return hostList.get(i);
    }
}

loadBalanceClient通过serviceId为我们返回了单个instance,而不是一个列表;这样我们就不需要手动指定选择那个服务的实例进行调用。loadBalanceClient默认是通过轮询的策略返回服务注册中心中可用的单个微服务实例

测试

同样的,我们重启USER服务,浏览器中访问http://localhost:8881/user/orders,我们多访问几次用来观察loadBalanceClient的负载均衡策略

在这里插入图片描述

@loadBalanced

loadBalanced注解修饰在方法上,让当前方法、当前对象具有ribbon负载均衡特性。上面的代码中每次我们都new一个RestTemplate对象,现在我们新建一个配置类,将RestTemplate交由SpringFactory管理,后面我们直接注入就可以了

BeanConfig
package com.christy.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * @Author Christy
 * @Date 2021/6/3 9:33
 **/
@Configuration
public class BeanConfig {

    /*
     * Configuration相当于一个spring配置类 spring.xml
     *
     * @Bean 注解指将当前对象交由springFactory管理
     * 工厂创建对象<bean id="" class="">
     * new RestTemplate()就相当于class=""
     * 方法名就相当于id=”“
     *
     * LoadBalanced注解使当前RestTemplate具有负载均衡的作用
     */
    @Bean
  	@LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
UserController
/**
 * @Author Christy
 * @Date 2021/6/1 17:04
 **/
@RestController
@RequestMapping("/user")
public class UserController {
    private static final Logger log = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private DiscoveryClient discoveryClient;

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/orders")
    public String getUserOrders() {
        System.out.println("开始获取用户的所有运单");

        // 使用RestTemplate调用订单服务的接口
        /*RestTemplate restTemplate = new RestTemplate();*/

        /** 使用RestTemplate无负载均衡 **/
        /*String result = restTemplate.getForObject("http://localhost:8882/order/all", String.class);*/

        /** 使用RestTemplate + 自定义负载均衡 **/
        /*String result = restTemplate.getForObject(randomHost() + "/order/all", String.class);*/

        /**
         * 使用Ribbon + RestTemplate
         * 使用discovery client进行客户端调用
         * 使用loadBalanceClient进行客户端调用
         * 使用@loadBalanced注解进行客户端调用
         **/

        /** discovery client **/
        /*List<ServiceInstance> instances = discoveryClient.getInstances("CONSUL-ORDER");
        instances.forEach(serviceInstance -> {
            log.info("服务主机: {} 服务端口:{} 服务地址:{}",serviceInstance.getHost(),serviceInstance.getPort(),serviceInstance.getUri());
        });
        String result = restTemplate.getForObject(instances.get(0).getUri() + "/order/all", String.class);*/

        /** loadBalanceClient **/
        /*ServiceInstance instance = loadBalancerClient.choose("CONSUL-ORDER");
        log.info("服务主机: {} 服务端口:{} 服务地址:{}",instance.getHost(),instance.getPort(),instance.getUri());
        String result = restTemplate.getForObject(instance.getUri() + "/order/all", String.class);*/

        /** @LoadBalanced **/
        String result = restTemplate.getForObject("http://CONSUL-ORDER/order/all", String.class);

        System.out.println("获取用户所有的运单成功,结果是:" + result);
        return "UserController->getUserOrders:result=" + result;
    }

    /**
     * 自定义负载均衡方法
     * @author Christy
     * @date 2021/6/2 11:45
     * @return
     */
    public String randomHost(){
        List<String> hostList = new ArrayList<>();

        hostList.add("http://localhost:8882");
        hostList.add("http://localhost:8883");

        //生成随机数 只能在0-hosts.size()
        int i = new Random().nextInt(hostList.size());
        return hostList.get(i);
    }
}
测试

我们重新启动USER服务,然后浏览器继续输入http://localhost:8881/user/orders,如下图

在这里插入图片描述

Ribbon总结

Ribbon原理

根据调用服务的服务id去服务注册中心获取对应服务id的服务列表,并将服务列表拉取到本地进行缓存,然后在本地通过默认的轮询策略在现有列表中选择一个可用节点提供服务

这里要明确一点就是Ribbon是在客户端进行负载均衡的

Ribbon的负载均衡策略

源码分析

首先我们通过ServiceInstance instance = loadBalancerClient.choose("CONSUL-ORDER");的choose入口追踪一下源码

在这里插入图片描述

我们看到choose方法进入到了ServiceInstanceChooser中的choose方法,我们看下他的实现类

在这里插入图片描述

点击RibbonLoadBalanceClient,看到里面的的choose方法,

在这里插入图片描述

**this.choose()说明他是一个本类方法,我们继续追踪发现本类方法getServer()**给我们返回了一个server

在这里插入图片描述

继续追踪该方法

在这里插入图片描述

我们继续点击loadBalancer.chooseServer()方法进入到ILoadBalancer中,可以看到chooseServer方法

在这里插入图片描述

它有三个实现类

在这里插入图片描述

我们点击BaseLoadBalancer

在这里插入图片描述

我们可以看到最终提供负载均衡的是rule,他是一个本类的成员变量,我们点击rule,发现他是一个接口

在这里插入图片描述

在这里插入图片描述

我们最后看下他的类关系图

在这里插入图片描述

常用策略
  • RoundRobinRule(轮训策略–按顺序循环选择Server)

  • RandomRule(随机策–随机选择Server)

  • AvailabilityFilteringRule(可用过滤策略)

    会先过滤由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问

  • WeightedResponseTimeRule(响应时间加权策略)
    根据平均响应的时间计算所有服务的权重,响应时间越快服务权重越大被选中的概率越高,刚启动时如果统计信息不足,则使用RoundRobinRule策略,等统计信息足够会切换至该策略

  • RetryRule(重试策略)

    先按照RoundRobinRule的策略获取服务,如果获取失败则在制定时间内进行重试,获取可用的服务。

  • BestAviableRule(最低并发策略)

    会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务

更换策略

想要更换Ribbon默认的负载均衡策略需要在调用方(USER)的配置文件中做以下配置,写法是固定的

比如现在我先将默认的轮询策略修改成随机策略,那么在USER服务的配置文件中做以下修改

server.port=8881
spring.application.name=CONSUL-USERS
# 注册consul服务的主机
spring.cloud.consul.host=localhost
# 注册consul服务的端口号
spring.cloud.consul.port=8500

# 修改Ribbon默认的负载均衡策略为随机策略
CONSUL-ORDER.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

配置完毕后我们重新启动USER服务并访问,如下图

在这里插入图片描述

Ribbon停更

Ribbon停更说明

Project Status: On Maintenance
Ribbon comprises of multiple components some of which are used in production internally and some of which were replaced by non-OSS solutions over time. This is because Netflix started moving into a more componentized architecture for RPC with a focus on single-responsibility modules. So each Ribbon component gets a different level of attention at this moment.

More specifically, here are the components of Ribbon and their level of attention by our teams:

ribbon-core: deployed at scale in production
ribbon-eureka: deployed at scale in production
ribbon-evcache: not used
ribbon-guice: not used
ribbon-httpclient: we use everything not under com.netflix.http4.ssl. Instead, we use an internal solution developed by our cloud security team
ribbon-loadbalancer: deployed at scale in production
ribbon-test: this is just an internal integration test suite
ribbon-transport: not used
ribbon: not used

ribbon-coreribbon-loadbanlancer依然在大规模的使用,我们也主要使用这两个,所以虽然ribbon停止更新了,但是我们还是可以放心的使用ribbon

以上是关于05.负载均衡之Ribbon的主要内容,如果未能解决你的问题,请参考以下文章

负载均衡之Ribbon与LoadBalance

springcloud之Ribbon负载均衡

SpringCloud之实现服务器端的负载均衡Ribbon

聊聊Ribbon源码解读之负载均衡

springcloud之Ribbon,Feign,Hystrix,Gateway介绍

微服务系列之Ribbon负载均衡