负载均衡——nacos实现篇

Posted 小码助力助你前行

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了负载均衡——nacos实现篇相关的知识,希望对你有一定的参考价值。

前言:前面我们讲到了什么是负载均衡,以及通过nginx我们该如何实现它,还没有看的小伙伴可以点这里过去查看
负载均衡——Nginx实现篇
在讲具体配置实现之前,我先带大家了解一下,什么是nacos。

什么是Nacos

首先我们来看看官网对nacos(官网地址在这里 https://nacos.io/zh-cn/docs/what-is-nacos.html)的描述:

Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。

通过官网的描述,我们可以大致可以了解到,nacos主要是一个用来管理各个服务的工具,能够将发现并管理你的服务。关于这个服务我们一般叫他微服务,这个“微”是相对于以前的单服务架构而言的。将一个业务内容颇多的单服务通过某种关系进行多服务拆分模组化,就得到了我们现在常说的微服务。每一个微服务本身其实就是一个单服务,只不过它的业务更为精简,因此每一个服务都需要单独部署。然而分别手动去管理每一个服务,对于部署和运维人员来说,这是一个很艰巨的任务,因此就急需一个平台工具,能够可视化的管理这些所有服务,比如查看链接情况、处理请求数量,查看对应的服务是否正常运行没有宕机,以及我们今天核心要讲的,通过配置实现负载均衡的目的。关于微服务这里还是不太理解的同学可以看看我的另一篇文章浅析SpringCloud

nacos不同于Nginx,Nginx一般只有技术经理之类的人才会在部署项目的过程中去使用,作为一个才接触开发一两年没多久的新人而言,则比较难实操。但nacos就不一样了,nacos作为配置文件管理的项目,如果使用公司统一的测试环境nacos去启动服务,你会发现你的电脑会莫名其妙的刷日志,而自己的请求却不是每次都会请求到自己的后台,原因就是因为有其他的服务也挂载在对应的nacos主机上了,导致了请求的分发。因此需要本地部署一套nacos,然后本地启动访问自己本地的nacos,就可以了。下面简单的介绍一下如何在本地启动nacos(以windows为例):

首先在官网下载符合自己电脑环境的安装包
选择第二个点击就可以开始下载了。下载完成后解压到合适的地方就可以了。
接下来在找到bin之前,我们要进行一个操作,nacos默认是集群运行,本地单机环境直接运行会发现起不来,我们需要改一下启动脚本。windows环境的朋友们我们以文档编辑的形式打开bin文件夹下的startup文件(注意了是.cmd后缀的不是.sh后缀的,如果文件开启了后缀名隐藏的话就看这个)

当然了,如果是mac本本,就改另外一个。我们找到这一行代码

将其中的cluster改成standalone,如下

保存退出后,双击就可以运行了。nacos默认的端口是8848,本地启动之后自带的Tomcat可以直接通过http://localhost:8848/nacos访问它的可视化页面,默认账密都是nacos。有些人本地起nacos如果起不来的话关注一下JDK是否安装、版本是否支持以及环境变量是否正确,如果报错信息是有关JAVA_HOME的肯定就是这三个原因之一了。启动登录之后就可以看到这个页面:

这两个yaml文件是我项目需要自己加进去的,你们没有任何操作的话是没有这两个文件的。这两个文件具体的名称是跟代码中读取配置的地方一一对应的,你们项目中需要什么配置文件找头头要就可以了。关于yaml文档有时间可以学习一下,很简单,比起xml简直不要强太多,几乎是个没有引号版的json。接下来我们来看看如何通过nacos来实现负载均衡,又该如何操作。

首先nacos实现负载均衡是通过内置的Ribbon实现的,nacos(某些版本)默认的负载均衡策略是使用的Ribbon中的默认策略——轮询,这个概念很简单我就不详细讲了。这种情况下如果想实现实现按权重策略来实现负载均衡,可以在代码中代码中的ConsumerApplication类里添加这串代码:

@Bean
@Scope(value="prototype")
public IRule loadBalanceRule()
    return new NacosRule();

在nacos的服务管理中,在配置了对应nacos的服务启动只能是可以在服务列表中找到对应服务实例的

这是nacos主动识别出来的服务,也可以手动去创建实例。对于有实例数的服务,在详情中是可以对每个实例进行权重分配的。这里讲一下什么叫实例,实例就是一个服务运行环境,docker出现之前可以理解为一台运行了该服务的服务器,现在一台服务器可以通过docker拆成多个独立的环境来运行服务,因此这也就对应了多个实例。然后针对这些实例就可以根据各自的性能等其他因素进行合理的权重分配,实现权重轮询的负载均衡策略。

关于Ribbon本身还有提供很多负载均衡的策略,也支持自定义的策略,只需要继承实现指定接口,然后找到对应的配置入口修改策略方式就行了,感兴趣的同学可以去了解和尝试Ribbon其他六种自带的均衡策略以及自己动手实现一个自定义的策略。

我是本篇的小编DJ,
遇到Bug需要帮助,
欢迎加wx:xmzl1988 ,
备注"csdn博客",
温馨提示此为有偿服务哦。

07篇 Nacos客户端是如何实现实例获取的负载均衡呢?

学习不用那么功利,二师兄带你从更高维度轻松阅读源码~

前面我们讲了Nacos客户端如何获取实例列表,如何进行缓存处理,以及如何订阅实例列表的变更。在获取到一个实例列表之后,你是否想过一个问题:如果实例列表有100个实例,Nacos客户端是如何从中选择一个呢?

这篇文章,就带大家从源码层面分析一下,Nacos客户端采用了如何的算法来从实例列表中获取一个实例进行请求的。也可以称作是Nacos客户端的负载均衡算法。

单个实例获取

NamingService不仅提供了获取实例列表的方法,也提供了获取单个实例的方法,比如:

Instance selectOneHealthyInstance(String serviceName, String groupName, List<String> clusters, boolean subscribe)
        throws NacosException;

该方法会根据预定义的负载算法,从实例列表中获得一个健康的实例。其他重载的方法功能类似,最终都会调用该方法,我们就以此方法为例来分析一下具体的算法。

具体实现代码:

@Override
public Instance selectOneHealthyInstance(String serviceName, String groupName, List<String> clusters,
        boolean subscribe) throws NacosException {
    String clusterString = StringUtils.join(clusters, ",");
    if (subscribe) {
        // 获取ServiceInfo
        ServiceInfo serviceInfo = serviceInfoHolder.getServiceInfo(serviceName, groupName, clusterString);
        if (null == serviceInfo) {
            serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString);
        }
        // 通过负载均衡算法获得其中一个实例
        return Balancer.RandomByWeight.selectHost(serviceInfo);
    } else {
        // 获取ServiceInfo
        ServiceInfo serviceInfo = clientProxy
                .queryInstancesOfService(serviceName, groupName, clusterString, 0, false);
        // 通过负载均衡算法获得其中一个实例
        return Balancer.RandomByWeight.selectHost(serviceInfo);
    }
}

selectOneHealthyInstance方法逻辑很简单,调用我们之前讲到的方法获取ServiceInfo对象,然后作为参数传递给负载均衡算法,由负载均衡算法计算出最终使用哪个实例(Instance)。

算法参数封装

先跟踪一下代码实现,非核心业务逻辑,只简单提一下。

上面的代码可以看出调用的是Balancer内部类RandomByWeight的selectHost方法:

public static Instance selectHost(ServiceInfo dom) {
    // ServiceInfo中获去实例列表
    List<Instance> hosts = selectAll(dom);
    // ...
    return getHostByRandomWeight(hosts);
}

selectHost方法核心逻辑是从ServiceInfo中获取实例列表,然后调用getHostByRandomWeight方法:

protected static Instance getHostByRandomWeight(List<Instance> hosts) {
    // ... 判断逻辑
    // 重新组织数据格式
    List<Pair<Instance>> hostsWithWeight = new ArrayList<Pair<Instance>>();
    for (Instance host : hosts) {
        if (host.isHealthy()) {
            hostsWithWeight.add(new Pair<Instance>(host, host.getWeight()));
        }
    }
    // 通过Chooser来实现随机权重负载均衡算法
    Chooser<String, Instance> vipChooser = new Chooser<String, Instance>("www.taobao.com");
    vipChooser.refresh(hostsWithWeight);
    return vipChooser.randomWithWeight();
}

getHostByRandomWeight前半部分是将Instance列表及其中的权重数据进行转换,封装成一个Pair,也就是建立成对的关系。在此过程中只使用了健康的节点。

真正的算法实现则是通过Chooser类来实现的,看名字基本上知道实现的策略是基于权重的随机算法。

负载均衡算法实现

所有的负载均衡算法实现均位于Chooser类中,Chooser类的提供了两个方法refresh和randomWithWeight。

refresh方法用于筛选数据、检查数据合法性和建立算法所需数据模型。

randomWithWeight方法基于前面的数据来进行随机算法处理。

先看refresh方法:

public void refresh(List<Pair<T>> itemsWithWeight) {
    Ref<T> newRef = new Ref<T>(itemsWithWeight);
    // 准备数据,检查数据
    newRef.refresh();
    // 上面数据刷新之后,这里重新初始化一个GenericPoller
    newRef.poller = this.ref.poller.refresh(newRef.items);
    this.ref = newRef;
}

基本步骤:

  • 创建Ref类,该类为Chooser的内部类;
  • 调用Ref的refresh方法,用于准备数据、检查数据等;
  • 数据筛选完成,调用poller#refresh方法,本质上就是创建一个GenericPoller对象;
  • 成员变量重新赋值;

这里重点看Ref#refresh方法:

/**
 * 获取参与计算的实例列表、计算递增数组数总和并进行检查
 */
public void refresh() {
    // 实例权重总和
    Double originWeightSum = (double) 0;
    
    // 所有健康权重求和
    for (Pair<T> item : itemsWithWeight) {
        
        double weight = item.weight();
        //ignore item which weight is zero.see test_randomWithWeight_weight0 in ChooserTest
        // 权重小于等于0则不参与计算
        if (weight <= 0) {
            continue;
        }
        // 有效实例放入列表
        items.add(item.item());
        // 如果值无限大
        if (Double.isInfinite(weight)) {
            weight = 10000.0D;
        }
        // 如果值为非数字
        if (Double.isNaN(weight)) {
            weight = 1.0D;
        }
        // 权重值累加
        originWeightSum += weight;
    }
    
    double[] exactWeights = new double[items.size()];
    int index = 0;
    // 计算每个节点权重占比,放入数组
    for (Pair<T> item : itemsWithWeight) {
        double singleWeight = item.weight();
        //ignore item which weight is zero.see test_randomWithWeight_weight0 in ChooserTest
        if (singleWeight <= 0) {
            continue;
        }
        // 计算每个节点权重占比
        exactWeights[index++] = singleWeight / originWeightSum;
    }
    
    // 初始化递增数组
    weights = new double[items.size()];
    double randomRange = 0D;
    for (int i = 0; i < index; i++) {
        // 递增数组第i项值为items前i个值总和
        weights[i] = randomRange + exactWeights[i];
        randomRange += exactWeights[i];
    }
    
    double doublePrecisionDelta = 0.0001;
    // index遍历完则返回;
    // 或weights最后一位值与1相比,误差小于0.0001,则返回
    if (index == 0 || (Math.abs(weights[index - 1] - 1) < doublePrecisionDelta)) {
        return;
    }
    throw new IllegalStateException(
            "Cumulative Weight calculate wrong , the sum of probabilities does not equals 1.");
}

可结合上面代码中的注释来理解,核心步骤包括以下:

  • 遍历itemsWithWeight,计算权重总和数据;非健康节点会被剔除掉;
  • 计算每个节点的权重值在总权重值中的占比,并存储在exactWeights数组当中;
  • 将exactWeights数组当中值进行数据重构,形成一个递增数组weights(每个值都是exactWeights坐标值的总和),后面用于随机算法;
  • 判断是否循环完成或误差在指定范围内(0.0001),符合则返回。

所有数据准备完成,调用随机算法方法randomWithWeight:

public T randomWithWeight() {
    Ref<T> ref = this.ref;
    // 生成0-1之间的随机数
    double random = ThreadLocalRandom.current().nextDouble(0, 1);
    // 采用二分法查找数组中指定值,如果不存在则返回(-(插入点) - 1),插入点即随机数将要插入数组的位置,即第一个大于此键的元素索引。
    int index = Arrays.binarySearch(ref.weights, random);
    // 如果没有查询到(返回-1或"-插入点")
    if (index < 0) {
        index = -index - 1;
    } else {
        // 命中直接返回结果
        return ref.items.get(index);
    }
    
    // 判断坐标未越界
    if (index < ref.weights.length) {
        // 随机数小于指定坐标的数值,则返回坐标值
        if (random < ref.weights[index]) {
            return ref.items.get(index);
        }
    }
    
    // 此种情况不应该发生,但如果发生则返回最后一个位置的值
    /* This should never happen, but it ensures we will return a correct
     * object in case there is some floating point inequality problem
     * wrt the cumulative probabilities. */
    return ref.items.get(ref.items.size() - 1);
}

该方法的基本操作如下:

  • 生成一个0-1的随机数;
  • 使用Arrays#binarySearch在数组中进行查找,也就是二分查找法。该方法会返回包含key的值,如果没有则会返回”-1“或”-插入点“,插入点即随机数将要插入数组的位置,即第一个大于此键的元素索引。
  • 如果命中则直接返回;如果未命中则对返回值取反减1,获得index值;
  • 判断index值,符合条件,则返回结果;

至此,关于Nacos客户端实例获取的负载均衡算法代码层面追踪完毕。

算法实例演示

下面用一个实例来演示一下,该算法中涉及的数据变化。为了数据美观,这里采用4组数据,每组数据进来确保能被整除;

节点及权重数据(前面节点,后面权重)如下:

1 100
2 25
3 75
4 200

第一步,计算权重综合:

originWeightSum = 100 + 25 + 75 + 200 = 400

第二步,计算每个节点权重比:

exactWeights = {0.25, 0.0625, 0.1875, 0.5}

第三步,计算递增数组weights:

weights = {0.25, 0.3125, 0.5, 1}

第四步,生成0-1的随机数:

random = 0.3049980013493817

第五步,调用Arrays#binarySearch从weights中搜索random:

index = -2

关于Arrays#binarySearch(double[] a, double key)方法这里再解释一下,如果传入的key恰好在数组中,比如1,则返回的index为3;如果key为上面的random值,则先找到插入点,取反,减一。

插入点即第一个大于此key的元素索引,那么上面第一个大于0.3049980013493817的值为0.3125,那么插入点值为1;

于是按照公式计算Arrays#binarySearch返回的index为:

index = - ( 1 ) - 1 = -2

第六步,也就是没有恰好命中的情况:

index = -( -2 ) - 1 = 1

然后判断index是否越界,很明显 1 < 4,未越界,则返回坐标为1的值。

算法的核心

上面演示了算法,但这个算法真的能够做到按权重负载吗?我们来分析一下这个问题。

这个问题的重点不在random值,这个值基本上是随机的,那么怎么保证权重大的节点获得的机会更多呢?

这里先把递增数组weights用另外一个形式来表示:

上面的算法可以看出,weights与exactWeights为size相同的数组,对于同一坐标(index),weights的值是exactWeights包含当前坐标及前面所有坐标值的和。

如果把weights理解成一条线,对应节点的值是线上的一个个点,体现在图中便是(图2到图5)有色(灰色+橘黄色)部分。

而Arrays#binarySearch算法的插入点获取的是第一个大于key(也就是random)的坐标,也就是说每个节点享有的随机范围不同,它们的范围由当前点和前一个点的区间决定,而这个区间正好是权重比值。

权重比值大的节点,占有的区间就比较多,比如节点1占了1/4,节点4占了1/2。这样,如果随机数是均匀分布的,那么占有范围比较大的节点更容易获得青睐。也就达到了按照权重获得被调用的机会了。

小结

本篇文章追踪Nacos客户端源码,分析了从实例列表中获得其中一个实例的算法,也就是随机权重负载均衡算法。整体业务逻辑比较简单,从ServiceInfo中获得实例列表,一路筛选,选中目标实例,然后根据它们的权重进行二次处理,数据结构封装,最后基于Arrays#binarySearch提供的二分查找法来获得对应的实例。

而我们需要注意和学习的重点便是权重获取算法的思想及具体实现,最终达到能够在实践中进行运用。

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

以上是关于负载均衡——nacos实现篇的主要内容,如果未能解决你的问题,请参考以下文章

负载均衡——nacos实现篇

微服务管理平台nacos虚拟ip负载均衡集群模式搭建

SpringCloud之微服务实用篇1

SpringCloud之微服务实用篇1

云原生SpringCloud系列之客户端负载均衡Ribbon

Springcloud + nacos + gateway 负载均衡(ribbon)