你要的一致性hash来了-上 [全网最深入浅出没有之一 + 附代码分析]

​前言

最近总有人问我一致性hash的事情,求锤得锤,我们今天就来聊聊看。前两篇我们分别介绍了两类哈希分片的方法:《基础轮询》和《虚拟桶》。

  • 基础轮询法导致架构缺乏灵活性,需要扩、缩容一倍的节点才能保证50%的映射关系不变,否则查询命中率会更低,当有一台节点异常时,简直是灾难。

  • 虚拟桶的分片方法在hash取模的基础上做了优化,符合通用的3层路由分片模型,此外将分片数量固定,避免了取模敏感度高的问题,节点变动后每台老节点会有部分分片迁移到新节点上

虽然虚拟桶比hash取模好上很多,但总归是有影响比较多节点的,那有没有影响更小的方法呢?本篇我们就开始讲解一致性hash的内容。

写着写着就上万字了,本想拆成多篇,但是想了下,为了保证知识的连续性,只分为上下两篇了,第一篇为基础篇,下篇为进阶篇。一致性hash确实是值得分析和学习的。

一致性hash原理实现

1. 构建空hash环

  • 我们一步一步来,之前我们尝试过对节点取模,尝试过分片数量取模,而在一致性hash中是对 2^32取模的。在取模的时候我们一般选取正数,因此取模的结果必然是在[0,2^32-1]这个区间内的。较真儿一下,为什么是2的32次方呢?经过一番资料的翻找,通常解释为“java中int的最大值是2^31-1,最小值是-2^31,2^32刚好是无符号整形的最大值”,这个答案也虽然也解释的通,但是为啥不是short 呢?它招谁惹谁了。我猜测有可能算法的创始者觉得short的最大值32767(2^16)过小。那为啥不是其他较大的数值呢?有知道原因的小伙伴可以文章下面留言~

  • 这里我们把这个范围想象成一个圆环,如下所示,正中间为为0,数值以顺时针的方向,依次增大,直到最大值2^32-1,我们称这样的环为hash环。


2. 构建hash环上的节点拓扑

  • 一致性hash与上图中的hash环到底有什么关系呢?

这里我们通过实际的例子来讲解。假设这里我们有三个节点,A、B、C。每个节点有自己的ip地址和主机名,(一般同一个VPC下或者集群中不会存在重复的ip和主机名),首先将节点的ip地址或者主机名进行hash处理,然后对2^32次方取模。

hash(节点的IP地址或主机名) %  2^32
  • 如上述相同,其取模的结果必然是在[0,2^32-1]这个区间中,也意味着,必然会落到hash环中。我们对A、B、C三个节点,均执行相同的操作。此时会得到如下所示的结果,每个节点都会根据hash取模后的数值大小,在hash环上进行分布。


3. 将key映射到hash环

  • 我们将key使用和上述同样的方法进行处理,通过哈希、取模2^32,并映射到hash环上。

  • 在一致性hash的逻辑中,每个key会顺时针寻找的最近的一个节点,作为其对应的节点,如下图所示,我们拿3个key进行举例。

此时小伙伴有疑问了,这看着跟之前讲的通用模型并不相符啊,是不是代表着一致性hash已经脱离通用的路由分片模型了呢?

实际上,一致性hash也是符合通用路由模型的,我们之前聊到会有数据-分片-节点的分层,那么分片在哪呢?我们回看一下2中的hash环,通过3个物理节点将环分为3区间,即节点A-节点B、节点B-节点C、节点C-节点A。这里我们可以把每一个区间都当作一个分片,比如key-01,是落在节点C-节点A的分片上,因此分片归属于节点A。这样看依然是符合咱们的通用路由分片模型的。


4. 模拟节点下线

接下来我们就开始模拟节点下线(机器宕机或者节点缩容),如下图所示,当节点C下线后,整个hash环上只存在节点A和节点B,意味着只存在两个分片:节点A-节点B,节点B-节点A。那么原来在节点C(节点B-节点C这一分片)上的key03,按照上面的逻辑,顺时针寻找下一个节点,这时找到了节点A,即节点B-节点A的分片上。

此时发现,当一个节点下线后,只影响着hash环上当前下线节点到逆时针方向第一个节点之间的所有key,这些key需要重新分布在新节点上。而hash环上其他分片上的所有key是不受影响的。


5. 模拟节点上线

同样的,在上面模拟节点下线后,我们开始模拟节点上线(扩容节点、宕机节点恢复),此时有个新的节点D加入到hash环上,并且取模后的数值,在节点B和节点C之间,将原本节点B-节点C的一个分片,切分为节点B-节点D和节点D到节点C2个分片。

与节点下线类似,此时key03会顺时针找到找到第一个节点,由过去的C节点上重分布到节点D上。影响的范围也仅限在上线的节点到hash环上逆时针方向的第一个节点之间的所有key。


6.模型分析

上述就是一致性hash的核心原理和实现,我们此时再回头看,针对数据重映射程度高的问题,一致性hash相比hash取模来说,是有很大提升的。

那究竟为什么一致性hash在节点变动时会有明显的提升呢?

我们还是按照我们的老方法,回到通用的路由分片模型上,当一个节点变化的时候,一致性hash实际上只会影响一个分片、一个节点上的数据,并不会像虚拟桶以及hash取模一样,会影响多个分片和多个节点。因此一致性hash在容错性和扩展性上是比较优势的

代码实现

理论太干,上代码。(下面只涉及基础代码,并对重要部分作出注释)

#!/bin/bash/env python
# -*- coding:utf8 -*-
# auorth:lzj
import hashlib
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

class ConsistentHash(object):
    def __init__(self, nodes=None):
        '''
           a. 初始化nodes即节点列表
           b. 字典ring 为我们文中讲述的节点hash取模数值和节点的映射关系
           c. 列表sortedKeys为模拟顺时针数值从0到2^32依次增大的关系,可以想象成环
           d. 然后将节点增加到集群中
        '''

        self.nodes = nodes
        self.ring = {}
        self.sortedKeys = []
        self.addNodes(nodes)

    #nodes是一个节点的列表,遍历列表逐个添加到hash环中
    def addNodes(self, nodes):
        if nodes:
            for node in nodes:
                # 和第一篇hash取模相同,我们使用sha1算法进行hash处理,然后强转int
                nodeHashResult = hashlib.sha1(node).hexdigest()
                intNodeHashResult = int(nodeHashResult, 16)
                modIntNodeHashResult = intNodeHashResult % (2 ** 32)
                self.ring[modIntNodeHashResult] = node
                self.sortedKeys.append(modIntNodeHashResult)
                self.sortedKeys.sort()

    def removeNodes(self, nodes):
        '''
        和增加节点相同,此时我们需要将节点hash取模数值和节点的映射关系进行更新删除,
        并在环上将清理该节点的信息
        '''
        if nodes:
            for node in nodes:
                nodeHashResult = hashlib.sha1(node).hexdigest()
                intNodeHashResult = int(nodeHashResult, 16)
                modIntNodeHashResult = intNodeHashResult % (2 ** 32)
                self.ring.pop(modIntNodeHashResult)
                self.sortedKeys.remove(modIntNodeHashResult)

    def getNode(self, modKeyHashResult):
        '''
            依次遍历 sortedKeys中的所有node,由于是有序的,依次判断,直到node的数值大于key的数值,
            那么此时key就是属于对应的节点
        '''
        position = 0
        for _modIntNodeHashResult in self.sortedKeys:
            position += 1
            if modKeyHashResult < _modIntNodeHashResult:
                return self.ring[_modIntNodeHashResult]
            else:
                continue
        ''' 
            遍历了全部节点都对比失败的话,说明是在hash环上0开始逆时针第一个节点到顺时针第一个节点之间,
            因此将key对应到第一个节点
        '''
        if position == len(self.sortedKeys):
            return self.ring[self.sortedKeys[0]]

    def allocateKey(self,number):
        keyNodeMap = []
        # 模拟若干个key,同样的将key进行hash取模
        for i in range(number):
            keyName = 'testKey' + str(i)
            keyHashResult = hashlib.sha1(keyName).hexdigest()
            intKeyHashResult = int(keyHashResult, 16)
            modKeyHashResult = intKeyHashResult % (2 ** 32)
            # 对key进行寻址,即确定key 是属于哪一个节点,哪一个分片
            _node = self.getNode(modKeyHashResult)
            print '%s is allocateKey to %s' %(keyName,_node)
            keyNodeMap.append(keyName + '_' + _node)
        return keyNodeMap

#模拟集群有4个节点
iniServers = [
    '192.168.1.1',
    '192.168.1.2',
    '192.168.1.3',
    '192.168.1.4',
    ]

print '初始化一个hash环,构建hash环上的节点拓扑'
h = ConsistentHash(iniServers)
print h.ring
print h.sortedKeys

print '构造若干个key,将key映射到hash环'
number = 40
oldMap = h.allocateKey(40)

print  '尝试上线一个新节点,我们看下key是否会正常迁移到新节点'
newNode = ['192.168.1.5']
h.addNodes(newNode)
print h.ring
print h.sortedKeys
addNodeMap = h.allocateKey(40)

print '尝试下线一个节点,我们再观察下'
removeNode = ['192.168.1.1']
h.removeNodes(removeNode)
print h.ring
print h.sortedKeys
removeNodeMap = h.allocateKey(40)
究竟代码完成度高不高呢?我们在从运行结果的角度反向测试下。如下所示
初始化一个hash环,构建hash环上的节点拓扑
环上的节点:{560662416L: '192.168.1.1', 216828752L: '192.168.1.3', 2895068098L: '192.168.1.2', 1580996791L: '192.168.1.4'}
节点hash值排序:[216828752L, 560662416L, 1580996791L, 2895068098L]
构造若干个key,将key映射到hash环
testKey0 is allocateKey to 192.168.1.4
testKey1 is allocateKey to 192.168.1.1
testKey2 is allocateKey to 192.168.1.4
testKey3 is allocateKey to 192.168.1.4
testKey4 is allocateKey to 192.168.1.3
testKey5 is allocateKey to 192.168.1.3
testKey6 is allocateKey to 192.168.1.2
testKey7 is allocateKey to 192.168.1.2
testKey8 is allocateKey to 192.168.1.3
testKey9 is allocateKey to 192.168.1.2
testKey10 is allocateKey to 192.168.1.4
testKey11 is allocateKey to 192.168.1.1
testKey12 is allocateKey to 192.168.1.3
testKey13 is allocateKey to 192.168.1.4
testKey14 is allocateKey to 192.168.1.3
testKey15 is allocateKey to 192.168.1.2
testKey16 is allocateKey to 192.168.1.4
testKey17 is allocateKey to 192.168.1.4
testKey18 is allocateKey to 192.168.1.1
testKey19 is allocateKey to 192.168.1.1
testKey20 is allocateKey to 192.168.1.3
testKey21 is allocateKey to 192.168.1.2
testKey22 is allocateKey to 192.168.1.4
testKey23 is allocateKey to 192.168.1.2
testKey24 is allocateKey to 192.168.1.2
testKey25 is allocateKey to 192.168.1.3
testKey26 is allocateKey to 192.168.1.2
testKey27 is allocateKey to 192.168.1.3
testKey28 is allocateKey to 192.168.1.2
testKey29 is allocateKey to 192.168.1.2
testKey30 is allocateKey to 192.168.1.2
testKey31 is allocateKey to 192.168.1.1
testKey32 is allocateKey to 192.168.1.3
testKey33 is allocateKey to 192.168.1.2
testKey34 is allocateKey to 192.168.1.2
testKey35 is allocateKey to 192.168.1.3
testKey36 is allocateKey to 192.168.1.2
testKey37 is allocateKey to 192.168.1.2
testKey38 is allocateKey to 192.168.1.2
testKey39 is allocateKey to 192.168.1.2
尝试上线一个新节点,我们看下key是否会正常迁移到新节点[注意标红处]
环上的节点:{560662416L: '192.168.1.1', 216828752L: '192.168.1.3', 2895068098L: '192.168.1.2', 1785826697L: '192.168.1.5', 1580996791L: '192.168.1.4'}
节点hash值排序:[216828752L, 560662416L, 1580996791L, 1785826697L, 2895068098L]
testKey0 is allocateKey to 192.168.1.4
testKey1 is allocateKey to 192.168.1.1
testKey2 is allocateKey to 192.168.1.4
testKey3 is allocateKey to 192.168.1.4
testKey4 is allocateKey to 192.168.1.3
testKey5 is allocateKey to 192.168.1.3
testKey6 is allocateKey to 192.168.1.2
testKey7 is allocateKey to 192.168.1.2
testKey8 is allocateKey to 192.168.1.3
testKey9 is allocateKey to 192.168.1.2
testKey10 is allocateKey to 192.168.1.4
testKey11 is allocateKey to 192.168.1.1
testKey12 is allocateKey to 192.168.1.3
testKey13 is allocateKey to 192.168.1.4
testKey14 is allocateKey to 192.168.1.3
testKey15 is allocateKey to 192.168.1.5
testKey16 is allocateKey to 192.168.1.4
testKey17 is allocateKey to 192.168.1.4
testKey18 is allocateKey to 192.168.1.1
testKey19 is allocateKey to 192.168.1.1
testKey20 is allocateKey to 192.168.1.3
testKey21 is allocateKey to 192.168.1.2
testKey22 is allocateKey to 192.168.1.4
testKey23 is allocateKey to 192.168.1.5
testKey24 is allocateKey to 192.168.1.2
testKey25 is allocateKey to 192.168.1.3
testKey26 is allocateKey to 192.168.1.2
testKey27 is allocateKey to 192.168.1.3
testKey28 is allocateKey to 192.168.1.2
testKey29 is allocateKey to 192.168.1.2
testKey30 is allocateKey to 192.168.1.2
testKey31 is allocateKey to 192.168.1.1
testKey32 is allocateKey to 192.168.1.3
testKey33 is allocateKey to 192.168.1.2
testKey34 is allocateKey to 192.168.1.2
testKey35 is allocateKey to 192.168.1.3
testKey36 is allocateKey to 192.168.1.5
testKey37 is allocateKey to 192.168.1.2
testKey38 is allocateKey to 192.168.1.2
testKey39 is allocateKey to 192.168.1.2
尝试下线一个节点,我们再观察下[注意标红处]
环上的节点:{216828752L: '192.168.1.3', 2895068098L: '192.168.1.2', 1785826697L: '192.168.1.5', 1580996791L: '192.168.1.4'}
节点hash值排序:[216828752L, 1580996791L, 1785826697L, 2895068098L]
testKey0 is allocateKey to 192.168.1.4
testKey1 is allocateKey to 192.168.1.4
testKey2 is allocateKey to 192.168.1.4
testKey3 is allocateKey to 192.168.1.4
testKey4 is allocateKey to 192.168.1.3
testKey5 is allocateKey to 192.168.1.3
testKey6 is allocateKey to 192.168.1.2
testKey7 is allocateKey to 192.168.1.2
testKey8 is allocateKey to 192.168.1.3
testKey9 is allocateKey to 192.168.1.2
testKey10 is allocateKey to 192.168.1.4
testKey11 is allocateKey to 192.168.1.4
testKey12 is allocateKey to 192.168.1.3
testKey13 is allocateKey to 192.168.1.4
testKey14 is allocateKey to 192.168.1.3
testKey15 is allocateKey to 192.168.1.5
testKey16 is allocateKey to 192.168.1.4
testKey17 is allocateKey to 192.168.1.4
testKey18 is allocateKey to 192.168.1.4
testKey19 is allocateKey to 192.168.1.4
testKey20 is allocateKey to 192.168.1.3
testKey21 is allocateKey to 192.168.1.2
testKey22 is allocateKey to 192.168.1.4
testKey23 is allocateKey to 192.168.1.5
testKey24 is allocateKey to 192.168.1.2
testKey25 is allocateKey to 192.168.1.3
testKey26 is allocateKey to 192.168.1.2
testKey27 is allocateKey to 192.168.1.3
testKey28 is allocateKey to 192.168.1.2
testKey29 is allocateKey to 192.168.1.2
testKey30 is allocateKey to 192.168.1.2
testKey31 is allocateKey to 192.168.1.4
testKey32 is allocateKey to 192.168.1.3
testKey33 is allocateKey to 192.168.1.2
testKey34 is allocateKey to 192.168.1.2
testKey35 is allocateKey to 192.168.1.3
testKey36 is allocateKey to 192.168.1.5
testKey37 is allocateKey to 192.168.1.2
testKey38 is allocateKey to 192.168.1.2
testKey39 is allocateKey to 192.168.1.2       

尾记

由此可见我们上面写的代码高度还原了一致性hash,从我们上述分析来看,他的表现还是挺不错的,**那么是否意味着一致性hash没有缺陷呢?我们下篇针对一致性hash的痛点问题进行分析。

原创不易,觉得多少有所收获的话,就请点个赞并关注我吧。你的支持是我写作的动力。

猜你喜欢

转载自blog.csdn.net/weixin_47158466/article/details/107837353