Dubbo中的负载均衡算法之一致性哈希算法

news2024/11/15 8:59:16

Dubbo中的负载均衡算法之一致性哈希算法

哈希算法

假设这样一个场景,我们申请一台服务器来缓存100万的数据,这个时候是单机环境,所有的请求都命中到这台服务器。后来业务量上涨,我们的数据也从100万上升到了300万,原来的单机环境,无法容纳如此多的数据,所以我们决定扩容到三台服务器。但这个时候有一个问题,这三百万的数据如何平均分配到这三台服务器上呢?假设我们随机分配,那么当我们需要查询某一个缓存项时,则需要先去第一台机器进行查找,如果找到则返回给用户,如果没有,继续去第二台机器查找,如果还没有则继续去第三台机器查找,这样一来,查询缓存项的性能则会明显下降,有没有办法根据缓存项的键,直接定位到正确的机器进行查询呢?

一种行之有效的方法就是通过哈希算法去定位。对于相同的键,每次计算得出的哈希值应该是一致的。假设我们有三台机器,可以对键通过哈希算法得到哈希值,然后对3取余,结果只能是0、1或2,我们将这三个结果分别作为三台机器的编号。这样每次有查询缓存的请求过来,我们都可以按照以下步骤去做

  1. 对键进行哈希运算,得到一个哈希值,然后对3取余,得到一个机器编号
  2. 前往对应的机器进行查询。如果找到缓存项,直接返回结果;如果未找到,表示缓存项不存在。
    在这里插入图片描述

上述的方案看起来已经解决了问题,但是,如果在分布式场景中,却有着很大的局限性。因为在生产环境中,不可避免会发生以下几种情况:

  1. 服务器故障宕机,无法提供服务而下线
  2. 业务高峰期需要弹性扩容
  3. 业务低谷期需要弹性缩容

这几种场景都会导致后台服务器的数量 n 发生变化,那么我们原来的公式hash(key) % n因为n发生了变化,导致计算出的值也会发生变化,这样一来就会导致大量的缓存失效,造成缓存雪崩。如果是采用这种方案做负载均衡的话,也会导致大量的请求路由到错误的服务器,找不到对应的服务,导致服务异常。因此就有了一致性hash算法,用来解决这个问题,保证当后端移除或者添加一个服务器时,要尽可能小的改变存量请求与服务器之间的正确映射关系。

一致性哈希算法

基本介绍

一致性哈希算法最早是由David Karger等人在1997年发表的论文Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web 中提出的。该算法的设计目标是为了解决因特网中的热点问题,即如何将数据尽可能均匀地分布到不同的节点上,以避免某些节点承受过大的负载。

它的主要思路是将整个哈希值空间组织成一个虚拟的圆环,让每个服务器节点在环上占据一个位置。有新的请求的时候,通过hash算法将请求映射到环上的一个位置,然后顺时针找到最近的服务器去进行处理。这样当一个服务器节点加入或退出时,只会影响到其附近的一小部分数据,而不会对整个系统造成很大的影响。他的一般步骤为:

  1. 如下图所示,我们虚拟出一个有232-1个节点的hash环,然后将服务器地址(ip+port)计算hash值,,对232-1取模,算出其在hash环上的位置,假设我们有三个服务器节点:S1、S2 、S3,其映射关系如图所示。
  2. 当一个新的服务请求到来时,也对该请求的标识符进行哈希计算,得到其在环上的位置。
  3. 从环上顺时针找到距离该请求最近的一个节点,将请求分配给该节点处理。例如请求R1,R2被分配给S1这个服务器去处理

在这里插入图片描述

如何解决服务器节点增加、减少带来的问题的

如果在hash环上加一个节点,例如我们添加了S4这个节点,那么原本应该路由到S1的请求和路由到S2的请求不会受到影响,依然可以正确路由,不过原本路由到S3的部分请求则将会被路由到新节点S4。这样就做到了我们前面说的尽可能小的改变已存在的服务请求与处理请求服务器之间的映射关系。

在这里插入图片描述

减少一个节点也是同样的道理,例如下图中我们下线了节点S3,那么原本应该路由到S1的请求和路由到S2的请求不会受到影响,依然可以正确路由,受影响的只是原本应该路由到S3的请求因为S3下线,而会路由到S1。

在这里插入图片描述

什么是虚拟节点

前面的图例中,我们可以看到S1,S2,S3三个节点是均匀分布在hash环上的,但是实际上,我们通过hash算法计算的时候可能达不到理想的效果,有可能会得到这样的分布效果。这就会导致大量的请求被路由到S3这个节点。因此提出了虚拟节点的概念。
在这里插入图片描述

所谓虚拟节点就是说并非真实的物理节点,而是对物理节点拷贝的一个副本,他们和其对应的真实物理节点有一样的IP和端口号,因此称为虚拟节点。如下图所示,我们有三个物理节点,S1、S2、 S3。然后对每个物理节点虚拟出了两个虚拟节点,将其分布在hash环上。这样假设有一个请求访问过来,通过hash计算落到了S3-2到S2-2的区间,那么他将会路由到S2-2这个服务器节点,而S2-2其实是S2的一个虚拟节点,所以实际处理这个请求的是S2这个物理节点。通过图我们可以直观的看到,引入虚拟节点后,请求的分布会进一步变得平衡,而引入的虚拟节点数量越多,则平衡性越好

在这里插入图片描述

Dubbo一致性哈希负载均衡的实现

dubbo一致性哈希负载均衡策略的实现在ConsistentHashLoadBalance这个类里,它继承了AbstractLoadBalance这个抽象类,实现了方法doSelect()方法,这个方法的作用是用来从多个服务提供者里面选择一个来调用。这里有两个重要的参数InvokerInvocationInvoker 我们可以理解为对服务提供者的一个封装,所有的服务提供者在dubbo里都会被转化为Invoker。所以List<Invoker<T>> invokers其实就是我们本次请求的所有服务提供者的列表。Invocation我们理解为对本次请求会话的一个封装,里面包括了请求的参数、方法以及变量

InvokerInvocation 的概念可参考:代码架构 | Apache Dubbo、实现细节 | Apache Dubbo

/**
 * 线程安全的一个map,缓存了所有的Selector(可以理解为一个选择器,用来从众多的服务提供者里面选择一个,实现负载均衡的效果),
 * key:服务提供者的签名
 * value:具体某一个服务对应的Selector
 */
private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors = new ConcurrentHashMap<String, ConsistentHashSelector<?>>();

/**
 * 从invokers里选一个调用
 */
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    String methodName = RpcUtils.getMethodName(invocation);

    //  1. 根据服务提供者的签名从前面提到的map里找对应的选择器
    String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
    int invokersHashCode = invokers.hashCode();
    ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);

    // 1.1  没有找到?那么初始化,这是一个懒加载的过程
    if (selector == null || selector.identityHashCode != invokersHashCode) {
        selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, invokersHashCode));
        selector = (ConsistentHashSelector<T>) selectors.get(key);
    }

    // 1.2 通过selector选一个具体的服务提供者,处理请求
    return selector.select(invocation);
}

前面提到如果没有找打对应的Selector,那么我们要去做初始化,这里是如何初始化一个一致性哈希负载均衡策略的逻辑。

private static final class ConsistentHashSelector<T> {
	
    // 用TreeMap模拟一个hash环
    private final TreeMap<Long, Invoker<T>> virtualInvokers;

    private final int replicaNumber;

    private final int identityHashCode;

    private final int[] argumentIndex;

    // 初始化Selector的逻辑
    ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
        this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
        this.identityHashCode = identityHashCode;
        URL url = invokers.get(0).getUrl();
        // 物理节点的副本数,默认160
        this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
        String[] index = COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, HASH_ARGUMENTS, "0"));
        argumentIndex = new int[index.length];
        for (int i = 0; i < index.length; i++) {
            argumentIndex[i] = Integer.parseInt(index[i]);
        }

        // 初始化hash环,将所有的invokers均匀的分布到环上
        for (Invoker<T> invoker : invokers) {
            String address = invoker.getUrl().getAddress();
            // 创建虚拟节点,每个物理机节点应该有replicaNumber个虚拟节点,但是这里用两次for循环,应该是为了加强hash的随机性
            for (int i = 0; i < replicaNumber / 4; i++) {
                byte[] digest = Bytes.getMD5(address + i);
                for (int h = 0; h < 4; h++) {
                    long m = hash(digest, h);
                    virtualInvokers.put(m, invoker);
                }
            }
        }
    }

    // 具体的选择逻辑,对请求计算md5值,算一个hash
    public Invoker<T> select(Invocation invocation) {
        byte[] digest = Bytes.getMD5(RpcUtils.getMethodName(invocation));
        return selectForKey(hash(digest, 0));
    }

    // 用ceilingEntry方法模拟从哈希环上选择顺时针最近的一个服务节点
    private Invoker<T> selectForKey(long hash) {
        Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
        if (entry == null) {
            entry = virtualInvokers.firstEntry();
        }
        return entry.getValue();
    }

    private long hash(byte[] digest, int number) {
        return (((long) (digest[3 + number * 4] & 0xFF) << 24)
                | ((long) (digest[2 + number * 4] & 0xFF) << 16)
                | ((long) (digest[1 + number * 4] & 0xFF) << 8)
                | (digest[number * 4] & 0xFF))
                & 0xFFFFFFFFL;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1164214.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VS使用小技巧——如何让别人看不到你写的代码,却能够运行你的代码

VS使用小技巧 前言方法使用静态库的示例如何创建静态库如何导入静态库Xcode里导入静态库VS2022导入静态库 前言 在实际生活中&#xff0c;作为程序员偶尔会因为资金不够用了选择去兼职写代码&#xff0c;当我们写完一个代码&#xff0c;将他发给某个公司的时候&#xff0c;我们…

C++——定义一个 Box(盒子)类,在该类定义中包括数据成员和成员函数

完整代码&#xff1a; /*定义一个 Box(盒子)类&#xff0c;在该类定义中包括数据成员和成员函数。 数据成员&#xff1a;length &#xff08;长&#xff09;、width&#xff08;宽&#xff09;和 height&#xff08;高&#xff09;&#xff1b; 成员函数&#xff1a;构造函数 …

阿里云双十一大促:云服务器1年99元,新老同享,续费同价!

2023年双十一购物狂欢节来临&#xff0c;阿里云推出了金秋云创季活动&#xff0c;活动力度很大&#xff0c;不再是老用户与狗不得入内&#xff0c;2核2G3M云服务器1年99元&#xff0c;新老同享&#xff0c;续费同价&#xff01; 活动地址&#xff1a;传送门>>> 活动详…

【C++】C++11【下】lambda表达式|thread线程库

目录 1、lambda表达式 1.1 lambda表达式的引入 1.2 lambda表达式的语法 1.3 lambda表达式的原理 2、线程库 2.1thread类的介绍 2.2 线程函数参数 2.3 原子性操作库(atomic) 2.4 使用场景 应用场景1&#xff1a; 应用场景2: 应用场景3&#xff1a; 应用场景4&#xf…

SAFe大规模敏捷框架

Leangoo领歌是一款永久免费的专业的敏捷开发管理工具&#xff0c;提供端到端敏捷研发管理解决方案&#xff0c;涵盖敏捷需求管理、任务协同、进展跟踪、统计度量等。 
 Leangoo领歌上手快、实施成本低&#xff0c;可帮助企业快速落地敏捷&#xff0c;提质增效、缩短周期、加速…

与AI对话的艺术:如何优化Prompt以获得更好的响应反馈

前言 在当今数字化时代&#xff0c;人工智能系统已经成为我们生活的一部分。我们可以在智能助手、聊天机器人、搜索引擎等各种场合与AI进行对话。然而&#xff0c;要获得有益的回应&#xff0c;我们需要学会与AI进行有效的沟通&#xff0c;这就涉及到如何编写好的Prompt。 与…

【Linux学习笔记】进程概念(下)

进程地址空间1. 虚拟地址2. 什么是进程地址空间3. 进程地址空间的映射。4. 地址空间存在的意义5. 写时拷贝 进程地址空间 1. 虚拟地址 来看这样一段代码。 #include <stdio.h> #include <unistd.h>int global_value 100;int main() {pid_t id fork();if(id &l…

3206. 拼图

给出一个 nm 的方格图&#xff0c;现在要用如下 L 型的积木拼到这个图中&#xff0c;使得方格图正好被拼满&#xff0c;请问总共有多少种拼法。 其中&#xff0c;方格图的每一个方格正好能放积木中的一块。 积木可以任意旋转。 输入格式 输入的第一行包含两个整数 n,m&#xff…

scrapy+selenium框架模拟登录

目录 一、cookie和session实现登录原理 二、模拟登录方法-Requests模块Cookie实现登录 三、cookiesession实现登录并获取数据 四、selenium使用基本代码 五、scrapyselenium实现登录 一、cookie和session实现登录原理 cookie:1.网站持久保存在浏览器中的数据2.可以是长期…

3D视觉引导工业机器人上下料,助力汽车制造业实现智能化生产

在工业制造领域&#xff0c;机器人技术一直是推动生产效率和质量提升的重要力量。近年来&#xff0c;随着3D视觉技术的快速发展&#xff0c;工业机器人在处理复杂任务方面迈出了重要的一步。特别是在汽车制造行业&#xff0c;3D视觉引导工业机器人的应用已经取得了令人瞩目的成…

dockefile

文章目录 应用的部署MySql的部署Tomcat的部署 dockerfileDocker原理镜像的制作容器转镜像Dockerfile 服务编排Docker Compose Docker 私有仓库 应用的部署 搜索app的镜像拉去app的镜像创建容器操作容器中的app MySql的部署 容器内的网络服务和外部机器无法直接通信外部机器和…

软件测试 —— 移动端测试

1. 移动端 指移动设备&#xff08;如智能手机、平板电脑、智能手表等&#xff09;上的操作系统和应用程序。移动设备具有便携性和多功能性&#xff0c;可以随时随地连接互联网&#xff0c;提供丰富的应用和服务。 2. 移动端应用分类 (1) 原生应用&#xff08;Native App&…

访问单通道Mat中的值之at()、ptr()、iscontinuous()【C++的OpenCV 第十四课-OpenCV基础强化(二)】

&#x1f389;&#x1f389;&#x1f389; 欢迎各位来到小白 p i a o 的学习空间&#xff01; \color{red}{欢迎各位来到小白piao的学习空间&#xff01;} 欢迎各位来到小白piao的学习空间&#xff01;&#x1f389;&#x1f389;&#x1f389; 目录 一、访问的方法 \color{blu…

voronoi diagram(泰森多边形) 应用 - Empire Strikes Back

欢迎关注更多精彩 关注我&#xff0c;学习常用算法与数据结构&#xff0c;一题多解&#xff0c;降维打击。 voronoi 图求解点击前往 题目链接&#xff1a;https://vjudge.net/problem/URAL-1520 题目大意 有一个城市&#xff0c;形状是圆形。 城市里有很多化工场。 现在想…

matlab双目标定中基线物理长度获取

在MATLAB进行双目摄像机标定时,通常会获得相机的内参,其中包括像素单位的焦距(focal length)以及物理单位的基线长度(baseline)。对于应用中的深度估计和测量,基线长度的物理单位非常重要,因为它直接影响到深度信息的准确性。有时候,您可能只能获取像素单位的焦距和棋…

华为RS设备状态及接口配置命令

1、查看硬件信息 ①查看序列号 查看整机序列号 display esn display sn ②、查看功率 电源功率 display power 查看光模块功率 display transceiver interface gigabitethernet 1/0/0 verbose ③、查看风扇 display fan ④、查看温度 display temperature all ⑤、查看硬…

微信聚合聊天系统的便捷功能:自动发圈,跟圈

快到双十一咯&#xff0c;很多商家和自媒体、运营人都在发圈做运营&#xff0c;所以现在发圈的频率也会比以往的多一些&#xff0c;但事情一多就会担心今天的朋友圈忘记发、漏发或者错过发圈的时间导致错过私域里的好友、客户会错过活动时间。 其实这些都是可以不用担心&#…

Python 正则表达式(RegEx)指南

正则表达式&#xff08;RegEx&#xff09;是一系列字符&#xff0c;形成了一个搜索模式。RegEx 可用于检查字符串是否包含指定的搜索模式。 RegEx 模块 Python 中有一个内置的包叫做 re&#xff0c;它可以用于处理正则表达式。导入 re 模块&#xff1a; import rePython 中的…

怪物猎人世界Mod制作——替换模型、音效

太喜欢《怪物猎人&#xff1a;世界》这款游戏了&#xff0c;在冰原更新后&#xff0c;游戏版本趋于稳定。卡普空做一些bug修复后宣布不再更新此游戏&#xff0c;游戏版本稳定在v15.11.01。从此这个游戏长达三年未更新&#xff0c;我玩了八百小时也未发现什么明显BUG&#xff0c…

Linux多虚拟主机和配置限制访问与日志

目录 一、多虚拟主机 1.配置单网卡多个ip 2.给每个主机站点设置主页 3.测试访问 二、限制访问 1.限制所有 2.放行192.168.0.0/24网段访问 三、日志与状态页 1.定义访客日志 2.状态页配置 一、多虚拟主机 1.配置单网卡多个ip ip address add 192.168.0.231/24 dev e…