0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

一致性哈希算法简介

算法与数据结构 来源:labuladong 2023-04-04 11:53 次阅读

最近有一位读者跟我交流,说除了算法题之外,系统设计题是一大痛点。算法题起码有很多刷题平台可以动手实践,但系统设计类的题目一般很难实践,所以看一些教程总结也只是一知半解,遇到让写代码实现系统的就懵了。

比如他最近被问到一个大型爬虫系统的设计题,让手写一致性哈希算法,加上一系列 follow up,就被难住了。

说实话这个算法的实现并不难,所以本文就结合一致性哈希算法在工程中的应用场景介绍一下这个算法算法,并给出代码实现,避免大家重蹈覆辙。

一致性哈希算法简介

这个名词大家肯定不陌生,应该也大概理解这个算法的逻辑,不过我这里还是要再介绍一下。

一致性哈希主要解决把数据平均分配到多个节点上的问题,并且某些节点上线/下线后依然能够做到自动负载均衡。

其原理就是抽象出一个哈希环,把服务器节点的 id 通过哈希函数映射到这个哈希环上:

ca0e0d10-d298-11ed-bfe3-dac502259ad0.jpg

同时,把需要处理的数据也通过哈希函数映射到这个哈希环上,然后顺时针找,遇到的第一个服务器节点来负责处理这个数据:

ca202ab8-d298-11ed-bfe3-dac502259ad0.jpg

这样一来,我们其实已经提供了一种机制将若干数据分布在若干服务节点上了,不妨称它为 V1 版本的一致性哈希算法。

但 V1 版本的算法还有问题:负载分布很可能不均衡。由于哈希函数的结果是难以预测的,所以可能出现这种情况:

ca2baf28-d298-11ed-bfe3-dac502259ad0.jpg

即某些服务器节点要负责的哈希环很长,而其他服务器负责的哈希环很短。这就会导致某些服务器负载很高,而其他的服务器负载很低,很不均衡。

而且,当某个服务器节点下线后,它的负载会顺时针转移到下一个节点上,那么某些特定的节点下线顺序很可能导致某些服务器节点负责的哈希环不断加长,负载不断增加。专业点说,这就是「数据倾斜」。

如何解决数据倾斜的问题呢?可以给每个服务器节点添加若干「虚拟节点」分布在哈希环上,我们不妨称其为 V2 版的一致性哈希算法:

ca39e728-d298-11ed-bfe3-dac502259ad0.jpg

上图给每个 Node 设置了 4 个虚拟节点,这样一来,由于哈希函数的随机性,每个服务器节点的虚拟节点能够平均分布在哈希环上,那么数据就能够比较均匀地分配到所有服务器节点上。

如果某个服务器节点下线,那么该服务器节点的所有虚拟节点都会从哈希环上摘除,它们的负载会迁移到顺时针的下一个服务器节点上。

和 V1 版算法不同的是,因为虚拟节点有多个,它们的下一位不太可能是同一台服务器的虚拟节点,所以它们的负载大概率会均分到多台服务器的虚拟节点上。

综上,V2 版算法通过虚拟节点的方式完美解决了数据倾斜的问题,是不是很巧妙?不过俗话说,纸上得来终觉浅,绝知此事要躬行,我们需要实践才能真正写出正确的一致性哈希算法。

比方说,应该用什么数据结构实现哈希环?如果哈希函数出现哈希冲突怎么办?真正写代码的时候,这些细节问题都是要考虑的。

下面我们就结合代码和实际场景来看看一致性哈希算法的真实应用

实际场景分析

就以消息队列的消费模型为例吧,我在前文用消息队列做一个联机游戏介绍过 Apache Pulsar 的消费模型,Pulsar 通过 subscription 的抽象提供多种订阅模式,其中 key_shared 模式比较有意思:

每条消息会有一个 key,Pulsar 可以根据这个 key 分发消息,保证带有相同 key 的消息分发到同一个消费者上。

官网的这幅图比较好理解,图中的K就是指消息的 key,V就是指消息的数据:

ca532dfa-d298-11ed-bfe3-dac502259ad0.png

通过某些算法,所有的消息都会有消费者去处理,比如上图就是consumerA负责处理key=K3的消息,consumerB负责处理key=K1的消息,consumerC负责处理key=K2的消息。

当然,如果有 consumer 加入或者离开,消息的分配会重置。比如consumerA下线,那么key=K3的消息会被分配给其他消费者消费;如果有新的消费者consumerD加入,那么当前的分配方案也可能会改变。

简单总结就是:

1、在没有 consumer 加入或者离开的前提下,保证 key 相同的消息都会分配到固定的 consumer,不能一会儿分配到consumerA,一会儿分配给consumerB。

2、如果有 consumer 加入或者离开,可以重新进行分配每个 consumer 负责的 key,要求尽量把 key 平均分配给 consumer,避免出现某些 consumer 负责过多 key 的情况导致数据倾斜。

3、以上两个操作,尤其是给 consumer 重新分配 key 的操作,效率要尽可能高。

对于上述场景,你如何设计分配算法,把这些带有 key 的消息高效地、均匀地分配给所有 consumer 呢

我们来看看 Pulsar 是如何做的,官网对这部分的实现原理描述的比较清楚,参考链接如下:

https://pulsar.apache.org/docs/next/concepts-messaging/#key_shared

结合我之前在学习开源项目的套路中介绍的查看源码背景信息的技巧,可以发现 Pulsar 的 key_shared 模式的消费者实现其实是经历了一些演进的。

最开始的默认实现方式叫做 Auto-split Hash Range,即抽象出来一个[0, 65535]的哈希区间,让每个 consumer 负责这个区间的一部分。比如有C1~C44 个 consumer,那么它们会平分整个哈希区间:

016,38432,76849,15265,536
|-------C3------|-------C2------|-------C4------|-------C1------|

然后我们可以对每条消息的 key 计算哈希值并求模映射到[0, 65535]的区间中,这样我们就可以选出负责处理这条消息的 consumer 了,而且 key 相同的消息总会分配到这个 consumer 上。

那么如果有 consumer 上线或者下线怎么处理呢?

如果有 consumer 下线,那么它负责的哈希区间会直接交给右侧的 consumer。比如上例中C4下线,那么哈希区间就会变成这样:

016,38432,76865,536
|-------C3------|-------C2------|----------------C1---------------|

当然这里也有个特殊情况,就是下线的那个 consumer 右边没有其他 consumer 的情况,我们可以让它左边的 consumer 顶上来。比如现在的C1下线,那么就让左边的C2负责C1的区间:

016,38465,536
|-------C3------|--------------------------C2-----------------------|

如果有 consumer 上线,那么算法可以把最长的哈希区间平分,分一半给新来的 consumer。比如此时C5上线,我们就可以把C2负责的一半哈希区间分给C5:

016,38440,96065,536
|-------C3------|-----------C5-----------|----------C2----------|

这就是 Auto-split Hash Range 的方案,不算复杂,具体的实现可以看 Pulsar 源码中HashRangeAutoSplitStickyKeyConsumerSelector这个类,我在这里就不列举了。

这个方案的问题主要还是数据倾斜,比如上面的例子出现的这种情况,C2的负载显然比C3多很多:

016,38465,536
|-------C3------|--------------------------C2-----------------------|

按照这个算法逻辑,一些 consumer 下线后很容易产生这种数据倾斜的情况,所以这个解决方案并不能均匀地把 key 分配给 consumer

那么如何优化这个算法呢?就要用到一致性哈希算法了。

一致性哈希算法的实现

结合我在本文开头对一致性哈希算法的介绍,应该很容易想到优化思路。其实现在 Pulsar 就是使用一致性哈希算法来实现的 key_shared 订阅。

首先抽象出一个值在[0, MAX_INT]的哈希环,然后给每个 consumer 分配 100 个虚拟节点映射到这个哈希环上。接下来,我们把 key 的哈希值放在哈希环上,顺时针方向找到最近的 consumer 虚拟节点,也就找到了负责处理这个 key 的 consumer。

哈希环我们一般用 TreeMap 实现,直接看 Pulsar 源码中ConsistentHashingStickyKeyConsumerSelector的实现吧,我提取了其中的关键逻辑并添加了详细的注释:

classConsistentHashingStickyKeyConsumerSelector{
//哈希环,虚拟节点的哈希值->consumer列表
//因为存在哈希冲突,多个虚拟节点可能映射到同一个哈希值,所以值为List类型
NavigableMap>hashRing=newTreeMap<>();
//每个consumer有100个虚拟节点
intnumberOfPoints=100;

//将该consumer的100个虚拟节点添加到哈希环上
publicvoidaddConsumer(Consumerconsumer){
for(inti=0;i< numberOfPoints; i++) {
            // 计算虚拟节点在哈希环上的位置
            String key = consumer.consumerName() + i;
            int hash = Murmur3_32Hash.getInstance().makeHash(key.getBytes());
            // 把虚拟节点放到哈希环上
            hashRing.putIfAbsent(hash, new ArrayList<>());
hashRing.get(hash).add(consumer);
}
}

//在哈希环上删除该consumer的所有虚拟节点
publicvoidremoveConsumer(Consumerconsumer){
for(inti=0;i< numberOfPoints; i++) {
            // 计算虚拟节点在哈希环上的位置
            String key = consumer.consumerName() + i;
            int hash = Murmur3_32Hash.getInstance().makeHash(key.getBytes());
            // 删除虚拟节点
            if (hashRing.containsKey(hash)) {
                hashRing.get(hash).remove(consumer);
            }
        }
    }

    // 通过 key 的哈希值选择 consumer
    public Consumer select(int hash) {
        if (hashRing.isEmpty()) {
            return null;
        }
        // 选择顺时针方向的第一个 consumer
        Map.Entry>ceilingEntry=hashRing.ceilingEntry(hash);
ListconsumerList;
if(ceilingEntry!=null){
consumerList=ceilingEntry.getValue();
}else{
//哈希环顺时针转一圈,回到开头寻找第一个节点
consumerList=hashRing.firstEntry().getValue();
}
//保证相同的key都会分配到同一个consumer
returnconsumerList.get(hash%consumerList.size());
}
}

当消息被发送过来后,Pulsar 可以通过select方法选择对应的 consumer 来处理数据;当新的 consumer 上线时,可以通过addConsumer将它的虚拟节点放到哈希环上并开始接收消息;当有 consumer 下线时,可以通过removeConsumer将它的虚拟节点从哈希环上摘除,由其他 consumer 承担它的工作。

因为每个 consumer 有 100 个虚拟节点,所以在 consumer 下线时,负载其实是均匀地分配给了其他 consumer,因此一致性哈希算法能够解决之前 Auto-split Hash Range 方案数据倾斜的问题。




审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 虚拟机
    +关注

    关注

    1

    文章

    855

    浏览量

    27377
  • 哈希算法
    +关注

    关注

    1

    文章

    56

    浏览量

    10690

原文标题:一致性哈希算法设计题,栽了

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    基于相位一致性的边缘检测的Labview实现程序

    基于相位一致性算法编写个Labview的子模块
    发表于 03-31 08:24

    相位一致性边缘检测

    大家起探讨相位一致性边缘检测,求指导
    发表于 06-11 13:38

    一致性测试

    谁有聚星公司射频一致性测试的程序啊,求个做参考,!
    发表于 07-14 18:11

    c6678cache一致性

    专家您好!    我现在在做6678 cache一致性的东西,想请问一下一致性的维护哪些是硬件实现的,哪些需要程序员实现?谢谢!
    发表于 06-24 04:38

    LTE基站一致性测试的类别

    就LTE基站而言,RF测试方法与一致性要求至为关键,然而,调变格式、带宽、资源分配与移动导致选项复杂度增加,因此优化的一致性测试配置参数组合需求更为殷切。第三代合作伙伴项目(3GPP)长期演进计划
    发表于 06-06 06:41

    一致性非锁定读分析

    MySQL探秘(六)InnoDB一致性非锁定读
    发表于 09-17 08:39

    MIPI一致性测试

    MIPI一致性测试测试项目:> TX测试;> RX测试;> S参数和阻抗测试;> DigRF,Unipro和LLI的测试;测试环境: MIPI测试对示波器带宽的要求 >
    发表于 09-26 13:31

    Infiniium一致性测试软件

    Infiniium 一致性测试软件
    发表于 10-28 17:28

    什么是霍尔元件的一致性

    什么是霍尔元件的一致性?霍尔开关元件主要是通过感应磁性来进行开关机,霍尔元件本身又属于无触点开关,因此具有感应距离。霍尔开关都有个触发值和释放值,触发值是指霍尔元件表面达到参数磁性大小,霍尔元器件
    发表于 10-12 09:34

    怎样去验证可部署目标硬件与软件算法模型之间的算法性能一致性

    如何去设计款合理的电子硬件解决方案,从而实现经济有效的大规模生产与部署?怎样去验证可部署目标硬件与软件算法模型之间的算法性能一致性?System Generator是什么?有什么功能
    发表于 04-08 06:25

    为什么需要进行WiMAX协议一致性测试?

    为什么需要进行WiMAX协议一致性测试看完你就知道
    发表于 04-15 06:16

    如何确保蓝牙设计通过EMI一致性测试 ?

    选择蓝牙模块时需要考虑哪些因素?如何确保蓝牙设计通过EMI一致性测试 ?
    发表于 05-07 06:25

    如何实现信号电压幅值的一致性

    如何实现信号电压幅值的一致性
    发表于 05-20 07:23

    顺序一致性和TSO一致性分别是什么?SC和TSO到底哪个好?

    内存一致性之顺序一致性(sequential consistency)可以说,最直观的内存一致性模型是sequentially consistent(SC):内存访问执行的顺序与程序指定的顺序相同
    发表于 07-19 14:54

    Dubbo负载均衡策略之一致性哈希

    本文主要讲解了一致性哈希算法的原理以及其存在的数据倾斜的问题,然后引出解决数据倾斜问题的方法,最后分析一致性哈希
    的头像 发表于 06-16 15:30 318次阅读
    Dubbo负载均衡策略之<b class='flag-5'>一致性</b><b class='flag-5'>哈希</b>