【微服务】负载均衡 - LoadBalance(day4)

下述所有代码都是在订单服务中修改的,商品服务并不需要修改,只需要启动多个实例即可。

引入

在介绍Eureka组件的最后,留下了一个问题就是,无论启动多少个实例,只能调用第一个。原因是因为服务调用时获取的是一个实例数组,但是我们进行了硬编码,只get第一个实例进行了调用。找到原因之后,我们就要找到修改代码的方式,进而合理分配调用的主机,减少单机的压力。

但是,如何修改代码,进行合理分配成了一个难点。我想到一个解决方法就是在项目启动之初,去注册中心拉取被调用服务的数量,这样每次进行调用时使用 计数器 % 实例数 的方法来进行分配每次远程调用访问的主机。但是,由于拉取被调用服务的数量这个操作也是远程调用,因此可能每次拉取的实例顺序是不同的,这个我们可以在项目启动时把数量和主机内容都进行记录,没有啥问题;不过,更严重的一个问题是,在运行过程中,主机是可能宕机的,并且对于一个大促是可能进行动态扩缩容的,这是对被调用方进行一个改变,调用方并不会重启,这就导致调用方启动时获取的实例数量不会改变,从而导致扩缩容添加的主机起不到作用,因此微服务自己出了一个类似于Nginx的组件来进行负载均衡。

复制代码
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

    // 版本三:自定义负载均衡版本
    private static final String PRODUCT_SERVICE_NAME = "cloud-provider-payment8001";

    @Resource
    private RestTemplate restTemplate;

    @Resource
    private DiscoveryClient discoveryClient;

    // 使用原子类来表示调用次数
    private AtomicInteger count = new AtomicInteger(1);
    
    // 初始化方法,在项目启动时调取实例
    private List<ServiceInstance> instances;
    @PostConstruct
    private void init() {
        instances = discoveryClient.getInstances(PRODUCT_SERVICE_NAME);
        log.info("查看被调用方实例的数量:" + instances.size());
    }

    @Override
    public Order getOrderById(Integer id) {
        // 获取订单
        Order order = this.getById(id);
        // 获取应该调用的实例下标
        int index = this.count.getAndIncrement() % instances.size();
        // 获取该实例的内容
        ServiceInstance serviceInstance = this.instances.get(index);
        // 拼接URL
        String url = serviceInstance.getUri() + "/product/query/" + order.getProductId();
        log.info("被调用服务的URI:" + serviceInstance.getUri());
        // 远程调用
        Product product = this.restTemplate.getForObject(url, Product.class);
        order.setProduct(product);
        // 返回结果
        return order;
    }

}

如上图所示,订单服务在启动时,就会去拉取被调用方存在几个实例。

如何启动个实例?

如下图,想要启动哪个服务的多实例,直接右击该服务。

右击之后,点击Copy Configuration选项。

点击之后,会出现如下弹窗,首先将Name改换成自己想要的,我一般会改一下端口号,表示我复制实例之后的端口号,然后点击Modify options。

点击之后,会出现一堆选项,然后点击Add VM options。

点击之后,会在原来的弹框中多出现一行内容,然后配置自己的信息即可。我一般不会修改其他内容,只会把端口号改变,这样不会造成端口冲突。

再次重启商品服务,由于之前我已经启动了三个实例,因此在Eureka服务列表中会出现四个实例,如下图:

如下图,在启动订单服务之后,也表示商品服务现在有四个实例,因此启动成功。

负载均衡介绍

概念

负载均衡 Load Balance,简称LB:负载就是压力、流量,均衡就是公平的、合理的将流量进行分配。负载均衡是高并发、高可用必不可少的关键组件。

当服务流量增大时,通常会通过增加机器数量的方式来进行扩容,负载均衡就是按照一定的算法把客户端的请求发送到这些机器中,完成流量分发。

分类

服务端负载均衡

在服务端进行负载均衡的算法分配,比较有名的服务端负载均衡器是Nginx。客户端发送请求,请求首先到达负载均衡器,然后负载均衡器通过负载均衡算法,在多个服务器之间选择一个进行调用。

客户端负载均衡

在客户端进行负载均衡的算法分配。把负载均衡的功能以库的方式集中到客户端,而不是再由一台指定的负载均衡设备集中提供,比如SpringCloud的LoadBalancer,请求到达客户端,客户端从Eureka中拉取服务列表。然后在发送请求到服务器之前,客户端自己通过负载均衡选择一个服务器,进行访问。

对于客户端和服务器来说,并不是服务器就一直是服务器,客户端一直就是客户端。对于上述来说,客户端请求到达服务端,但是服务端又要调用另一个服务端。因此这个中间的服务端也可以称作客户端,如果针对前端请求来说,他就是服务端;如果对于被调用方来说,他就是客户端。因此,不要太纠结这个东西。

SpringCloudLoadBalancer

在早期,负载均衡是SpringCloud的Ribbon来实现的,但是由于它不进行维护了。因此SpringCloud官方又实现了一个SpringCloudLoadBalancer来进行维护使用。作为客户端负载均衡器来说,使用还是比较简单的。

引入依赖

复制代码
        <!--负载均衡-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

修改业务类

复制代码
@Configuration
public class RestTemplateConfig {

    @Bean
    @LoadBalanced // 负载均衡
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

    // 版本一:没有引入微服务组件时
    /*@Resource
    private RestTemplate restTemplate;

    @Override
    public Order getOrderById(Integer id) {
        Order order = this.getById(id);
        String url = "http://127.0.0.1:8001/product/query/" + order.getProductId();
        Product product = this.restTemplate.getForObject(url, Product.class);
        order.setProduct(product);
        return order;
    }*/

    // 版本二:引入了服务注册与发现
    /*private final String PRODUCT_SERVICE_NAME = "cloud-provider-payment8001";

    @Resource
    private RestTemplate restTemplate;

    @Resource
    private DiscoveryClient discoveryClient;

    @Override
    public Order getOrderById(Integer id) {
        // 获取订单
        Order order = this.getById(id);

        // 获取商品服务的实例
        List<ServiceInstance> instances = discoveryClient.getInstances(this.PRODUCT_SERVICE_NAME);
        // 输出日志查看获取到的实例有哪些内容
        log.info(instances.toString());
        // 获取第一个商品实例的uri
        String uri = instances.get(0).getUri().toString();
        log.info(uri);
        // 拼接url
        String url = uri + "/product/query/" + order.getProductId();
        // 远程调用
        Product product = this.restTemplate.getForObject(url, Product.class);
        order.setProduct(product);
        // 返回结果
        return order;
    }*/

    // 版本三:自定义负载均衡版本
    /*private static final String PRODUCT_SERVICE_NAME = "cloud-provider-payment8001";

    @Resource
    private RestTemplate restTemplate;

    @Resource
    private DiscoveryClient discoveryClient;

    private AtomicInteger count = new AtomicInteger(1);

    private List<ServiceInstance> instances;
    @PostConstruct
    private void init() {
        instances = discoveryClient.getInstances(PRODUCT_SERVICE_NAME);
        log.info("查看被调用方实例的数量:" + instances.size());
    }

    @Override
    public Order getOrderById(Integer id) {
        // 获取订单
        Order order = this.getById(id);
        // 获取应该调用的实例下标
        int index = this.count.getAndIncrement() % instances.size();
        // 获取该实例的内容
        ServiceInstance serviceInstance = this.instances.get(index);
        // 拼接URL
        String url = serviceInstance.getUri() + "/product/query/" + order.getProductId();
        log.info("被调用服务的URI:" + serviceInstance.getUri());
        // 远程调用
        Product product = this.restTemplate.getForObject(url, Product.class);
        order.setProduct(product);
        // 返回结果
        return order;
    }*/


    // 版本四:负载均衡版本
    // 不需要自己获取实例,只需要调用服务名就可以自动进行负载均衡获取实例
    @Resource
    private RestTemplate restTemplate;

    private final String PRODUCT_SERVICE_NAME = "cloud-provider-payment8001";

    @Override
    public Order getOrderById(Integer id) {
        // 获取订单
        Order order = this.getById(id);
        // 拼接url
        String url = "http://" + this.PRODUCT_SERVICE_NAME + "/product/query/" + order.getProductId();
        // 远程调用
        Product product = this.restTemplate.getForObject(url, Product.class);
        order.setProduct(product);
        // 返回结果
        return order;
    }

}

测试服务

启动服务之后,输入URL:127.0.0.1:80/order/query/80,多点击几次,就会发现订单服务在进行调用时,会调用不同实例的商品服务,从而证明实现了负载均衡器。

负载均衡策略

策略概述

负载均衡是一种思想,无论是哪种负载均衡器,他们的负载均衡策略都是相似的。SpringCloudBalancer支持两种负载均衡策略:轮询策略和随机选择策略。

轮询策略(Round-Robin):表示服务器轮流处理客户端发来的请求,这是一种最简单的、也是最常用的负载均衡策略。例如学生值日轮流打扫卫生。

随机选择(Random):表示随机一个服务器来执行客户端发来的请求。

自定义策略:SpringCloud默认负载均衡策略是轮询策略,实现是RoundRobinLoadBalancer;如果服务的消费者想要采用随机的负载均衡策略,实现是RandomLoadBalancer;当然,也可以自己实现一个负载均衡策略,例如非公平负载均衡策略(权重)。

  1. 定义随机算法对象,通过@Bean将其加载到Spring容器中。

    // 该类需要满足:不用@Configuration注释、在组件扫描范围内

    public class CustomLoadBalancerConfig {

    复制代码
     @Bean
     ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
                                                             LoadBalancerClientFactory loadBalancerClientFactory) {
         String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
         return new RandomLoadBalancer(loadBalancerClientFactory
                 .getLazyProvider(name, ServiceInstanceListSupplier.class),
                 name);
     }

    }

  2. 添加注解

    @Configuration
    // 添加@LoadBalancerClient注解
    // 可以对不同的服务提供⽅配置不同的客⼾端负载均衡算法策略
    // name:该负载均衡策略对哪个服务生效(服务提供方)
    // configuration:该负载均衡策略用哪个负载均衡策略实现
    @LoadBalancerClient(name = "cloud-consumer-order80", configuration = CustomLoadBalancerConfig.class)
    public class RestTemplateConfig {

    复制代码
     @Bean
     @LoadBalanced
     public RestTemplate restTemplate() {
         return new RestTemplate();
     }

    }

实现原理

SpringCloudLoadBalancer的实现,主要是依赖于LoadBalancerInterceptor来实现的。顾名思义,这是一个拦截器,会拦截RestTemplate远程调用的请求,下述内容具体看源码进行解析:

下述图片是LoadBalancerInteceptor拦截器中的一个类,就算用来进行拦截远程调用请求的。可以看到,在这个类中,第一行代码获取了服务的URI,即http://cloud-provider-product8001。第二行代码是获取URI中的主机,即cloud-provider-product8001。稍后判断一下名字是否存在,存在的话就进去进行负载均衡处理。

点击excute方法之后,先进去一个接口,然后再找到接口的实现类,即如下类。该类中会做一系列的处理,然后又进去一个choose方法。

在chosse方法中,是根据服务名和负载均衡策略,选择处理的服务。首先,会根据服务名找到实现的负载均衡策略,然后根据负载均衡算法,在列表中选择一个服务实例。

对于下面这个方法,再点进去对应的choose方法。

在choose方法点进去之后,就是一个接口,接口对应的实现类则是具体的负载均衡策略,对实现的负载均衡策略感兴趣的可以点进去看看。

相关推荐
蝎子莱莱爱打怪2 天前
XZLL-IM干货系列 04|Netty 长连接实战:Pipeline 怎么排、心跳怎么跳、连接怎么管
后端·微服务·面试
SamDeepThinking3 天前
Java微服务练习方式
java·后端·微服务
米丘6 天前
微前端之 Web Components 完全指南
微服务·html
霸道流氓气质9 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
霸道流氓气质9 天前
Spring Boot 微服务性能优化完全指南
spring boot·微服务·性能优化
地瓜伯伯9 天前
从MESI缓存一致性协议讲透synchronized的底层
java·spring boot·spring·spring cloud·微服务·springcloud
Devin~Y9 天前
大厂 Java 面试实录:从音视频内容社区到 AI RAG 的全链路技术设计
java·spring boot·redis·spring cloud·微服务·kafka·音视频
递归尽头是星辰9 天前
AI 访问数据仓库:从直连到微服务化
数据仓库·人工智能·微服务·dataagent·ai数据治理
就改了10 天前
Windows 环境 SkyWalking 完整实操教程
windows·微服务·skywalking
至乐活着10 天前
Docker Compose多服务编排实战:从零搭建Node.js+MySQL+Redis全栈应用
docker·微服务·devops·容器编排·compose