探索一致性哈希算法以及在 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) {
    	// 遍历所有 Invoker
        String 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 对象后,负载均衡策略执行完毕,后序获取远程调用客户端,执行调用流程。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

国产AI大模型推荐(一)

文心一言 主要功能&#xff1a; 各种类型的问答、各种文本创作、推理与数学计算、写代码、聊天交流、图片生成等。 链接&#xff1a;文心一言 讯飞星火 特点&#xff1a; 内容生成能力&#xff1a;我可以进行多风格多任务长文本生成&#xff0c;例如邮件、文案、公文、作文、对…

汇总:五个开源的Three.js项目

Three.js 是一个基于 WebGL 的 JavaScript 库&#xff0c;它提供了一套易于使用的 API 用来在浏览器中创建和显示 3D 图形。通过抽象和简化 WebGL 的复杂性&#xff0c;Three.js 使开发者无需深入了解 WebGL 的详细技术就能够轻松构建和渲染3D场景、模型、动画、粒子系统等。 T…

使用Node.js常用命令提高开发效率

Node.js是一个基于Chrome V8引擎的JavaScript运行时环境&#xff0c;广泛用于构建服务器端应用程序和命令行工具。Node.js提供了丰富的命令和工具&#xff0c;可以帮助开发者更高效地开发应用程序。在日常开发中&#xff0c;除了Node.js本身的核心功能外&#xff0c;npm&#x…

蓝桥杯习题

https://www.lanqiao.cn/problems/1265/learning/ 第一题---排序 给定一个长度为N的数组A&#xff0c;请你先从小到大输出它的每个元素&#xff0c;再从大到小输出他的每个元素。 输入描述&#xff1a; 第一行包含一个整数N 第二行包含N个整数a1,a2,a3,...an&#xff0c;表…

牛客周赛 Round 38(A,B,C,D,E,F,G)

比赛链接 官方讲解&#xff08;不分P不分段直接两小时怼上来是坏文明 &#xff09; 这场的题很棒&#xff0c;思维有难度&#xff0c;考察的知识点广泛&#xff0c;有深度&#xff0c;很透彻。感觉学到了很多。建议补题。 A 小红的正整数自增 思路&#xff1a; 签到。 可以…

k8s的pod访问service的方式

背景 在k8s中容器访问某个service服务时有两种方式&#xff0c;一种是把每个要访问的service的ip注入到客户端pod的环境变量中&#xff0c;另一种是客户端pod先通过DNS服务器查找对应service的ip地址&#xff0c;然后在通过这个service ip地址访问对应的service服务 pod客户端…

Python面对对象 - 类的反射机制

Python面对对象类的反射机制是面向对象编程语言中比较重要的功能&#xff0c;可以动态获取对象信息以及动态调用对象。通过字符串形式的类名或属性来访问对应类或属性。 一、对象的反射 1. getattr 获取指定字符串名称的对象属性、方法&#xff1a; 当访问的属性不存在时&#…

AtCoder Beginner Contest 347(A~D)

A - Divisible 如果序列里面的数能被k整除&#xff0c;就整除后输出 #include <bits/stdc.h> //#define int long long #define per(i,j,k) for(int (i)(j);(i)<(k);(i)) #define rep(i,j,k) for(int (i)(j);(i)>(k);--(i)) #define debug(a) cout<<#a<…

Android ImageView 的scaleType 属性图解

目录 前言测试素材测试布局xmlscaleType前言 一、ScaleType.FIT_CENTER 默认二、ScaleType.FIT_START三、ScaleType.FIT_END四、ScaleType.FIT_XY五、ScaleType.CENTER六、ScaleType.CENTER_CROP七、ScaleType.CENTER_INSIDE八、ScaleType.MATRIX 前言 原文链接&#xff1a; A…

macOS搭建php环境以及调试Symfony

macOS搭建php环境以及调试Symfony macOS搭建php环境以及调试Symfony 古老的传说运行环境快速前置安装环境 php 的安装安装 Xdebug 来调试 php如何找到你的 php.iniXdebug 安装成功 创建并调试的 Hello world 安装 PHP Debug 安装 Symfony 安装 Composer安装 Symfony CLI 创建 …

tcpdump + wireshark 服务器抓包分析

tcpdump wireshark 服务器抓包分析 1.tcpdump安装2.tcpdump使用3.安装wireshark4.使用wireshark 本文用以总结使用tcpdump进行抓包&#xff0c;然后使用wireshark工具打开抓包出来的pacp文件进行分析。通过tcpdump可以实时监控到linux服务器中tcp和http、https等通讯的内容和信…

HarmonyOS 应用开发之FA模型访问Stage模型DataShareExtensionAbility

概述 无论FA模型还是Stage模型&#xff0c;数据读写功能都包含客户端和服务端两部分。 FA模型中&#xff0c;客户端是由DataAbilityHelper提供对外接口&#xff0c;服务端是由DataAbility提供数据库的读写服务。 Stage模型中&#xff0c;客户端是由DataShareHelper提供对外接…

【重学C语言】二、C语言简介和开发工具

【重学C语言】二、C语言简介和开发工具 C语言发展史C语言标准变迁C语言特点开发软件CLion安装步骤 VIsual Studio安装步骤 Clion 和 VS2022 绑定 VS 项目创建项目 Clion 项目(本博主主用)创建项目Clion 配置 构建类型构建模式 C语言发展史 1970年&#xff0c;美国 AT&T 公…

leetcode.209.长度最小的子数组

题目 给定一个含有 n 个正整数的数组和一个正整数 s &#xff0c;找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组&#xff0c;并返回其长度。如果不存在符合条件的子数组&#xff0c;返回 0。 示例&#xff1a; 输入&#xff1a;s 7, nums [2,3,1,2,4,3] 输出&#…

JDK和IntelliJ IDEA下载和安装及环境配置教程

一、JDK下载&#xff08;点击下方官网链接&#xff09; Java Downloads | Oracle 选择对应自己电脑系统往下拉找到自己想要下载的JDK版本进行下载&#xff0c;我下的是jdk 11&#xff0c;JDK有安装版和解压版&#xff0c;我就直接下安装版的了。 .exe和.zip的区别&#xff1a…

设计模式10--适配器模式

定义 案例一 案例二 优缺点

稀碎从零算法笔记Day34-LeetCode:最小栈

感谢耶稣&#xff0c;笔者又过了一天“复活节”。月末斩杀一道Hard画上句号 题型&#xff1a;栈、模拟数据结构 链接&#xff1a;155. 最小栈 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述 设计一个支持 push &#xff0c;pop &#xff0c;…

「Android高级工程师」BAT大厂面试基础题集合-下-Github标星6-5K

C、 com.android.provider.contact D、 com.android.provider.contacts 11.下面关于ContentProvider描述错误的是&#xff08;&#xff09;。 A、 ContentProvider可以暴露数据 B、 ContentProvider用于实现跨程序共享数据 C、 ContentProvider不是四大组件 D、 ContentP…

阿里云轻量应用服务器优惠价格表,61元和165元一年

阿里云轻量应用服务器2核2G和2核4G配置优惠价格表&#xff0c;轻量2核2G3M带宽61元一年&#xff0c;轻量2核4G4M带宽165元1年&#xff0c;均不限制月流量&#xff0c;阿里云活动链接 aliyunfuwuqi.com/go/aliyun 活动打开如下图&#xff1a; 阿里云轻量应用服务器价格 61元/年…

线程池详解、核心参数、拒绝策略

什么是线程池 线程池是一种池化技术&#xff0c;它预先创建一组线程&#xff0c;用于执行异步任务。当有新任务到来时&#xff0c;线程池可以立即分配一个线程来处理&#xff0c;而不需要临时创建。这样可以减少因为频繁创建和销毁线程而导致的开销。 线程池的应用场景 高并…