Spring Cloud Alibaba之负载均衡组件 - Ribbon详解(三)

2022-04-13 16:54:42 浏览数 (1)

Ribbon是Netflix公司开源的一个负载均衡的项目(https://github.com/Netflix/ribbon),它是一个基于HTTP、TCP的客户端负载均衡器。

服务端负载均衡

负载均衡是微服务架构中必须使用的技术,通过负载均衡来实现系统的高可用、集群扩容等功能。负载均衡可通过硬件设备及软件来实现,硬件比如:F5、Array等,软件比如:LVS、Nginx等。

用户请求先到达负载均衡器(也相当于一个服务),负载均衡器根据负载均衡算法将请求转发到微服务。负载均衡算法有:轮训、随机、加权轮训、加权随机、地址哈希等方法,负载均衡器维护一份服务列表,根据负载均衡算法将请求转发到相应的微服务上,所以负载均衡可以为微服务集群分担请求,降低系统的压力

客户端负载均衡

上图是服务端负载均衡,客户端负载均衡与服务端负载均衡的区别在于客户端要维护一份服务列表,Ribbon从Eureka Server获取服务列表,Ribbon根据负载均衡算法直接请求到具体的微服务,中间省去了负载均衡服务。

Ribbon负载均衡的流程图:

  • 在消费微服务中使用Ribbon实现负载均衡,Ribbon先从Eureka Server 或 Nacos Server中获取服务列表。
  • Ribbon根据负载均衡的算法去调用微服务。

Ribbon测试

Spring Cloud引入Ribbon配合 restTemplate 实现客户端负载均衡。Java中远程调用的技术有很多,如:webservice、socket、rmi、Apache HttpClient、OkHttp等。

  1. 在客户端添加Ribbon依赖

注意:由于我们之前整合Nacos时引入了spring-cloud-starter-alibaba-nacos-discovery这个依赖包,而这个包默认已经帮我们继承了Ribbon,所有这里可以不用单独引入Ribbon依赖包。

  1. 配置Ribbon参数
代码语言:javascript复制
ribbon:
  MaxAutoRetries: 2  #最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试
  MaxAutoRetriesNextServer: 3  #切换实例的重试次数
  OkToRetryOnAllOperations: false  #对所有操作请求都进行重试,如果是get则可以,如果是post,put等操作没有实现幂等的情况下是很危险的,所以设置为false
  ConnectTimeout: 5000 #请求连接的超时时间
  ReadTimeout: 6000  #请求处理的超时时间
  1. 负载均衡测试

启动两个服务,端口需要不一致

定义RestTemplate,使用@LoadBalanced注解

代码语言:javascript复制
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

测试代码

代码语言:javascript复制
@Slf4j
@RestController
public class TestController {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/test")
    public String test() {
        String result = restTemplate.getForObject("http://alibaba-nacos-discovery-server/hello?name=wolf", String.class);
        return "Return : "   result;
    }
}

可以看到,在定义RestTemplate时候,增加了@LoadBalanced注解,而在真正调用服务接口的时候,原来host部分是通过手工拼接ip和端口形式。而这里直接采用服务名来写请求路径即可。在真正调用的时候,Spring Cloud会将请求拦截下来,然后通过Ribbon从Nacos Server获取服务列表,并通过负载均衡器选出节点,并替换服务名部分为具体的ip和端口,交给RestTemplate去请求,从而实现基于服务名的负载均衡调用。

Ribbon饥饿加载

默认情况下Ribbon是懒加载的。当服务起动好之后,第一次请求是非常慢的,第二次之后就快很多。

解决方式:开启饥饿加载

代码语言:javascript复制
ribbon:
 eager-load:
  enabled: true #开启饥饿加载
  clients: server-1,server-2,server-3 #为哪些服务的名称开启饥饿加载,多个用逗号分隔

Ribbon组件

接口

作用

默认值

IclientConfig

读取配置

DefaultClientConfigImpl

IRule

负载均衡规则,选择实例

ZoneAvoidanceRule

IPing

筛选掉ping不通的实例

DummyPing(该类什么不干,认为每个实例都可用,都能ping通)

ServerList<Server>

交给Ribbon的实例列表

Ribbon:ConfigurationBasedServerList Spring Cloud Alibaba:NacosServerList

ServerListFilter<Server>

过滤掉不符合条件的实例

ZonePreferenceServerListFilter

ILoadBalancer

Ribbon的入口

ZoneAwareLoadBalancer

ServerListUpdater

更新交给Ribbon的List的策略

PollingServerListUpdater

这里的每一项都可以自定义: IclientConfig Ribbon支持非常灵活的配置就是有该组件提供的 IRule 为Ribbon提供规则,从而选择实例、该组件是最核心组件

举例:

  • 代码方式:
代码语言:javascript复制
@Configuration
public class RibbonRuleConfig {
    @Bean
    public IRule ribbonRulr() {
        return new RandomRule();
    }
    @Bean
    public IPing iPing(){
        return new PingUrl();
    }
}
  • 配置属性方式:
代码语言:javascript复制
<clientName>:
 ribbon:
  NFLoadBalancerClassName: #ILoadBalancer该接口实现类
  NFLoadBalancerRuleClassName: #IRule该接口实现类
  NFLoadBalancerPingClassName: #Iping该接口实现类
  NIWSServerListClassName: #ServerList该接口实现类
  NIWSServerListFilterClassName: #ServiceListFilter该接口实现类

在这些属性中定义的类优先于使用@RibbonClient(configuration=RibbonConfig.class)Spring 定义的bean 以及由Spring Cloud Netflix提供的默认值。描述:配置文件中定义ribbon优先代码定义

Ribbon负载均衡的八种算法,其中ResponseTimeWeightedRule已废除

规则名称

特点

AvailabilityFilteringRule

过滤掉一直连接失败的被标记为circuit tripped(电路跳闸)的后端Service,并过滤掉那些高并发的后端Server或者使用一个AvailabilityPredicate来包含过滤Server的逻辑,其实就是检查status的记录的各个Server的运行状态

BestAvailableRule

选择一个最小的并发请求的Server,逐个考察Server,如果Server被tripped了,则跳过

RandomRule

随机选择一个Server

ResponseTimeWeightedRule

已废弃,作用同WeightedResponseTimeRule

RetryRule

对选定的负责均衡策略机上充值机制,在一个配置时间段内当选择Server不成功,则一直尝试使用subRule的方式选择一个可用的Server

RoundRobinRule

轮询选择,轮询index,选择index对应位置Server

WeightedResponseTimeRule

根据相应时间加权,相应时间越长,权重越小,被选中的可能性越低

ZoneAvoidanceRule

(默认是这个)负责判断Server所Zone的性能和Server的可用性选择Server,在没有Zone的环境下,类似于轮询(RoundRobinRule)

实现负载均衡<细粒度>配置-随机

  • 方式一:代码方式 首先定义RestTemplate,并且添加注解@LoadBalanced,这样RestTemplate就实现了负载均衡
代码语言:javascript复制
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
//template.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));//解决中文乱码
return new RestTemplate();
}

在SpringBootApplication主类下添加配置类。该类主要作用于为哪个服务做负载均衡。默认的是轮训

代码语言:javascript复制
@Configuration
@RibbonClient(name = "${服务名称}", configuration = GoodsRibbonRuleConfig.class)//configuration: 指向负载均衡规则的配置类
public class GoodsRibbonConfig {
}

添加Ribbon的配置类,注意该类必须配置在@SpringBootApplication主类以外的包下。不然的话所有的服务都会按照这个规则来实现。会被所有的RibbonClient共享。主要是主类的主上下文和Ribbon的子上下文起冲突了。父子上下文不能重叠

代码语言:javascript复制
@Configuration
public class GoodsRibbonRuleConfig {
    @Bean
    public IRule ribbonRulr() {
        return new RandomRule();
    }
}
  • 方式二:配置属性方式
代码语言:javascript复制
server-1: # 服务名称 Service-ID
  ribbon:
    # 属性配置方式【推荐】
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #  配置文件配置负载均衡算法-我这里使用的是自定义的Ribbon的负载均衡算法,默认

优先级:配置(不会影响其他服务)>(大于) 硬编码(类得写在SpringBoot启动类包外,不然会影响其他服务)

总结:

配置方式

优点

缺点

代码配置

基于代码,更加灵活

有坑(父子上下文)线上修改得重新打包,发布

属性配置

易上手

配置更加直观线上修改无需重新打包,发布优先级更高极端场景下没有配置配置方式灵活

实现负载均衡<全局>配置-随机

  • 方式一:Ribbon的配置类定义在主类下 让ComponentScan上下文重叠(强烈不建议使用)
  • 方式二:
代码语言:javascript复制
@Configuration
@RibbonClients(defaultConfiguration = GoodsRibbonRuleConfig.class)//Ribbon负载均衡全局粒度配置(所有服务都按照这个配置)
public class RibbonConfig {
}

扩展Ribbon-支持Nacos权重

默认情况下Ribbon是不支持Nacos的权重负载均衡选择的,这里我们自己扩展一个Rule,然Ribbon支持Nacos的权重规则。

  1. 创建NacosWeightedRule类
代码语言:javascript复制
package com.thtf.contentcenter.configuration;

import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties;
import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer;


@Slf4j
public class NacosWeightedRule extends AbstractLoadBalancerRule {
    
    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;
    
    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        //读取配置文件,并初始化
    }

    @Override
    public Server choose(Object o) {
        try {
            BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
            //想要请求的微服务名称
            String name = loadBalancer.getName();
            //实现负载均衡算法
            //拿到服务发现相关API
            NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
            //nacos client自动通过基于权重的负载均衡算法,给我们选择一个实例。
            Instance instance = namingService.selectOneHealthyInstance(name);
            
            log.info("选择的实例是:port = {}, instance = {}", instance.getPort(), instance);
            return new NacosServer(instance);
        } catch (NacosException e) {
            log.error("选择实例异常:{}", e.getMessage(), e);
            return null;
        }
    }
}
  1. 创建RibbonConfiguration类
代码语言:javascript复制
package com.thtf.contentcenter.ribbonconfiguration;

import com.istimeless.contentcenter.configuration.NacosWeightedRule;
import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RibbonConfiguration {
    
    @Bean
    public IRule ribbonRule() {
        return new NacosWeightedRule();
    }
}

特别注意:RibbonConfiguration要建在启动类扫描不到的地方,如图所示:

  1. 创建UserCenterRibbonConfiguration类,实现全局配置
代码语言:javascript复制
package com.thtf.contentcenter.configuration;

import com.istimeless.ribbonconfiguration.RibbonConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Configuration;

@Configuration
@RibbonClients(defaultConfiguration = RibbonConfiguration.class)
public class UserCenterRibbonConfiguration {
}

扩展Ribbon-同集群优先

在Nacos上,支持集群配置。集群是指对指定微服务的一种虚拟分类。集群还是比较有用的,例如:

  • 为了容灾,把指定微服务同时部署在两个机房(例如同城多活,其中1个机房崩溃了,另一个机房还能顶上,异地多活防止自然灾害)
  • 调用时,可优先调用同机房的实例,如果同机房没有实例,再跨机房调用。

虽然Spring Cloud Alibaba支持集群配置,例如:

代码语言:javascript复制
spring: 
  cloud:    
    nacos:  
      discovery:    
        # 北京机房集群    
        cluster-name: BJ

但在调用时,服务消费者并不会优先调用同集群的实例。 本节来探讨如何扩展Ribbon,从而实现同集群优先调用的效果,并且还能支持Nacos权重配置。关于权重配置,前面已经实现了,在前面的基础上实现同集群优先策略。

代码语言:javascript复制
/** 
 * 支持优先调用同集群实例的ribbon负载均衡规则.    
 *  
 * @author itmuch.com   
 */ 
@Slf4j  
public class NacosRule extends AbstractLoadBalancerRule {   
    @Autowired  
    private NacosDiscoveryProperties nacosDiscoveryProperties;  
    
    @Override   
    public Server choose(Object key) {  
        try {   
            String clusterName = this.nacosDiscoveryProperties.getClusterName();    
            DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer(); 
            String name = loadBalancer.getName();   
    
            NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();    
        // 1. 找到指定服务的所有实例 A
            List<Instance> instances = namingService.selectInstances(name, true);   
            if (CollectionUtils.isEmpty(instances)) {   
                return null;    
            }   
    
            List<Instance> instancesToChoose = instances;   
            if (StringUtils.isNotBlank(clusterName)) {  
                // 2. 过滤出相同集群下的所有实例 B
                List<Instance> sameClusterInstances = instances.stream()    
                        .filter(instance -> Objects.equals(clusterName, instance.getClusterName())) 
                        .collect(Collectors.toList());  
                // 3. 如果B为空,就用A
                if (!CollectionUtils.isEmpty(sameClusterInstances)) {   
                    instancesToChoose = sameClusterInstances;   
                } else {    
                    log.warn("发生跨集群的调用,name = {}, clusterName = {}, instance = {}", name, clusterName, instances);  
                }   
            }   
        // 4. 基于权重的负载均衡算法,返回一个实例
            Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToChoose);   
       
            return new NacosServer(instance);   
        } catch (Exception e) { 
            log.warn("NacosRule发生异常", e);   
            return null;    
        }   
    }   
    
    @Override   
    public void initWithNiwsConfig(IClientConfig iClientConfig) {   
    }   
}

负载均衡算法:

代码语言:javascript复制
// Balancer来自于com.alibaba.nacos.client.naming.core.Balancer,也就是Nacos Client自带的基于权重的负载均衡算法。  
public class ExtendBalancer extends Balancer {  
    /** 
     * 根据权重,随机选择实例  
     *  
     * @param instances 实例列表    
     * @return 选择的实例    
     */ 
    public static Instance getHostByRandomWeight2(List<Instance> instances) {   
        return getHostByRandomWeight(instances);    
    }   
}

配置:

代码语言:javascript复制
microservice-provider-user: 
  ribbon:   
    NFLoadBalancerRuleClassName: com.itmuch.cloud.study.ribbon.NacosClusterAwareWeightedRule

这样,服务在调用microservice-provider-user 这个服务时,就会优先选择相同集群下的实例。

扩展Ribbon-基于元数据的版本控制

至此,已经实现了

  • 优先调用同集群下的实例
  • 实现基于权重配置的负载均衡 但实际项目,我们可能还会有这样的需求: 一个微服务在线上可能多版本共存,例如:
  • 服务提供者有两个版本:v1、v2
  • 服务消费者也有两个版本:v1、v2 v1/v2是不兼容的。服务消费者v1只能调用服务提供者v1;消费者v2只能调用提供者v2。如何实现呢?

下面围绕该场景,实现微服务之间的版本控制。

元数据 元数据就是一堆的描述信息,以map存储。举个例子:

代码语言:javascript复制
spring:
  cloud:
    nacos:
        metadata: 
          # 自己这个实例的版本
          version: v1
          # 允许调用的提供者版本
          target-version: v1

需求分析

我们需要实现的有两点:

  • 优先选择同集群下,符合metadata的实例
  • 如果同集群加没有符合metadata的实例,就选择所有集群下,符合metadata的实例

代码实现

代码语言:javascript复制
@Slf4j
public class NacosFinalRule extends AbstractLoadBalancerRule {
    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;

    @Override
    public Server choose(Object key) {
        // 负载均衡规则:优先选择同集群下,符合metadata的实例
        // 如果没有,就选择所有集群下,符合metadata的实例

        // 1. 查询所有实例 A
        // 2. 筛选元数据匹配的实例 B
        // 3. 筛选出同cluster下元数据匹配的实例 C
        // 4. 如果C为空,就用B
        // 5. 随机选择实例
        try {
            String clusterName = this.nacosDiscoveryProperties.getClusterName();
            String targetVersion = this.nacosDiscoveryProperties.getMetadata().get("target-version");

            DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
            String name = loadBalancer.getName();

            NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();

            // 所有实例
            List<Instance> instances = namingService.selectInstances(name, true);

            List<Instance> metadataMatchInstances = instances;
            // 如果配置了版本映射,那么只调用元数据匹配的实例
            if (StringUtils.isNotBlank(targetVersion)) {
                metadataMatchInstances = instances.stream()
                        .filter(instance -> Objects.equals(targetVersion, instance.getMetadata().get("version")))
                        .collect(Collectors.toList());
                if (CollectionUtils.isEmpty(metadataMatchInstances)) {
                    log.warn("未找到元数据匹配的目标实例!请检查配置。targetVersion = {}, instance = {}", targetVersion, instances);
                    return null;
                }
            }

            List<Instance> clusterMetadataMatchInstances = metadataMatchInstances;
            // 如果配置了集群名称,需筛选同集群下元数据匹配的实例
            if (StringUtils.isNotBlank(clusterName)) {
                clusterMetadataMatchInstances = metadataMatchInstances.stream()
                        .filter(instance -> Objects.equals(clusterName, instance.getClusterName()))
                        .collect(Collectors.toList());
                if (CollectionUtils.isEmpty(clusterMetadataMatchInstances)) {
                    clusterMetadataMatchInstances = metadataMatchInstances;
                    log.warn("发生跨集群调用。clusterName = {}, targetVersion = {}, clusterMetadataMatchInstances = {}", clusterName, targetVersion, clusterMetadataMatchInstances);
                }
            }

            Instance instance = ExtendBalancer.getHostByRandomWeight2(clusterMetadataMatchInstances);
            return new NacosServer(instance);
        } catch (Exception e) {
            log.warn("发生异常", e);
            return null;
        }
    }

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
    }
}

** 负载均衡算法:**

代码语言:javascript复制
public class ExtendBalancer extends Balancer {
    /**
     * 根据权重,随机选择实例
     *
     * @param instances 实例列表
     * @return 选择的实例
     */
    public static Instance getHostByRandomWeight2(List<Instance> instances) {
        return getHostByRandomWeight(instances);
    }
}

思考

截止到这里,我们已经对Ribbon的基本使用已经如何自定义Ribbon负载均衡规则做了详细说明,但细心的人会发现,我使用上面 RestTemplate 地址拼接方式调用服务接口会存在以下几点不足:

  • 代码可读性差
  • 复杂的url接口地址难以维护
  • 编码体验不统一

带着这些不足,我们引入下一章要讲解的另一种服务调用方式:Feign

0 人点赞