探索一致性哈希算法以及在 Dubbo 负载均衡中的应用

文章目录

  • 负载均衡简介
  • 基于哈希算法的负载均衡策略
    • 传统哈希算法
    • 一致性哈希算法
    • 虚拟一致性哈希算法
  • 一致性哈希在 Dubbo 中的应用
    • ConsistentHashSelector 构造方法
    • ConsistentHashSelector select方法

负载均衡简介

负载均衡(Load Balance,简称 LB) 是一种技术,用于 将请求按照某种策略分配给后端集群中的不同机器进行处理,缓解单点服务器的高负载,提升服务整体的吞吐量和可用性。

负载均衡的作用如下:

  1. 提高系统的可靠性和可用性:负载均衡可以防止任何单点故障影响整个系统,如果某个服务器失败,负载均衡器下线出现故障的机器,并将流量重定向到其他健康的服务器上。
  2. 优化资源的利用率:一个良好的负载均衡策略能充分利用集群中所有机器的资源,没有机器处于过载或空闲的状态。
  3. 提升吞吐量:负载均衡可以分散流量到最少负载的服务器或地理位置最近的服务器,减少用户请求的响应时间,提升服务整体的吞吐量。
  4. 提供横向扩展的能力:当集群不能支撑现有的业务流量时,可以通过增加更多服务器来扩展服务的处理能力,灵活应对不断变化的负载需求。

负载均衡的类型:

  • 四层负载均衡(L4):在网络传输层进行,主要 根据 IP 地址端口号来分发流量。比较著名的 L4 软件有:Linux Virtual Server(LVS)、HAProxy。
  • 七层负载均衡(L7):处理应用层数据,根据HTTP头部、URL、甚至请求的具体内容来进行智能路由和负载分配。比较著名的 L7 软件有:Nginx、HAProxy、Apache HTTP Server。

基于哈希算法的负载均衡策略

传统哈希算法

适用传统的哈希算法实现数据负载均衡的步骤如下:

  1. 添加、查询还是删除数据,都先将数据的 id(例如:Redis 的 key)通过哈希函数(如:CRC16)转换成一个哈希值 hash;
  2. 如果集群中有 N 台机器,计算 hash % N 的值,这个值就是数据所属的机器编号,该条数据的增删改查都是在这台机器上进行。

示意图如下:



这种负载策略算法的缺点增加或删除机器(N 变化),会导致所有的数据不得不根据 id 重新计算一遍哈希值,并将哈希值对新的机器数进行取模运算,然后执行大规模数据迁徙。

假设增加一台 Redis 实例 server_2,键值对的分布需要重新计算哈希值,示意图如下:


一致性哈希算法

一致性哈希算法就是为了解决上述问题而产生的,假设数据的 id 通过哈希函数转换成的哈希值范围是 2 32 2^{32} 232,即 0 ∼ 2 32 − 1 0\sim2^{32}-1 02321 的区间内。可以将这些数据头尾相连,形成一个闭环,如下图所示:

数据 id 计算出哈希值后,对应到环上的一个位置。


假设集群中存在三台机器,根据机器 id 计算出的哈希值得到机器在环上的位置。

数据如何确定属于哪台机器

  1. 根据数据 id 用哈希函数计算出哈希值,映射到环中相应的位置;
  2. 从数据映射的位置开始,顺时针找寻最近的机器,那台机器负责该数据的增删改查。

以上图为例,离 key1 顺时针最近的是机器 ③、key4 属于机器 ① 管理、key2 和 key3 属于机器 ② 管理。


如何增加节点

如果新加入机器 4(如下图所示),由机器 ① 到机器 ④ 掌管的数据先前由机器 ② 负责管理,现在需要将这部分数据迁徙到机器 ④。即 key2 所代表的数据需要从机器 ② 迁移至机器 ④。


虚拟一致性哈希算法

如果机器数量较少,很有可能机器在整个环上的分布不均匀,从而导致机器之间的负载不均衡。如下图,机器 ① 到机器 ② 的距离远远大于机器 ② 到机器 ①,这导致大多数数据将分配至机器 ②,引起机器负载不均:


为了解决这种数据倾斜问题,一致性哈希算法引入了 虚拟节点机制,对每一台机器 通过不同的哈希函数计算出多个哈希值,对多个位置都放置一个服务节点,称为虚拟节点。

具体的做法可以在 机器 IP 地址或主机名的后面增加编号 来实现。例如存在两台机器 m1、m2,每台机器有两个虚拟节点,分别计算 m1-1、m1-2、m2-1、m2-2 的哈希值,形成四个虚拟节点,节点数变多后哈希函数的散列效果更好,集群负载的平衡性也就更好。


当某一条数据计算出哈希值并找到最近的一个虚拟节点时,根据虚拟节点到实际机器的映射关系找到实际机器,数据将最终归属于实际的机器。同理,虚拟节点间的数据迁移、更新删除等操作,也都是根据虚拟节点到实际机器的映射,找到实际机器完成相应操作

让每台机器分配数量较多的虚拟节点抢占哈希环,数量多起来后哈希函数的离散型可以得到很好的体现,每台机器就可以按照所占虚拟节点的比例来分配负载。


一致性哈希在 Dubbo 中的应用

Dubbo 中的负载均衡:服务消费者 Consumer 会 从所有服务提供者 Provider 中选出一个实例进行远程过程调用(RPC),在代码中 Provider 封装为 Invoker 实例。通俗的讲,负载均衡策略就是从 Invoker 列表中选出一个 Invoker,供 Consumer 完成调用。

Dubbo 的一致性哈希算法分为两步:

  1. 映射 Provider(实际为 Invoker) 至 Hash 值区间中;
  2. 映射请求,然后找到大于请求Hash值的第一个Invoker;

Dubbo 中所有负载均衡实现类继承自抽象类 AbstractLoadBalance,调用 LoadBalance#select 方法时,实际调用的是 AbstractLoadBalance#select

@Override
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {if (CollectionUtils.isEmpty(invokers)) {return null;}if (invokers.size() == 1) {return invokers.get(0);}// 执行负载均衡实现类的doSelect方法return doSelect(invokers, url, invocation);
}

Dubbo 一致性哈希的具体实现类为 ConsistentHashLoadBalance

@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {String methodName = RpcUtils.getMethodName(invocation); // 方法名称String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName; // 服务名称int identityHashCode = System.identityHashCode(invokers); // Invoker列表的哈希值ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key); // 获取指定服务的选择器// 选择器没有初始化 或 invoker列表发生了变更, 初始化一个ConsistentHashSelector实例if (selector == null || selector.identityHashCode != identityHashCode) {selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));selector = (ConsistentHashSelector<T>) selectors.get(key);}return selector.select(invocation);
}

方法步骤说明:

  1. 通过入参 invocation 获取方法调用信息,包括:方法名 methodName、服务唯一标识 key、服务提供者列表 invokes 的哈希码 identityHashCode;
  2. 从 ConcurrentHashMap 中获取特定服务的选择器;
  3. 当出现如下情况时,则初始化一个选择器:
    • 不存在服务选择器(select == null);
    • 服务提供者集群发生了变更 (selector.identityHashCode != identityHashCode),例如实例增加、删除、ip 地址更改等。
  4. 执行选择器的 select 方法,获取一个 Invoker 实例用于远程调用。

ConsistentHashSelector 构造方法

一致性哈希策略的选择器实现类为 ConsistentHashSelector,它包含如下属性:

private static final class ConsistentHashSelector<T> {// 虚拟节点哈希值到 Invoker 实例的映射private final TreeMap<Long, Invoker<T>> virtualInvokers;// private final int replicaNumber;// 识别 Invoker 列表是否发生变化的哈希码private final int identityHashCode;// 请求中用来做哈希映射的参数索引private final int[] argumentIndex;// ...
}

在新建 ConsistentHashSelector 时,会遍历所有 Invoker 对象,计算出

ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {// 使用红黑树存储 Invoker 实例, 查询、插入、删除性能比较平衡this.virtualInvokers = new TreeMap<Long, Invoker<T>>();this.identityHashCode = identityHashCode;URL url = invokers.get(0).getUrl();// 获取每个 Invoker 实例的虚拟节点数目, 默认为160个, 可以通过hash.nodes属性配置this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);// 解析方法中哪些参数用于计算哈希值(通过URL对象中的方法参数得到)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]);}for (Invoker<T> invoker : invokers) {// 遍历所有 InvokerString address = invoker.getUrl().getAddress(); // Invoker实例地址for (int i = 0; i < replicaNumber / 4; i++) {// url后拼接序号i后执行md5运算, 得到16字节的摘要byte[] digest = md5(address + i);// 16字节的摘要, 可以得到四个 32位哈希值for (int h = 0; h < 4; h++) {long m = hash(digest, h);// 每个哈希值相当于一个虚拟节点, virtualInvokers维护虚拟节点到Invoker实例的映射virtualInvokers.put(m, invoker);}}}
}

上述代码生成虚拟节点哈希值的过程(以 replicaNumber 取默认值 160 为例):

  1. 假设当前遍历到的 Invoker 地址为 192.168.0.1:20880。该方法会依次计算 192.168.0.1:208800192.168.0.1:208801、… 、192.168.0.1:2088039 的 md5 摘要。
  2. 利用 md5 生成的 16 字节摘要,生成四个 32 位哈希值,每个哈希值相当于一个虚拟节点,virtualInvokers 维护虚拟节点到 Invoker 实例的映射。

hash 方法将 digest 字节数组划分为 4 个部分,每个部分 4字节。每次调用返回 number 指定部分的 32 位整数作为哈希值。

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;
}

以图中所示的摘要数组为例,图中代码将根据 number 返回四个不同的哈希值:



注:Dubbo 使用 URL 传递方法级别参数,例如

dubbo://192.168.0.1:20880/cn.wzz.demo?serialization=fastjson& 
method.selectUsers.timeout=5000&
method.selectUsers.retries=2

URL 的参数中,名称包含 method 前缀的为方法参数,method.selectUsers 表示方法名为 selectUsers 的方法参数。


总结:一致性哈希选择器的构造方法会 为特定服务的每个 Invoker 实例创建 replicaNumber 个虚拟节点,不同的 hash 值标识虚拟节点,通过 virtualInvokers 维护虚拟节点到 Invoker 实例的映射关系。

ConsistentHashSelector select方法

介绍完选择器的构造方法后,我们来看选择器的 select 方法如何挑选出处理请求的 Invoker 实例。

public Invoker<T> select(Invocation invocation) {// 根据远程调用的参数值确定key, 默认使用第一个参数计算String key = toKey(invocation.getArguments());// md5 哈希算法计算key的摘要byte[] digest = md5(key);return selectForKey(hash(digest, 0));
}// 将参数列表args中, 由argumentIndex指定下标的参数值拼接成字符串
private String toKey(Object[] args) {StringBuilder buf = new StringBuilder();for (int i : argumentIndex) {if (i >= 0 && i < args.length) {buf.append(args[i]);}}return buf.toString();
}// 从红黑树virtualInvokers 中查找哈希值大于等于hash的第一个 Invoker 实例
private Invoker<T> selectForKey(long hash) {Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);if (entry == null) {// 找不到大于等于 hash 的虚拟节点, 回到哈希环起始位置, 找出哈希值最小的节点entry = virtualInvokers.firstEntry();}return entry.getValue();
}

select 方法的步骤如下:

  1. 从 Invocation 对象中获取远程调用的参数列表,利用 toKey 方法获取参数值拼接而成的字符串 key。
  2. 使用 md5 哈希算法计算 key 的摘要 digest;
  3. 取 digest 的低位 4 字节作为哈希值,使用 selectForKey 方法从红黑树中找到 大于等于 hash 的首个 Invoker 对象。如果找不到(entry == null),此时应该 跳转到哈希环的起始位置,即找到哈希值最小的 Invoker 对象

selectForKey 中获取到 Invoker 对象后,负载均衡策略执行完毕,后序获取远程调用客户端,执行调用流程。

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

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

相关文章

WPF中获取TreeView以及ListView获取其本身滚动条进行滚动

实现自行调节scoll滚动的位置(可相应获取任何控件中的内部滚动条) TreeView:TreeViewAutomationPeer lvap new TreeViewAutomationPeer(treeView); var svap lvap.GetPattern(PatternInterface.Scroll) as ScrollViewerAutomationPeer; var scroll svap.Owner as ScrollVie…

【HCIP学习】网络类型级数据链路层协议

思维导图在上面哦~ 一、网络类型的分类&#xff08;4种&#xff09; 出现原因&#xff1a;数据链路层使用的协议及规则不同&#xff0c;造成了不同的网络类型 1、多点接入网络&#xff08;MA&#xff09;------一条网段内上出现多个设备 BMA&#xff1a;广播型多点接入&…

linux内核:ftrace——追踪内核行为

文章目录 1. 简介2. 使用2.1 加入ftrace2.2 ftrace 基础2.2.1 tracer2.2.2 filter&#xff08;可选&#xff09;2.2.3 读取trace2.2.4 ftrace_enabled 2.3 使用function_graph查看do_sys_open的执行过程2.3 使用function查看do_sys_open的执行2.3 使用wakeup2.3 使用wakeup_rt2…

C语言例1-11:语句 while(!a); 中的表达式 !a 可以替换为

A. a!1 B. a!0 C. a0 D. a1 答案&#xff1a;C while()成真才执行&#xff0c;所以!a1 &#xff0c;也就是 a0 原代码如下&#xff1a; #include<stdio.h> int main(void) {int a0;while(!a){a;printf("a\n");} return 0; } 结果如…

JUC:Monitor 与 Java对象头的内容与锁关系

文章目录 Monitorjava对象头Monitor&#xff08;锁、管程&#xff09; Monitor java对象头 普通对象 Mark Word 主要用来存储对象自身的运行时数据、klass word就是指向该对象的类型。 数组对象 mark word 不同对象状态下结构和含义不同。 Monitor&#xff08;锁、管…

SRS OBS利用RTMP协议实现音视频推拉流

参考&#xff1a;https://ossrs.net/lts/zh-cn/docs/v5/doc/getting-started 1&#xff09;docker直接运行SRS服务&#xff1a; docker run --rm -it -p 1935:1935 -p 1985:1985 -p 8080:8080 registry.cn-hangzhou.aliyuncs.com/ossrs/srs:5运行起来后可以http://localho…

数据恢复宝典:揭秘分区合并后的数据拯救之路

在计算机存储管理中&#xff0c;分区合并是一项常见的硬盘操作。它通过将两个或多个相邻的磁盘分区合并成一个更大的分区&#xff0c;来扩展存储空间或简化磁盘管理。然而&#xff0c;这个看似简单的操作背后&#xff0c;却隐藏着数据丢失的巨大风险。许多用户在尝试分区合并时…

ElementUI表格table组件实现单选及禁用默认选中效果

在使用ElementUI&#xff0c;需要ElementUI表格table组件实现单选及禁用默认选中效果, 先看下效果图&#xff1a; 代码如下&#xff1a; <template><el-tableref"multipleTable":data"tableData"tooltip-effect"dark"style"widt…

云原生应用(5)之Dockerfile精讲及新型容器镜像构建技术

一、容器与容器镜像之间的关系 说到Docker管理的容器不得不说容器镜像&#xff0c;主要因为容器镜像是容器模板&#xff0c;通过容器镜像我们才能快速创建容器。 如下图所示&#xff1a; Docker Daemon通过容器镜像创建容器。 二、容器镜像分类 操作系统类 CentOS Ubuntu 在…

深入理解element-plus table二次封装:从理论到实践的全面指南

前言 在许多中后台管理系统中&#xff0c;表格占据着半壁江山&#xff0c;如果使用element plus组件库&#xff0c;那么少不了要用到table组件&#xff0c;可是table组件的功能过于基础&#xff0c;因此&#xff0c;我在table组件的实现基础之上进一步封装&#xff0c;从而实现…

安卓工控一体机主板定制_联发科MTK平台解决方案

新移科技安卓工控一体机方案基于MT8766主芯片&#xff0c;采用四核 Cortex-A53 CPU&#xff0c;搭载Android 12.0系统&#xff0c;主频高达2.0GHz&#xff0c;具有低功耗和高性价比的优势。搭载ARM IMG GE8300 高性能GPU和4G全网通版本的RF&#xff0c;网络连接稳定快速。 可直…

【Node.js】图片验证码识别

现在越来越多的网站采取图片验证码&#xff0c;防止机器恶意向服务端发送请求。但是常规的图片验证码也不是非常安全了。有非常多第三方库可以对图片上的数字文字等进行识别。 代码实现 首先安装依赖&#xff1a; npm install node-native-ocrnpm&#xff1a;(node-native-oc…

经验分享:开源知识库才是企业低成本搭建的最佳选择!

身为企业所有者的你&#xff0c;是否为建设企业的知识库而头疼&#xff1f;想要一个功能全面而又简单易用的知识库&#xff0c;但又担心成本过高&#xff1f;那我今天要分享的内容&#xff0c;可能会给你带来一些启示。那便是&#xff1a;开源知识库便是你企业低成本搭建的最佳…

Tron波场区块链 | 使用Java将Tron钱包助记词转私钥 全网独门一份

如何使用Java将Tron钱包助记词转换为私钥? 本来想着这个问题挺简单&#xff0c;可是查了半天&#xff0c;不是&#xff0c;不止半天查了好长时间&#xff0c;看了半天官网文档&#xff0c;全网Java就没有实现的。 咋办。。。咋办呢&#xff1f; 好巧&#xff0c;官网我看到…

ARM-按键中断实验

代码 #include "stm32mp1xx_gic.h" #include "stm32mp1xx_exti.h" extern void printf(const char *fmt, ...); unsigned int i 0; void do_irq(void) {//获取要处理的中断的中断号unsigned int irqnoGICC->IAR&0x3ff;switch (irqno){case 99:pr…

C++奇迹之旅(三):缺省参数与函数重载

文章目录 &#x1f4dd;缺省参数分类&#x1f320; 缺省参数概念&#x1f309;缺省参数分类 &#x1f320;全缺省参数&#x1f309;半缺省参数 &#x1f320; 函数重载&#x1f309; 函数重载概念&#x1f320;参数类型不同&#x1f320;参数个数不同&#x1f320;参数类型顺序…

CQI-17:2021 V2 英文 、中文版。特殊过程:电子组装制造-锡焊系统评审标准

锡焊作为一个特殊的工艺过程&#xff0c;由于其材料特性的差异性、工艺参数的复杂性和过程控制的不确定性&#xff0c;长期以来一直视为汽车零部件制造业的薄弱环节&#xff0c;并将很大程度上直接导致整车产品质量的下降和召回风险的上升。 美国汽车工业行动集团AIAG的特别工…

2024年2月游戏手柄线上电商(京东天猫淘宝)综合热销排行榜

鲸参谋监测的线上电商&#xff08;京东天猫淘宝&#xff09;游戏手柄品牌销售数据已出炉&#xff01;2月游戏手柄销售数据呈现出强劲的增长势头。 根据鲸参谋数据显示&#xff0c;今年2月游戏手柄月销售量累计约43万件&#xff0c;同比去年上涨了78%&#xff1b;销售额累计达1…

武汉星起航:跨境电商获各大企业鼎力支持,共筑繁荣生态

随着全球化和数字化的深入发展&#xff0c;跨境电商行业逐渐成为连接国内外市场的重要桥梁。在这一进程中&#xff0c;各大企业纷纷加大对跨境电商行业的支持力度&#xff0c;通过投资、合作与创新&#xff0c;共同推动行业的繁荣与发展。武汉星起航将探讨各大企业对跨境电商行…

Linux安装python3

Linux安装python3 本文章中使用的安装包等相关文件&#xff1a; 链接: https://pan.baidu.com/s/1C4PTB6IqXtHM6XSOEMkefg 提取码: wyeq 1.编译环境安装 yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc mak…