一致性hash的原理介绍,前人已经做的很清楚了,可以参看下面链接:
一致性HASH算法详解
上文美中不足的是,数据结构的设计较复杂,hash环的实现,属性用简单的列表和字典实现即可。
首先放不设置虚拟节点的代码,可以看见删除掉某个节点时很容易引起雪崩效应,代码如下。
"""对一致性hash进行学习,构造没有vnode的hash,增加和删除节点以进行观察,会产生对应的雪崩效应"""
from zlib import crc32
import memcache
class conhashnorep(object):
def __init__(self,nodes=None):
"""此处nodes代表需要初始化的node列表,为进行对比先提供虚拟节点副本数,ring是存储hash-node的字典,key是存储所有hash值的列表"""
self.nodes=nodes
self.ring={}
self._sorted_keys=[]
self.add_nodes(nodes)
def _add_node(self,node):
bnode=bytes(node,encoding="utf8")
nodehash=abs(crc32(bnode))
self.ring[nodehash]=node
self._sorted_keys.append(nodehash)
self._sorted_keys.sort()
def add_nodes(self,nodes):
if nodes:
for node in nodes:
self._add_node(node)
def remove_nodes(self,nodes):
"""对于remove的操作,如果直接操作已经产生的ring字典,会比较麻烦,因为ring是{hashnode:node}的键值对形式,
直接处理涉及到字典直接查找值ring.values()/ring.keys(),以及通过值来remove(key)的操作,太麻烦"""
if nodes:
for node in nodes:
bnode = bytes(node, encoding="utf8")
nodehash = abs(crc32(bnode))
del self.ring[nodehash]
self._sorted_keys.remove(nodehash)
def get_node(self,key):
bkey=bytes(key,encoding="utf8")
keyhash=abs(crc32(bkey))
i=0
for nodehash in self._sorted_keys:
i += 1
if keyhash < nodehash:
#print("%d Keyhash:%s Nodehash:%s" % (i,keyhash,nodehash))
return self.ring[nodehash]
else:
continue
if i==len(self._sorted_keys):
#print("None Keyhash:%s Nodehash:%s" % (keyhash,self._sorted_keys[0]))
return self.ring[self._sorted_keys[0]]
def count_server(self,number):
'''server_list用于存放重复出现的server,server_counnt用于统计出现频数'''
server_list = []
server_count = {}
for i in range(number):
kei = "key_%s" % i
server = self.get_node(kei)
server_list.append(server)
for m in set(server_list):
server_count[m] = server_list.count(m)
'''根据字典的键进行排序,如果需要逆序则添加 reverse=True'''
server_count=dict(sorted(server_count.items(), key=lambda e: e[0]))
print("ServerCount:", server_count)
ini_servers = [
'127.0.0.1:1',
'127.0.0.1:2',
'127.0.0.1:3',
'127.0.0.1:4',
#'127.0.0.1:7005',
]
h=conhashnorep(ini_servers)
print(h.ring)
"""构建一些object来观察会分配到哪一个节点"""
#for i in range(20):
part_servers=['127.0.0.2:1','127.0.0.2:2']
h.count_server(200)
h.add_nodes(part_servers)
h.count_server(200)
h.remove_nodes(['127.0.0.1:1','127.0.0.1:2'])
h.count_server(200)
执行程序,得到如下结果
查看可知,在直接删除‘127.0.0.1:1’,‘127.0.0.1:2’两个节点后,原本属于这两个节点的key并没有较均匀的分散到其他节点,而是直接积压在‘127.0.0.2:1’,‘127.0.0.2:2’这两个新节点上。
"""对一致性hash进行学习,构造没有vnode的hash,增加和删除节点以进行观察,会产生对应的雪崩效应"""
from zlib import crc32
class conhashnorep(object):
def __init__(self,nodes=None,replicas=5):
"""此处nodes代表需要初始化的node列表,为进行对比先提供虚拟节点副本数,ring是存储hash-node的字典,key是存储所有hash值的列表"""
self.nodes=nodes
self.replicas=replicas
self.ring={}
self._sorted_keys=[]
self.add_nodes(nodes)
def _add_node(self,node):
for i in range(self.replicas):
bnode="%s_vnode%s" % (node,i)
bnode = bytes(bnode, encoding="utf8")
nodehash = abs(crc32(bnode))
self.ring[nodehash] = node
self._sorted_keys.append(nodehash)
self._sorted_keys.sort()
def add_nodes(self,nodes):
if nodes:
for node in nodes:
self._add_node(node)
def remove_nodes(self,nodes):
"""对于remove的操作,如果直接操作已经产生的ring字典,会比较麻烦,因为ring是{hashnode:node}的键值对形式,
直接处理涉及到字典直接查找值ring.values()/ring.keys(),以及通过值来remove(key)的操作,太麻烦"""
if nodes:
for node in nodes:
for i in range(self.replicas):
bnode = "%s_vnode%s" % (node, i)
bnode = bytes(bnode, encoding="utf8")
nodehash = abs(crc32(bnode))
del self.ring[nodehash]
self._sorted_keys.remove(nodehash)
def get_node(self,key):
bkey=bytes(key,encoding="utf8")
keyhash=abs(crc32(bkey))
i=0
for nodehash in self._sorted_keys:
i += 1
if keyhash < nodehash:
#print("%d Keyhash:%s Nodehash:%s" % (i,keyhash,nodehash))
return self.ring[nodehash]
else:
continue
if i==len(self._sorted_keys):
#print("None Keyhash:%s Nodehash:%s" % (keyhash,self._sorted_keys[0]))
return self.ring[self._sorted_keys[0]]
def count_server(self,number):
'''server_list用于存放重复出现的server,server_counnt用于统计出现频数'''
server_list = []
server_count = {}
for i in range(number):
kei = "key_%s" % i
server = self.get_node(kei)
server_list.append(server)
for m in set(server_list):
server_count[m] = server_list.count(m)
'''根据字典的键进行排序,如果需要逆序则添加 reverse=True'''
server_count=dict(sorted(server_count.items(), key=lambda e: e[1],reverse=True))
print("ServerCount:", server_count)
ini_servers = [
'127.0.0.1:1',
'127.0.0.1:2',
'127.0.0.1:3',
'127.0.0.1:4',
#'127.0.0.1:7005',
]
h=conhashnorep(ini_servers)
print(h.ring)
"""构建一些object来观察会分配到哪一个节点"""
#for i in range(20):
part_servers=['127.0.0.2:1','127.0.0.2:2']
h.count_server(200)
h.add_nodes(part_servers)
h.count_server(200)
h.remove_nodes(['127.0.0.1:1','127.0.0.1:2'])
h.count_server(200)
运行结果如下:
可见,去掉‘127.0.0.1:1’和‘127.0.0.1:2’两个节点,并没有完全积压到某一个节点,相比较而言做了一定的分散。
一致性hash的使用在分布式系统的实现应该比较常见,比如分布式存储,分布式计算(多队列多任务),在实际的使用中那种数据结构会取得比较好的效率还未实践,本文代码参考了python官方的hash ring的实现,也可以考虑使用一些别的东西,比如memcache(这个我个人感觉类似于一个代码加速器或者数据库中间件,但是可能这样的理解有问题)。也可以考虑用别的方式来实现一致性hash(存储量或者任务量达到一定程度,且是交互式查询时需要提升性能),例如map,例如红黑树,以前学艺不精,现在慢慢捡以前丢的东西,后续有时间有机会再来补充。