认识微服务(四)之 负载均衡 Ribbon


在上一个案例 认识微服务(三)之 Eureka 注册中心 中,我们启动了一个user-service,然后通过 DiscoveryClient 来获取服务实例信息,然后获取 ip 和端口来访问。
但是实际环境中,我们往往会开启很多个 user-service 的集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢?

一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。
不过 Eureka 中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。

什么是 Ribbon:
Ribbon 是 Netflix 发布的负载均衡器,它有助于控制 HTTP 和 TCP 客户端的行为。为 Ribbon 配置服务提供者地址列表后,Ribbon 就可基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon 默认为我们提供了很多的负载均衡算法,例如轮询、随机等。当然,我们也可为 Ribbon 实现自定义的负载均衡算法。

接下来,我们就来使用 Ribbon 实现负载均衡。

1 什么是负载均衡

(1)Load balancing,即负载均衡,是一种计算机技术,用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到最优化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的。
(2)负载均衡(Load Balance),意思是将负载(工作任务,访问请求)进行平衡、分摊到多个操作单元(服务器,组件)上进行执行。是解决高性能,单点故障(高可用),扩展性(水平伸缩)的终极解决方案。

2 启动两个服务实例

首先我们启动两个 user-service 实例,一个8081,一个8083。
在这里插入图片描述
运行注册中心,服务地址: localhost:10087
在这里插入图片描述

3 开启负载均衡

(1)consumer-service 子项目中添加 maven 依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>

因为 Eureka 中已经集成了 Ribbon,所以我们无需引入新的依赖。直接修改代码:
在 consumer-service 子项目启动类中 RestTemplate 的配置方法上添加 @LoadBalanced 注解:

package com.zc.sc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

/**
 * @作者: zc
 * @时间: 2021/2/3 11:07
 * @描述: 启动类,注册 spring RestTemplate
 */

@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }

/*
    //使用 HttpClient 或者 urlConnection 进行远程访问
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
*/

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        // 这次我们使用了OkHttp客户端,只需要注入工厂即可
        return new RestTemplate();
    }
}

注:新版本不用传入new RestTemplate(new OkHttp3ClientHttpRequestFactory())可不传

修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:

第一种 实现(麻烦):

@RestController
@RequestMapping("consumer5")
public class ConsumerController5 {
    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private LoadBalancerClient client;

    @GetMapping("{id}")
    public User queryById(@PathVariable("id") Long id) {
		//基于ribbon提供的负载均衡器来实现请求
        ServiceInstance instance = client.choose("user-service");
        String host = instance.getHost();
        int port = instance.getPort();
        String url = "http://" + host + ":" + port + "/user/" + id;
        return restTemplate.getForObject(url, User.class);
    }
}

第二种实现:通过访问服务名称。
最终版本:需要在 ConsumerApplicatioin 启动类中添加 @LoadBalanced(上图代码)

package com.zc.sc.dao;

import com.zc.sc.pojo.ConsumerUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.util.List;

/**
 * @作者: zc
 * @时间: 2021/2/3 11:14
 * @描述: 通过 RestTemplate 远程查询 user-service 中的接口
 */
@Component
public class ConsumerUserDao {

    @Autowired
    private RestTemplate restTemplate;

//    @Autowired
//    DiscoveryClient discoveryClient;  // spring包下的类

    public ConsumerUser queryUserByIdOne(Long id) {
//        List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
//        ServiceInstance instance = instances.get(0);
//        String host = instance.getHost();
//        int port = instance.getPort();
//        return restTemplate.getForObject("http://"+host+":"+port+"/us/user/"+id,ConsumerUser.class);

        return restTemplate.getForObject("http://user-service/us/user/"+id, ConsumerUser.class);
    }
}

访问页面 http://localhost:8082/cs/consumer/1,查看结果:
在这里插入图片描述
完美!

4 源码跟踪

为什么我们只输入了 service 名称就可以访问了呢?之前还要获取ip和端口。
显然有人帮我们根据 service 名称,获取到了服务实例的 ip 和端口。它就是 LoadBalancerInterceptor
我们进行源码跟踪(IDEA 搜索类快捷键:Ctrl + N):
在这里插入图片描述
继续跟入execute方法:发现获取了端口的服务
在这里插入图片描述

5 负载均衡策略

Ribbon 默认的负载均衡策略是简单的轮询,我们可以测试一下:
编写测试类,在刚才的源码中我们看到拦截中是使用 RibbonLoadBalanceClient 来进行负载均衡的,其中有一个 choose 方法,是这样介绍的:

T execute(){
    //.....
    Server server = getServer(loadBalancer, hint);
    //......
}

在这里插入图片描述
现在这个就是负载均衡获取实例的方法。

添加 maven 依赖:

<dependency>
     <groupId>junit</groupId>
     <artifactId>junit</artifactId>
     <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

我们对注入这个类的对象,然后对其测试:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ConsumerApplication.class)
public class LoadBalanceTest {

    @Autowired
    RibbonLoadBalancerClient client;

    @Test
    public void test(){
        for (int i = 0; i < 100; i++) {
            ServiceInstance instance = this.client.choose("user-service");
            System.out.println(instance.getHost() + ":" + instance.getPort());
        }
    }
}

结果:
在这里插入图片描述
符合了我们的预期推测,确实是轮询方式。

我们是否可以修改负载均衡的策略呢?
继续跟踪源码,发现这么一段代码:
在这里插入图片描述
我们看看这个 rule 是谁:
在这里插入图片描述
这里的 rule 默认值是一个 RoundRobinRule,看类的介绍:
在这里插入图片描述
这不就是轮询的意思嘛。

我们注意到,这个类其实是实现了接口 IRule 的,查看一下:
在这里插入图片描述
定义负载均衡的规则接口。
它有以下实现:
在这里插入图片描述

SpringBoot也帮我们提供了修改负载均衡规则的配置入口:

user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

格式是:{服务名称}.ribbon.NFLoadBalancerRuleClassName,值就是 IRule 的实现类。

6 重试机制(了解)

Eureka 的服务治理强调了 CAP 原则中的 AP,即可用性和可靠性。它与 Zookeeper 这一类强调 CP(一致性,可靠性)的服务治理框架最大的区别在于:Eureka 为了实现更高的服务可用性,牺牲了一定的一致性,极端情况下它宁愿接收故障实例也不愿丢掉健康实例,正如我们上面所说的自我保护机制。
但是,此时如果我们调用了这些不正常的服务,调用就会失败,从而导致其它服务不能正常工作!这显然不是我们愿意看到的。

我们现在关闭一个 user-service 实例(CloudStudyApplication2 停止运行)。

因为服务剔除的延迟,consumer 并不会立即得到最新的服务列表,此时再次访问你会得到错误提示:
在这里插入图片描述
但是此时,8081 服务其实是正常的。
因此Spring Cloud 整合了Spring Retry 来增强 RestTemplate 的重试能力,当一次服务调用失败后,不会立即抛出一次,而是再次重试另一个服务。

只需要简单配置即可实现 Ribbon 的重试:

spring:
  cloud:
    loadbalancer:
      retry:
        enabled: true # 开启Spring Cloud的重试功能
user-service:
  ribbon:
    ConnectTimeout: 250 # Ribbon的连接超时时间
    ReadTimeout: 1000 # Ribbon的数据读取超时时间
    OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
    MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
    MaxAutoRetries: 1 # 对当前实例的重试次数

根据如上配置,当访问到某个服务超时后,它会再次尝试访问下一个服务实例,如果不行就再换一个实例,如果不行,则返回失败。切换次数取决于 MaxAutoRetriesNextServer 参数的值

引入spring-retry依赖:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

我们重启 consumer-demo,测试,发现即使 user-service2 宕机,也能通过另一台服务实例获取到结果!

7 项目地址

github项目更新

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值