目录

spirngcloud负载均衡ribbon

概述

随着微服务都成功注册到 Eureka Server 中(此处包括 服务提供方(集群模式)服务消费者),此时接收到一个请求过来,通过 Eureka 服务名的方式进行访问,在调用 服务提供方 时,发现它是 集群模式 有多个服务可用,那此时到底该调用哪个服务进行数据返回?

此时就用到了 负载均衡 的概念。接下来我们就来介绍Spring Cloud 中的负载均衡机制。

负载均衡介绍

负载均衡,简单的说就是将用户的请求平摊分配到多个服务,从而达到系统的 HA(高可用)。日常中常用的负载均衡有:软件 Nginx、LVS;硬件 F5 等。

负载均衡,又分为两种:集中式负载均衡进程内负载均衡

集中式(服务端)负载均衡

即在服务的消费方和提供方之间,使用独立的负载均衡设施(可以是硬件,如 :F5;也可以是软件,如:Nginx)。由该设施负责把访问请求通过某种策略转发至服务的提供方。

进程内(客户端)负载均衡

即将负载均衡逻辑集成到消费方,消费方从服务注册中心获知有哪些服务地址可用,然后自己再从这些地址中选择出一个合适的服务器进行调用。Ribbon 就属于进程内负载均衡 ,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

两种负载均衡对比图解

http://img.cana.space/picStore/20201108153601.png

Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别

Nginx是服务器负载均衡,客户端所有请求都会交给Nginx,然后由Nginx实现转发请求。即负载均衡是由服务端实现的。

Ribbon本地负载均衡,在调用微服务接口的时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。

Ribbon是什么

Spring Cloud Ribbon是一个基于HTTP和TCP的 客户端 负载均衡 工具 。

简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。

官网资料

Ribbon官网

Ribbon目前也进入了维护模式,但是现在一时半会还替换不了,生产环境中有大量使用Ribbon。spring cloud负载均衡方案未来趋势是使用LoadBalancer

http://img.cana.space/picStore/20201108153925.png

前面演示Eureka使用的时候有介绍负载均衡,其实引入的spring-cloud-starter-netflix-eureka-client已经依赖了spring-cloud-starter-netflix-ribbon,所以我们才可能负载均衡式地进行访问。

Ribbon负载均衡演示

架构说明

Ribbon 其实就是一个软负载均衡的客户端组件。它可以和其他所需请求的客户端结合使用,和 Eureka 的结合只是其中的一个实例。

http://img.cana.space/picStore/20201109120540.png

Ribbon在工作时分成两步

  1. 选择EurekaServrr,它优先选择同一个区域内负载较少的server
  2. 根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。其中ribbon提供了多种策略,比如轮询、随机和根据响应时间加权。

pom

eureka-client已经包含了ribbon的依赖,无需再单独添加

RestTemplate

RestTemplate是 Spring 提供的用于访问 Res t服务的客户端,它提供了多种便捷访问远程 Http 服务的方法,能够大大提高客户端的编写效率。对比于之前我们使用的 HttpClient来说,它的底层还是基于 HttpClient 进行封装,而且封装的还不错,使用起来非常的方便。

RestTemplate 类官方介绍: RestTemplate 类官方介绍。在我们的日常开发中,常用到的也就是getForObject()/getForEntity()postForObject()/postForEntity()` 这几个方法。

xxxForObject 和 xxxForEntity 的区别

getForObject(): 返回对象为响应体中数据转换成的对象,基本上可以理解为是JSON

getForEntity(): 返回对象为 ResponseEntity 对象,包含了响应中的一些重要信息,比如 响应头响应状态码响应体 等。

推荐使用 xxxForObjec()。如果你需要一些请求的详细信息,那还是使用 xxxForEntity() 吧

使用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 使用 getForObject 直接返回结果对象
 */
@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPayment(@PathVariable("id") Long id) {
    return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
}

/**
 * 使用 getForEntity 返回的ResponseEntity对象,数据部分还需要通过 .getBody() 获取
 */
@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id){
    ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
    if (entity.getStatusCode().is2xxSuccessful()) {
        log.info(entity.getStatusCode() + "\t" + entity.getHeaders());
        return entity.getBody();
    }else{
        return new CommonResult<>(444, "操作失败");
    }
}

Ribbon核心组件IRule

http://img.cana.space/picStore/20201109122923.png

根据特定算法从服务列表中选取一个要访问的服务

http://img.cana.space/picStore/20201109130306.png

如何替换

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

http://img.cana.space/picStore/20201109133131.png

  1. 修改cloud-consumer-order80

  2. 新建package

    20201109135704

  3. 新建负载均衡规则类,需要使用@Configuration注解标注类,可以使用@Bean注入一个规则类

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    package com.eh.cloud2020.balrule;
       
    import com.netflix.loadbalancer.RandomRule;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
       
    @Configuration
    public class EhBalanceRule {
        @Bean
        public RandomRule randomRule() {
            return new RandomRule();
        }
    }
    
  4. 主启动类上添加注解

    1
    2
    3
    4
    5
    6
    7
    8
    
    @EnableEurekaClient
    @SpringBootApplication
    @RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = EhBalanceRule.class)
    public class OrderMain80 {
        public static void main(String[] args) {
            SpringApplication.run(OrderMain80.class, args);
        }
    }
    

    name 填写 服务提供者资源名称,configuration填写自定义规则类

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    @Configuration(proxyBeanMethods = false)
    @Import(RibbonClientConfigurationRegistrar.class)
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RibbonClient {
       
       /**
        * Synonym for name (the name of the client).
        *
        * @see #name()
        * @return name of the Ribbon client
        */
       String value() default "";
       
       /**
        * The name of the ribbon client, uniquely identifying a set of client resources,
        * including a load balancer.
        * @return name of the Ribbon client
        */
       String name() default "";
       
       /**
        * A custom <code>@Configuration</code> for the ribbon client. Can contain override
        * <code>@Bean</code> definition for the pieces that make up the client, for instance
        * {@link ILoadBalancer}, {@link ServerListFilter}, {@link IRule}.
        *
        * @see RibbonClientConfiguration for the defaults
        * @return the custom Ribbon client configuration
        */
       Class<?>[] configuration() default {};
       
    }
    
  5. 测试

    多次访问 http://localhost/order/payment/1 查看端口号变化

    http://img.cana.space/picStore/20201109135904.png

Ribbon负载均衡算法

算法流程

RoundRobin算法:实际调用服务器地址下标 = rest接口第几次请求数 % 服务器集群总数量,每次服务重启后rest接口计数重置为1。

源码

com.netflix.loadbalancer.RoundRobinRule#choose(com.netflix.loadbalancer.ILoadBalancer, java.lang.Object)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
private AtomicInteger nextServerCyclicCounter;

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

    Server server = null;
    int count = 0;
    while (server == null && count++ < 10) {
      // 获取可用集群中机器数量,demo里面是2
        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;
        }

      /* 自旋获取下一个服务器地址下标
      	private int incrementAndGetModulo(int modulo) {
        for (;;) {
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
	    }
      */
        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;
}

自定义负载均衡算法

步骤:使用discoveryClient获取所有ServiceInstance,从中使用自定义算法选择一个ServiceInstance,调用getUri()获得服务地址,再进行方法调用

  1. 为了演示效果,先去掉com.eh.cloud2020.order.config.ApplicationContextConfig#restTemplate去掉注解@LoadBalanced

  2. 自定义负载均衡类

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    package com.eh.cloud2020.order.config;
       
    import org.springframework.cloud.client.ServiceInstance;
    import org.springframework.stereotype.Component;
       
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;
       
    @Component
    public class MyLoadBalancer {
        // 下一个服务器地址下标
        private AtomicInteger nextServerCyclicCounter = new AtomicInteger(0);
       
        public ServiceInstance getInstance(List<ServiceInstance> instances) {
            if (instances == null || instances.size() < 1) {
                return null;
            }
            return instances.get(nextServerCyclicCounter.getAndIncrement() % instances.size());
        }
    }
    
  3. 改造OrderController

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    private final RestTemplate restTemplate;
    private final MyLoadBalancer myLoadBalancer;
    private final DiscoveryClient discoveryClient;
       
    public OrderController(
            RestTemplate restTemplate,
            MyLoadBalancer myLoadBalancer,
            DiscoveryClient discoveryClient
    ) {
        this.restTemplate = restTemplate;
        this.myLoadBalancer = myLoadBalancer;
        this.discoveryClient = discoveryClient;
    }
       
    @GetMapping("/order/payment/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
      // 使用discoveryClient显示地调用指定地址上服务器的服务
        ServiceInstance serviceInstance = myLoadBalancer.getInstance(discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE"));
        if (serviceInstance == null) {
            return null;
        }
        return restTemplate.getForObject(serviceInstance.getUri() + "/payment/" + id, CommonResult.class, id);
    }
    
  4. 测试

    http://localhost/order/payment/1