Spring Cloud Alibaba's load balancer component - Ribbon

Load Balancing

We all know that in the micro-service architecture, between micro-services always need to call each other, in order to achieve some of the needs of the business combination. Order details such as the assembly of data, because the order details in the user information, so you have to call customer service and order service to get user information. To achieve long-distance calls will need to send a request to the network, each micro-services are likely to have multiple instances exist distributed on different machines, then when a service call another micro-micro services will need to be evenly distributed to the request on each instance, in order to avoid excessive load some instances, some examples are too idle, so in this scenario must have a load balancer.

At present the main load balancing of two ways:

1, server load balancing; for example, do most classic use Nginx load balancer. First sent to the user's request Nginx, then through the Nginx configured load balancing algorithm to distribute requests to the various instances, as the need to deploy a service in the service side, the service side of the ways is called load balancing. Figure:
Spring Cloud Alibaba's load balancer component - Ribbon

2, the client-side load balancing; is called a client-side load balancing, because this way the load balancing client sends a request to implement, is currently used in micro-service architecture equalization between the service invocation request the common load balancing methods. Because this way, then you can make calls directly between service, no longer need through a dedicated load balancer, this can improve certain performance and high availability. A micro-micro-services call to the service B for example, it is simply a micro service through service discovery component A to obtain micro service call address all instances of B, and wherein a request for a call to select an address load balancing algorithm implemented locally. Figure:
Spring Cloud Alibaba's load balancer component - Ribbon

We write by DiscoveryClient Spring Cloud provides a very simple client-side load balancer, take an intuitive look at the kind of workflow load balancer, load balancing strategy that uses a random example, the following code:

package com.zj.node.contentcenter.discovery;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

/**
 * 客户端侧负载均衡器
 *
 * @author 01
 * @date 2019-07-26
 **/
public class LoadBalance {

    @Autowired
    private DiscoveryClient discoveryClient;

    /**
     * 随机获取目标微服务的请求地址
     *
     * @return 请求地址
     */
    public String randomTakeUri(String serviceId) {
        // 获取目标微服务的所有实例的请求地址
        List<String> targetUris = discoveryClient.getInstances(serviceId).stream()
                .map(i -> i.getUri().toString())
                .collect(Collectors.toList());
        // 随机获取列表中的uri
        int i = ThreadLocalRandom.current().nextInt(targetUris.size());

        return targetUris.get(i);
    }
}

Use Ribbon load balancing

What is the Ribbon:

  • Ribbon is open source Netflix client-side load balancer
  • Ribbon built a wealth of load balancing algorithm strategy

Ribbon虽然是个主要用于负载均衡的小组件,但是麻雀虽小五脏俱全,Ribbon还是有许多的接口组件的。如下表:
Spring Cloud Alibaba's load balancer component - Ribbon

Ribbon默认内置了八种负载均衡策略,若想自定义负载均衡策略则实现上表中提到的IRule接口或AbstractLoadBalancerRule抽象类即可。内置的负载均衡策略如下:
Spring Cloud Alibaba's load balancer component - Ribbon

  • 默认的策略规则为ZoneAvoidanceRule

Ribbon主要有两种使用方式,一是使用Feign,Feign内部已经整合了Ribbon,因此如果只是普通使用的话都感知不到Ribbon的存在;二是配合RestTemplate使用,这种方式则需要添加Ribbon依赖和@LoadBalanced注解。

这里主要演示一下第二种使用方式,由于项目中添加的Nacos依赖已包含了Ribbon所以不需要另外添加依赖,首先定义一个RestTemplate,代码如下:

package com.zj.node.contentcenter.configuration;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * bean 配置类
 *
 * @author 01
 * @date 2019-07-25
 **/
@Configuration
public class BeanConfig {

    @Bean
    @LoadBalanced  // 加上这个注解表示使用Ribbon
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

然后使用RestTemplate调用其他服务的时候,只需要写服务名即可,不需要再写ip地址和端口号。如下示例:

public ShareDTO findById(Integer id) {
    // 获取分享详情
    Share share = shareMapper.selectByPrimaryKey(id);
    // 发布人id
    Integer userId = share.getUserId();
    // 调用用户中心获取用户信息
    UserDTO userDTO = restTemplate.getForObject(
            "http://user-center/users/{id}",  // 只需要写服务名
            UserDTO.class, userId
    );

    ShareDTO shareDTO = objectConvert.toShareDTO(share);
    shareDTO.setWxNickname(userDTO.getWxNickname());

    return shareDTO;
}

如果不太清楚RestTemplate的使用,可以参考如下文章:


自定义Ribbon负载均衡配置

在实际开发中,我们可能会遇到默认的负载均衡策略无法满足需求,从而需要更换其他的负载均衡策略。关于Ribbon负载均衡的配置方式主要有两种,在代码中配置或在配置文件中配置。

Ribbon支持细粒度的配置,例如我希望微服务A在调用微服务B的时候采用随机的负载均衡策略,而在调用微服务C的时候采用默认策略,下面我们就来实现一下这种细粒度的配置。

1、首先是通过代码进行配置,编写一个配置类用于实例化指定的负载均衡策略对象:

@Configuration
public class RibbonConfig {

    @Bean
    public IRule ribbonRule(){
        // 随机的负载均衡策略对象
        return new RandomRule();
    }
}

然后再编写一个用于配置Ribbon客户端的配置类,该配置类的目的是指定在调用user-center时采用RibbonConfig里配置的负载均衡策略,这样就可以达到细粒度配置的效果:

@Configuration
// 该注解用于自定义Ribbon客户端配置,这里声明为属于user-center的配置
@RibbonClient(name = "user-center", configuration = RibbonConfig.class)
public class UserCenterRibbonConfig {
}

需要注意的是RibbonConfig应该定义在主启动类之外,避免被Spring扫描到,不然会产生父子上下文扫描重叠的问题,从而导致各种奇葩的问题。而在Ribbon这里就会导致该配置类被所有的Ribbon客户端共享,即不管调用user-center还是其他微服务都会采用该配置类里定义的负载均衡策略,这样就会变成了一个全局配置了,违背了我们需要细粒度配置的目的。所以需要将其定义在主启动类之外:
Spring Cloud Alibaba's load balancer component - Ribbon

关于这个问题可以参考官方文档的描述:

https://cloud.spring.io/spring-cloud-static/Greenwich.SR2/single/spring-cloud.html#_customizing_the_ribbon_client

2、使用配置文件进行配置就更简单了,不需要写代码还不会有父子上下文扫描重叠的坑,只需在配置文件中增加如下一段配置就可以实现以上使用代码配置等价的效果:

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

两种配置方式对比:
Spring Cloud Alibaba's load balancer component - Ribbon

最佳实践总结:

  • 尽量使用配置文件配置,配置文件满足不了需求的情况下再考虑使用代码配置
  • 在同一个微服务内尽量保持单一性,例如统一使用配置文件配置,尽量不要两种方式混用,以免增加定位问题的复杂度

以上介绍的是细粒度地针对某个特定Ribbon客户端的配置,下面我们再演示一下如何实现全局配置。很简单,只需要把注解改为@RibbonClients即可,代码如下:

@Configuration
// 该注解用于全局配置
@RibbonClients(defaultConfiguration = RibbonConfig.class)
public class GlobalRibbonConfig {
}

Ribbon默认是懒加载的,所以在第一次发生请求的时候会显得比较慢,我们可以通过在配置文件中添加如下配置开启饥饿加载:

ribbon:
  eager-load:
    enabled: true
    # 为哪些客户端开启饥饿加载,多个客户端使用逗号分隔(非必须)
    clients: user-center

支持Nacos权重

以上小节基本介绍完了负载均衡及Ribbon的基础使用,接下来的内容需要配合Nacos,若没有了解过Nacos的话可以参考以下文章:

在Nacos Server的控制台页面可以编辑每个微服务实例的权重,服务列表 -> 详情 -> 编辑;默认权重都为1,权重值越大就越优先被调用:
Spring Cloud Alibaba's load balancer component - Ribbon

权重在很多场景下非常有用,例如一个微服务有很多的实例,它们被部署在不同配置的机器上,这时候就可以将配置较差的机器上所部署的实例权重设置得比较低,而部署在配置较好的机器上的实例权重设置得高一些,这样就可以将较大一部分的请求都分发到性能较高的机器上。

但是Ribbon内置的负载均衡策略都不支持Nacos的权重,所以我们就需要自定义实现一个支持Nacos权重配置的负载均衡策略。好在Nacos Client已经内置了负载均衡的能力,所以实现起来也比较简单,代码如下:

package com.zj.node.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;

/**
 * 支持Nacos权重配置的负载均衡策略
 *
 * @author 01
 * @date 2019-07-27
 **/
@Slf4j
public class NacosWeightedRule extends AbstractLoadBalancerRule {

    @Autowired
    private  NacosDiscoveryProperties discoveryProperties;

    /**
     * 读取配置文件,并初始化NacosWeightedRule
     *
     * @param iClientConfig iClientConfig
     */
    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        // do nothing
    }

    @Override
    public Server choose(Object key) {
        BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
        log.debug("lb = {}", loadBalancer);

        // 需要请求的微服务名称
        String name = loadBalancer.getName();
        // 获取服务发现的相关API
        NamingService namingService = discoveryProperties.namingServiceInstance();

        try {
            // 调用该方法时nacos client会自动通过基于权重的负载均衡算法选取一个实例
            Instance instance = namingService.selectOneHealthyInstance(name);
            log.info("选择的实例是:instance = {}", instance);

            return new NacosServer(instance);
        } catch (NacosException e) {
            return null;
        }
    }
}

然后在配置文件中配置一下就可以使用该负载均衡策略了:

user-center:
  ribbon:
    NFLoadBalancerRuleClassName: com.zj.node.contentcenter.configuration.NacosWeightedRule

思考:既然Nacos Client已经有负载均衡的能力,Spring Cloud Alibaba为什么还要去整合Ribbon呢?

个人认为,这主要是为了符合Spring Cloud标准。Spring Cloud Commons有个子项目 spring-cloud-loadbalancer ,该项目制定了标准,用来适配各种客户端负载均衡器(虽然目前实现只有Ribbon,但Hoxton就会有替代的实现了)。

Spring Cloud Alibaba遵循了这一标准,所以整合了Ribbon,而没有去使用Nacos Client提供的负载均衡能力。


同一集群优先调用

Spring Cloud Alibaba之服务发现组件 - Nacos一文中已经介绍过集群的概念以及作用,这里就不再赘述,加上上一小节中已经介绍过如何自定义负载均衡策略了,所以这里不再啰嗦而是直接上代码,实现代码如下:

package com.zj.node.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.alibaba.nacos.client.naming.core.Balancer;
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;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 实现同一集群优先调用并基于随机权重的负载均衡策略
 *
 * @author 01
 * @date 2019-07-27
 **/
@Slf4j
public class NacosSameClusterWeightedRule extends AbstractLoadBalancerRule {

    @Autowired
    private NacosDiscoveryProperties discoveryProperties;

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        // do nothing
    }

    @Override
    public Server choose(Object key) {
        // 获取配置文件中所配置的集群名称
        String clusterName = discoveryProperties.getClusterName();
        BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
        // 获取需要请求的微服务名称
        String serviceId = loadBalancer.getName();
        // 获取服务发现的相关API
        NamingService namingService = discoveryProperties.namingServiceInstance();

        try {
            // 获取该微服务的所有健康实例
            List<Instance> instances = namingService.selectInstances(serviceId, true);
            // 过滤出相同集群下的所有实例
            List<Instance> sameClusterInstances = instances.stream()
                    .filter(i -> Objects.equals(i.getClusterName(), clusterName))
                    .collect(Collectors.toList());

            // 相同集群下没有实例则需要使用其他集群下的实例
            List<Instance> instancesToBeChosen;
            if (CollectionUtils.isEmpty(sameClusterInstances)) {
                instancesToBeChosen = instances;
                log.warn("发生跨集群调用,name = {}, clusterName = {}, instances = {}",
                        serviceId, clusterName, instances);
            } else {
                instancesToBeChosen = sameClusterInstances;
            }

            // 基于随机权重的负载均衡算法,从实例列表中选取一个实例
            Instance instance = ExtendBalancer.getHost(instancesToBeChosen);
            log.info("选择的实例是:port = {}, instance = {}", instance.getPort(), instance);

            return new NacosServer(instance);
        } catch (NacosException e) {
            log.error("获取实例发生异常", e);
            return null;
        }
    }
}

class ExtendBalancer extends Balancer {

    /**
     * 由于Balancer类里的getHostByRandomWeight方法是protected的,
     * 所以通过这种继承的方式来实现调用,该方法基于随机权重的负载均衡算法,选取一个实例
     */
    static Instance getHost(List<Instance> hosts) {
        return getHostByRandomWeight(hosts);
    }
}

同样的,想要使用该负载均衡策略的话,在配置文件中配置一下即可:

user-center:
  ribbon:
    NFLoadBalancerRuleClassName: com.zj.node.contentcenter.configuration.NacosSameClusterWeightedRule

基于元数据的版本控制

In both sections we achieve load balancing strategy based on lower weight Nacos rights load balancing strategy and the same cluster priority call, but in the actual project, you may face the problem of multiple versions co-exist, namely a micro service instances have different versions and it may be incompatible with each of these instances of different versions. A service such as micro-v1 version of the service instance can not be called a micro version of v2 B instance, you can only call the v1 version of the Micro-B services.

The metadata Nacos is more suitable to solve the problem with this version control, as the concept and configuration metadata is already in Spring Cloud Alibaba's service discovery component - Nacos introduced in one article, here introduces how to achieve through the Ribbon version control metadata based.

For example, there are two micro-line service, as a service provider as a service consumer, they have different versions of example, as follows:

  • Service provider There are two versions: v1, v2
  • Service consumers also have two versions: v1, v2

v1 and v2 are incompatible. Consumers can call the service provider v1 v1; v2 consumers can only call provider v2. How to achieve it? Let us around the scene to implement version control between micro-services.

In summary, we need to realize there are two main:

  • Preferences under the same cluster, in line with the example of metadata
  • If no instance in line with metadata under the same cluster, select the instance metadata in line with the other clusters

First, we have to configure metadata in the configuration file, metadata is a pile of descriptive information to k - configured v form, as follows:

spring:
  cloud:
    nacos:
      discovery:
        # 指定nacos server的地址
        server-addr: 127.0.0.1:8848
        # 配置元数据
        metadata: 
          # 当前实例版本
          version: v1
          # 允许调用的提供者实例的版本
          target-version: v1

Then you can write code, as before, is achieved through load balancing strategy, specific code as follows:

package com.zj.node.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.alibaba.nacos.client.naming.utils.CollectionUtils;
import com.alibaba.nacos.client.utils.StringUtils;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.DynamicServerListLoadBalancer;
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;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * 基于元数据的版本控制负载均衡策略
 *
 * @author 01
 * @date 2019-07-27
 **/
@Slf4j
public class NacosFinalRule extends AbstractLoadBalancerRule {

    @Autowired
    private NacosDiscoveryProperties discoveryProperties;

    private static final String TARGET_VERSION = "target-version";
    private static final String VERSION = "version";

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        // do nothing
    }

    @Override
    public Server choose(Object key) {
        // 获取配置文件中所配置的集群名称
        String clusterName = discoveryProperties.getClusterName();
        // 获取配置文件中所配置的元数据
        String targetVersion = discoveryProperties.getMetadata().get(TARGET_VERSION);

        DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
        // 需要请求的微服务名称
        String serviceId = loadBalancer.getName();
        // 获取该微服务的所有健康实例
        List<Instance> instances = getInstances(serviceId);

        List<Instance> metadataMatchInstances = instances;
        // 如果配置了版本映射,那么代表只调用元数据匹配的实例
        if (StringUtils.isNotBlank(targetVersion)) {
            // 过滤与版本元数据相匹配的实例,以实现版本控制
            metadataMatchInstances = filter(instances,
                    i -> Objects.equals(targetVersion, i.getMetadata().get(VERSION)));

            if (CollectionUtils.isEmpty(metadataMatchInstances)) {
                log.warn("未找到元数据匹配的目标实例!请检查配置。targetVersion = {}, instance = {}",
                        targetVersion, instances);
                return null;
            }
        }

        List<Instance> clusterMetadataMatchInstances = metadataMatchInstances;
        // 如果配置了集群名称,需筛选同集群下元数据匹配的实例
        if (StringUtils.isNotBlank(clusterName)) {
            // 过滤出相同集群下的所有实例
            clusterMetadataMatchInstances = filter(metadataMatchInstances,
                    i -> Objects.equals(clusterName, i.getClusterName()));

            if (CollectionUtils.isEmpty(clusterMetadataMatchInstances)) {
                clusterMetadataMatchInstances = metadataMatchInstances;
                log.warn("发生跨集群调用。clusterName = {}, targetVersion = {}, clusterMetadataMatchInstances = {}", clusterName, targetVersion, clusterMetadataMatchInstances);
            }
        }

        // 基于随机权重的负载均衡算法,选取其中一个实例
        Instance instance = ExtendBalancer.getHost(clusterMetadataMatchInstances);

        return new NacosServer(instance);
    }

    /**
     * 通过过滤规则过滤实例列表
     */
    private List<Instance> filter(List<Instance> instances, Predicate<Instance> predicate) {
        return instances.stream()
                .filter(predicate)
                .collect(Collectors.toList());
    }

    private List<Instance> getInstances(String serviceId) {
        // 获取服务发现的相关API
        NamingService namingService = discoveryProperties.namingServiceInstance();
        try {
            // 获取该微服务的所有健康实例
            return namingService.selectInstances(serviceId, true);
        } catch (NacosException e) {
            log.error("发生异常", e);
            return Collections.emptyList();
        }
    }
}

class ExtendBalancer extends Balancer {
    /**
     * 由于Balancer类里的getHostByRandomWeight方法是protected的,
     * 所以通过这种继承的方式来实现调用,该方法基于随机权重的负载均衡算法,选取一个实例
     */
    static Instance getHost(List<Instance> hosts) {
        return getHostByRandomWeight(hosts);
    }
}

Guess you like

Origin blog.51cto.com/zero01/2424180