面试系统架构

SpringCloud第三章:客户端负载均衡与服务调用

2022-06-01  本文已影响0人  Heitaokei

正常本章节应该讲解Netflix Ribbon,由于从Netflix Eureka Client 3.0版本开始内置Ribbon实现机制,不需要单独依赖Ribbon,如果引入Ribbon会报错 java.lang.IllegalStateException: No instances available for XXXXXX,并且想要修改或自定义负载策略也没有IRule接口可以实现,后面我会讲解到这些内容。

学习内容

负载均衡

在任何一个系统中,负载均衡都是一个十分重要且不得不去实施的内容,它是系统处理高并发、缓解网络压力和服务端扩容的重要手段之一。
负载均衡(Load Balance) ,简单点说就是将用户的请求平摊分配到多个服务器上运行,以达到扩展服务器带宽、增强数据处理能力、增加吞吐量、提高网络的可用性和灵活性的目的。
常用负载均衡有两种:

  1. 服务端负载均衡
  2. 客户端负载均衡

服务端负载均衡

服务端负载均衡在微服务没有流行起来之前是最常见的负载均衡方式,其工作原理如下图。


Nginx负载均衡

服务端负载均衡是在客户端和服务端之间建立一个独立的负载均衡服务器,该服务器既可以是硬件设备(例如 F5),也可以是软件(例如 Nginx)。这个负载均衡服务器维护了一份可用服务端清单,然后通过心跳机制来删除故障的服务端节点,以保证清单中的所有服务节点都是可以正常访问的。
当客户端发送请求时,该请求不会直接发送到服务端进行处理,而是全部交给负载均衡服务器,由负载均衡服务器按照某种算法(例如轮询、随机等),从其维护的可用服务清单中选择一个服务端,然后进行转发。

服务端负载均衡具有以下特点:

客户端负载均衡

客户端负载均衡是随着微服务发展而流行起来的,工作原理如下图:


客户端负载均衡

客户端负载均衡是将负载均衡逻辑以代码的形式封装到客户端上,即负载均衡器位于客户端。客户端通过服务注册中心(例如 Eureka Server)获取到一份服务端提供的可用服务清单。有了服务清单后,负载均衡器会在客户端发送请求前通过负载均衡算法选择一个服务端实例再进行访问,以达到负载均衡的目的;
客户端负载均衡也需要心跳机制去维护服务端清单的有效性,这个过程需要配合服务注册中心一起完成。

客户端负载均衡具有以下特点:

Eureka Client(Ribbon) 就是一个基于 HTTP 和 TCP 的客户端负载均衡器,Eureka Client 会从 Eureka Server(服务注册中心)中获取服务端列表,然后通过负载均衡策略将请求分摊给多个服务提供者,从而达到负载均衡的目的。

Netflix Eureka Client 实现服务调用

Eureka Client与 RestTemplate(Rest 模板)配合使用,以实现微服务之间的调用。

RestTemplate 是 Spring 家族中的一个用于消费第三方 REST 服务的请求框架。RestTemplate 实现了对 HTTP 请求的封装,提供了一套模板化的服务调用方法。通过它,Spring 应用可以很方便地对各种类型的 HTTP 请求进行访问。

RestTemplate 针对各种类型的 HTTP 请求都提供了相应的方法进行处理,例如 HEAD、GET、POST、PUT、DELETE 等类型的 HTTP 请求,分别对应 RestTemplate 中的 headForHeaders()、getForObject()、postForObject()、put() 以及 delete() 方法。

废话不多说我们上代码

  1. 在主工程 SpringCloud 下,创建一个名为 springcloud-consumer-user-80 的微服务,并在其 pom.xml 中引入所需的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>SpringCloud</artifactId>
        <groupId>com.heitaokei</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>springcloud-consumer-user-80</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入 Eureka Client 的依赖,将服务注册到 Eureka Server-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- Spring Boot 监控模块-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <artifactId>springcloud-api</artifactId>
            <groupId>com.heitaokei</groupId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 在resource下,创建配置文件 application.yml,配置内容如下
server:
  port: 80
eureka:
  client:
    register-with-eureka: false #本微服务为服务消费者,不需要将自己注册到服务注册中心
    fetch-registry: true  #本微服务为服务消费者,需要到服务注册中心搜索服务
    service-url:
      defaultZone: http://localhost:7001/eureka/
  1. 在 com.heitaokei 包下,创建一个名为 SpringCloudConsumer_80的启动类,并使用 @EnableEurekaClient开启Eureka客户端功能
package com.heitaokei;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class SpringCloudConsumer_80 {
    public static void main(String[] args) {
        SpringApplication.run(SpringCloudConsumer_80.class, args);
    }
}
  1. 在 com.heitaokei.config 包下,创建一个名为 ConfigBean 的配置类,将 RestTemplate 注入到容器中
package com.heitaokei.config;

import com.heitaokei.MyLoadBalancerConfiguration;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ConfigBean {
    @Bean
    @LoadBalanced // 开启负载均衡
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
  1. 在 com.heitaokei.controller 包下,创建一个名为 UserConsumerController 的 Controller,该 Controller 中定义的请求用于调用服务端提供的服务
package com.heitaokei.controller;

import com.heitaokei.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * 用户消费者
 */
@RestController
public class UserConsumerController {
    //面向微服务编程,即通过微服务的名称来获取调用地址 使用注册到 Spring Cloud Eureka 服务注册中心中的服务,*即服务提供者中 application.name*
    private static final String PROVIDER_URL_PREFIX = "http://PROVIDER-USER";
    @Autowired
    private RestTemplate restTemplate; // 注册ConfigBean配置的负载均衡bean

    @GetMapping(value = "/user")
    public User getUser() {
        return restTemplate.getForObject(PROVIDER_URL_PREFIX + "/user", User.class);
    }
}

由于要测试客户端负载均衡所以把springcloud-provider-user-8001项目中配置文件application.name修改成provider-user,具体如下图所示


服务提供者的应用名
Eureka注册中心 服务器提供者应用名
  1. 依次启动服务注册中心 springcloud-eureka-7001、服务提供者 springcloud-provider-user-8001 以及服务消费者 springcloud-consumer-user-80
  2. 使用浏览器访问 http://localhost/user,结果如下图,能够正常返回结果表示服务调用成功
    服务调用接口

Netflix Eureka Client 实现负载均衡

Eureka Client 内置了客户端的负载均衡器,能够轻松地实现客户端的负载均衡。Eureka Client会先从 Eureka Server(服务注册中心)去获取服务端列表,然后通过负载均衡策略将请求分摊给多个服务端,从而达到负载均衡的目的
Ribbon 提供了一个 IRule 接口,但是Eureka Client是没有这个接口的,Eureka Client负载策略接口为ReactorServiceInstanceLoadBalancer,它只内置两种负载策略,轮询策略(默认)、随机策略


内置两种策略

下面通过实例介绍一下客户端负载的应用

  1. 参考 springcloud-provider-user-8001,再创建两个微服务 Moudle :springcloud-provider-user-8002 和 springcloud-provider-user-8003
  2. 在 springcloud-provider-user-8002 中 application.yml 中,修改端口号
server:
  port: 8002
spring:
  application:
    name: provider-user #对外暴漏的微服务名称 不做修改,与springcloud-provider-user-8001一致
  1. 在 springcloud-provider-user-8002 中修改UserController中getUser返回值,方便后面区分调用的是哪个服务
package com.springcloud.controller;
import com.heitaokei.entity.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @GetMapping(value = "/user")
    public User getUser() {
        User user = new User();
        user.setUserName("用户222222");
        user.setAge(22);
        user.setAddress("火星");
        return user;
    }
}
  1. 在 springcloud-provider-user-8003 中 application.yml 中,修改端口号
server:
  port: 8003
spring:
  application:
    name: provider-user #对外暴漏的微服务名称 不做修改,与springcloud-provider-user-8001一致
  1. 在 springcloud-provider-user-8003 中修改UserController中getUser返回值,方便后面区分调用的是哪个服务
package com.springcloud.controller;
import com.heitaokei.entity.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @GetMapping(value = "/user")
    public User getUser() {
        User user = new User();
        user.setUserName("用户333");
        user.setAge(33);
        user.setAddress("地球村");
        return user;
    }
}
  1. 依次启动 springcloud-eureka-7001/7002/7003(服务注册中心集群,也可只启动一个,我就只启动一个因为比较吃内存,如果只启动一个可以把Eureka注册地址改成一个即可)、springcloud-provider-user-8001/8002/8003(服务提供者集群)以及 springcloud-consumer-user-80(服务消费者)
    6.使用浏览器访问 http://localhost/user,返回结果后继续刷新该请求,会发现每次返回的结果是从不同的服务提供者返回的,记录一下返回结果即可发现返回结果出现的顺序一直是一致的,例如:3-1-2-3-1-2-3-1-2-3-1所以我们确定Eureka Client 默认负载均衡策略为轮询

怎么修改负载策略

如果是Ribbon使用内置策略只需要重新注入IRule即可,但是Eureka Client确不是这样的,Eureka Client需要在启动类同级创建一个配置类

  1. 我们把策略改为随机,在springcloud-consumer-user-80模块的com.heitaokei包下创建一个MyLoadBalancerConfiguration类
package com.heitaokei;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

/**
 * 自定义负载配置
 */
public class MyLoadBalancerConfiguration {
    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
                                                            LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        // 随机
        return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier
                .class), name);
    }
}
  1. 在启动类SpringCloudConsumer_80 加上注解 @LoadBalancerClients,代码如下
@SpringBootApplication
@EnableEurekaClient
@LoadBalancerClients(defaultConfiguration = MyLoadBalancerConfiguration.class)
public class SpringCloudConsumer_80 {
    public static void main(String[] args) {
        SpringApplication.run(SpringCloudConsumer_80.class, args);
    }
}
  1. 重新启动springcloud-consumer-user-80,浏览器访问 http://localhost/user,返回结果后继续刷新该请求,我们会发现返回结果是没有规律的,证明我们修改为随机的负载均衡策略生效了

怎么自定义负载均衡策略

自定义策略我们只需要自己创建一个ReactorServiceInstanceLoadBalancer接口的实现类,实现choose方法即可,具体实现的算法需要根据具体需求来确定,一般默认就够了,所以这里就不演示实现算法了,因为实现方式已经知道了,具体实现就不操作了

上一篇下一篇

猜你喜欢

热点阅读