一致性哈希原理与应用

Posted 猿人课堂

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一致性哈希原理与应用相关的知识,希望对你有一定的参考价值。

概念

百科释义

一致性哈希算法简单来说就是一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。

哈希算法评价标准

在动态的缓存环境中,有下面这么几条标准,可以用来判断一个哈希算法的好坏。借用网上 一篇文章 对此的描述。

  • 平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

  • 单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。

  • 分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。

  • 负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

硬哈希

硬哈希,一般又被称为普通哈希。其原理较为简单,当然实现方式也多种多样。比如取余数法等。单调性也能很好的满足。

硬哈希在缓存服务的适用场景一般是缓存环境不怎么发生变化,对于缓存服务器群的稳定性要求较高。一旦服务器群出现故障,就有可能导致井喷现象,出现缓存服务瞬间崩溃。这么说可能不太容易理解,下面举个通俗点的例子。

张大胖负责维护公司的缓存服务群,手里现在有10台机器,每台服务器的内存使用率都达到了95%, 而且老板还贼抠,就是不给配新机器。这天,实习生小李写了个日志分析的脚本,没有考虑到目前服务器内存已然吃紧的现状,就直接执行了。结果第三台服务器出现了严重的Out Of Memory问题,导致服务器直接死掉了。然后缓存数据直接全部丢失。这个时候原本的10台服务器,变成了9台。原本可以命中的缓存内容这下都失效了。导致了缓存服务被迫更新,瞬间服务器压力都上来了。不仅缓存服务器挂了,后台的mysql服务器也撑不住这么大规模的请求啊,公司的整个业务线都陷入了瘫痪的状态。

简易实现硬哈希 hard-hash.py

# coding: utf8from hashlib import *from constraints import servers, entriesfrom func import *length = len(servers)
details1 = {key:[] for key in servers}for entry in entries:    # keyhash = sum([ord(c) for c in entry])
    keyhash = hashcode(entry)
    server = servers[keyhash%length]
    details1[server].append(entry)
print_pretty_dict(details1)

print("---"*28)
del servers[0]length = len(servers)
details2 = {key:[] for key in servers}for entry in entries:    # keyhash = sum([ord(c) for c in entry])
    keyhash = hashcode(entry)
    server = servers[keyhash%length]
    details2[server].append(entry)
print_pretty_dict(details2)

print("---"*28)
servers.insert(0, "192.168.0.1:11211")
servers.append("192.168.0.5:11211")length = len(servers)
details3 = {key:[] for key in servers}for entry in entries:    # keyhash = sum([ord(c) for c in entry])
    keyhash = hashcode(entry)
    server = servers[keyhash%length]
    details3[server].append(entry)
print_pretty_dict(details3)# -------计算缓存度print("==="*7)
print_pretty_dict(compute_cache_percentage(details1, details2))
print("---"*20)
print_pretty_dict(compute_cache_percentage(details1, details3))

硬哈希缓存效果

动态缓存环境下,硬哈希的命中率并不高。

一致哈希

一致性哈希算法的基本实现原理是将机器节点和key值都按照一样的hash算法映射到一个0~2^32(也不一定非得是2^32, 理论上能让节点分布均匀的‘环’就够了)的圆环上。当有一个写入缓存的请求到来时,计算Key值k对应的哈希值Hash(k),如果该值正好对应之前某个机器节点的Hash值,则直接写入该机器节点,如果没有对应的机器节点,则顺时针查找下一个节点,进行写入,如果超过2^32还没找到对应节点,则从0开始查找(因为是环状结构)。比如下面盗的一张图。 

简易代码实现consisthash.py

# coding: utf8# 简单一致性hash实现from constraints import servers, entriesfrom func import print_pretty_dict, hashcode, compute_cache_percentage, compute_cache_percentage_ringclass ConsistHash(object):
    """    简单一致性哈希算法实现    """
    def __init__(self, servers=[]):
        self.servers = servers        # sorted list which contains server nodes.
        self.ring = []        # node:[hashcode1, hashcode2, ...]
        self.hashnodemap = {}        for server in self.servers:
            self.addNode(server)    def addNode(self, node):
        code = hashcode(node)
        self.hashnodemap[code] = node
        self.ring.append(code)
        self.ring.sort()    def removeNode(self, node):
        del self.hashnodemap[hashcode(node)]
        self.ring.remove(hashcode(node))    def getNode(self, key):
        code = hashcode(key)        for ringitem in self.ring[::-1]:            if ringitem <= code:                return self.hashnodemap[ringitem]        return self.hashnodemap[self.ring[0]]class Cacher(object):
    """    普通一致性哈希算法的应用    """
    def __init__(self, servers):
        self.c = ConsistHash(servers=servers)
        self.container = {key:[] for key in servers}    def addServer(self, server):
        self.c.addNode(server)
        self.container[server] = []    def removeServer(self, server):
        self.c.removeNode(server)        del self.container[server]    def cache(self, key, value):
        server = self.c.getNode(key)
        self.container[server].append({key: value})    def get(self, key):
        server = self.c.getNode(key)        return self.container[server].items()[key]if __name__ == "__main__":    # c = ConsistHash(servers=servers)
    # print_pretty_list(c.ring)
    # print_pretty_dict(c.hashnodemap)
    cacher1 = Cacher(servers)    for entry in entries:
        cacher1.cache(entry, entry)
    print_pretty_dict(cacher1.container)    # 删除一个服务器
    cacher3 = Cacher(servers)
    cacher3.removeServer("192.168.0.1:11211")    for entry in entries:
        cacher3.cache(entry, entry)
    print_pretty_dict(cacher3.container)    # 添加一个服务器
    cacher2 = Cacher(servers)
    cacher2.addServer("192.168.0.5:11211")    for entry in entries:
        cacher2.cache(entry, entry)
    print_pretty_dict(cacher2.container)    # 计算缓存有效度
    print_pretty_dict(compute_cache_percentage_ring(cacher1.container, cacher2.container))
    print_pretty_dict(compute_cache_percentage_ring(cacher1.container, cacher3.container))

缓存效果

与硬哈希缓存命中率相比,一致哈希的缓存命中率确实提高了不少。

“虚拟”一致哈希

虽然缓存命中率得到了提高,但是仅仅这样还不能够真正的应用到实际生产环境中,因为目前的一致哈希还缺少了平衡性。

在此基础上,算法大佬们又引入了虚拟节点的概念。

“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列

带有虚拟节点的一致哈希virtualconstisthash.py

# coding: utf8# 带有虚拟节点的一致性哈希算法实现from constraints import servers, entriesfrom func import print_pretty_dict, hashcode, print_pretty_listfrom func import compute_cache_percentagefrom func import compute_cache_percentage_ringfrom func import compute_cache_percentage_virtual_ringclass VirtualConsistHash(object):
    """    带有虚拟节点的一致性哈希算法实现    """
    def __init__(self, servers=[], replicas=3):
        self.servers = servers        # sorted list which contains server nodes.
        self.ring = []        # node:[hashcode1, hashcode2, ...]
        # 虚拟节点的个数,其实这个名字叫虚拟节点的个数不太合适,每个真实节点“虚拟化”后的节点个数比较好
        self.replicas = replicas
        self.hashnodemap = dict()        for server in self.servers:
            self.addNode(server)    def addNode(self, node):
        for i in range(0, self.replicas):
            temp = "{}#{}".format(node, i)
            code = hashcode(temp)
            self.hashnodemap[code] = temp
            self.ring.append(code)
            self.ring.sort()    def removeNode(self, node):
        for i in range(0, self.replicas):
            temp = "{}#{}".format(node, i)
            code = hashcode(temp)
            self.ring.remove(code)            del self.hashnodemap[code]    def getNode(self, key):
        code = hashcode(key)        for ringitem in self.ring[::-1]:            if ringitem <= code:                return self.hashnodemap[ringitem]        return self.hashnodemap[self.ring[0]]class Cacher(object):
    """    带有虚拟节点的一致性哈希算法的应用    """
    def __init__(self, servers):
        self.c = VirtualConsistHash(servers=servers)
        self.container = {"{}#{}".format(server, index): [] for index in range(0, self.c.replicas) for server in self.c.servers}    def addServer(self, server):
        self.c.addNode(server)        for i in range(0, self.c.replicas):
            temp = "{}#{}".format(server, i)
            self.container[temp] = []    def removeServer(self, server):
        self.c.removeNode(server)        for i in range(0, self.c.replicas):
            temp = "{}#{}".format(server, i)            del self.container[temp]    def cache(self, key, value):
        server = self.c.getNode(key)
        self.container[server].append({key: value})    def get(self, key):
        server = self.c.getNode(key)        return self.container[server].items()[key]if __name__ == "__main__":
    c = VirtualConsistHash(servers=servers)
    print_pretty_list(c.ring)
    print_pretty_dict(c.hashnodemap)
    cacher1 = Cacher(servers)    for entry in entries:
        cacher1.cache(entry, entry)
    print_pretty_dict(cacher1.container)    # 删除一个服务器
    cacher3 = Cacher(servers)
    cacher3.removeServer("192.168.0.1:11211")    for entry in entries:
        cacher3.cache(entry, entry)
    print_pretty_dict(cacher3.container)    # 添加一个服务器
    cacher2 = Cacher(servers)
    cacher2.addServer("192.168.0.5:11211")    for entry in entries:
        cacher2.cache(entry, entry)
    print_pretty_dict(cacher2.container)    # 计算缓存有效度
    print("==="*19, "删除一个缓存服务器后~")
    print_pretty_dict(
        compute_cache_percentage_virtual_ring(cacher1.container, cacher2.container))
    print("==="*19, "添加一个缓存服务器后~")
    print_pretty_dict(
        compute_cache_percentage_virtual_ring(cacher1.container, cacher3.container))

实现效果

带有了虚拟节点的一致哈希实现,使得缓存的命中率得到了进一步的提高。而且平衡性也更趋于平和。

总结

通过上述分析,不难发现。

  • 硬哈希适用场景为“稳定”的缓存服务群,因此实际生产环境不怎么被用到。

  • 简单一致哈希,缓存命中率还算可以,但是缺乏平衡性,容易导致某台节点压力过大而有些节点空闲的状况。

  • 带有虚拟节点的一致哈希可以很好的解决上面的两个问题,但是具体的虚拟节点的设置replicas可能还需要根据实际的生产环境来进行设置。以此来达到一个最优的效果。


以上是关于一致性哈希原理与应用的主要内容,如果未能解决你的问题,请参考以下文章

一致性哈希算法原理详解

算法简述:一致性hash环,与redis 槽道原理

HashMap原理:哈希函数的设计

分布式核心原理:一致性哈希算法白话解析

面经zhi一致性哈希算法原理及应用

一致性哈希算法PHP测试片段