深入云存储系统Swift核心组件:Ring实现原理剖析

2023-11-14

深入云存储系统Swift核心组件:Ring实现原理剖析

 

 

 

简介

OpenStack是一个美国国家航空航天局和Rackspace合作研发的开源云计算项目,并成为Apache下的一个重要开源项目,目前已经发展到了180家公司参与其中。

OpenStack Object Storage(SwiftOpenStack开源云计算项目的子项目之一。Swift的目的是使用普通硬件来构建冗余的、可扩展的分布式对象存储集群,存储容量可达PB级。OpenStack Object Storage 最初由 Rackspace 采用Python语言开发,并于 2010 年 7 月贡献给 OpenStack ,作为该开源项目的一部分。它的目的是用于托管 Rackspace的 Cloud Files service ,原始项目代号是 swift,所以沿用至今。

在分布式对象存储中的一个关键问题是数据该如何存放。Ring是Swift中最重要的组件,用于记录存储对象与物理位置间映射关系。在涉及查询account、container、object信息时就需要查询集群的ring信息。

 

先来看一下Swift文档中关于Ring的描述:

       Ring用来确定数据驻留在集群中的位置。有单独对应于Account数据库、container数据库和单个object的ring。

       Ring中每个partition在集群中都(默认)有3个replica。每个partition的位置由ring来维护,并存储在映射中。

       Ring使用zone的概念来保证数据的隔离。每个partition的replica都确保放在了不同的zone中。一个zone可以是一个硬盘,一个服务器,一个机架,一个交换机,甚至是一个数据中心............

.......

 

在上述Ring的特性描述中提到了Ring使用zone、device、partition和replica等等来维护数据和磁盘间的映射信息。那么在Ring的背后采用什么算法,使用了什么机制来保证数据的安全、高效和可扩展呢?这些概念对于数据存储带来了什么好处?本文逐步深入探讨了Swift如何通过Ring组件来实现冗余的、可扩展的目的。

 

 

1.      普通Hash算法与场景分析

 

 

先来看一个简单的例子假设我们手里有N台存储服务器(以下简称node),打算用于图片文件存储,为了使服务器的负载均衡,需要把对象均匀地映射到每台服务器上,通常会使用哈希算法来实现,计算步骤如下:

 

1.计算object的hash值Key

2.计算Key mod N值

      

N个存储节点,将Key模N得到的余数就是该Key对应的值需要存放的节点。比如,N是2,那么值为0、1、2、3、4的Key需要分别存放在0、1、0、1和0号节点上。如果哈希算法是均匀的,数据就会被平均分配到两个节点中。如果每个数据的访问量比较平均,负载也会被平均分配到两个节点上。

但是,当数据量和访问量进一步增加,两个节点无法满足需求的时候,需要增加一个节点来服务客户端的请求。这时,N变成了3,映射关系变成了Key mod (N+1),因此,上述哈希值为2、3、4的数据需要重新分配(2->server 2,3 -> server 0,4 -> server 1)。如果数据量很大的话,那么数据量的迁移工作将会非常大。当N已经很大,从N加入一个节点变成N+1个节点的过程,会导致整个哈希环的重新分配,这个过程几乎是无法容忍的,几乎全部的数据都要重新移动一遍。

       我们举例说明,假设有100个node的集群,将107项数据使用md5 hash算法分配到每个node中,Python代码如下:

from hashlib import md5
from struct import unpack_from

NODE_COUNT = 
100
DATA_ID_COUNT = 10000000

node_counts = [0] * NODE_COUNT
for data_id in xrange(DATA_ID_COUNT):
    data_id = str(data_id)
    
# This just pulls part of the hash out as an integer
    hsh = unpack_from('>I', md5(data_id).digest())[0]
    node_id = hsh % NODE_COUNT
    node_counts[node_id] += 
1
desired_count = DATA_ID_COUNT / NODE_COUNT
print '%d: Desired data ids per node' % desired_count
max_count = max(node_counts)
over = 
100.0 * (max_count - desired_count) / desired_count
print '%d: Most data ids on one node, %.02f%% over' % \
    (max_count, over)
min_count = min(node_counts)
under = 
100.0 * (desired_count - min_count) / desired_count
print '%d: Least data ids on one node, %.02f%% under' % \
    (min_count, under)


100000: Desired data ids per node
100695: Most data ids on one node, 0.69% over
99073: Least data ids on one node, 0.93% under

      

分布结果如下所示:

名称

数据项数量

百分比值

数据项均值

100000

0%

最多数据项节点

100695

+0.69%

最少数据项节点

99073

-0.93%

 

       从数据分布上来看拥有最多/最少数据项的节点没有超出平均值的1%。现在假设增加一个节点提供负载能力,不过得重新分配数据项到新的节点上,代码如下:

 

from hashlib import md5
from struct import unpack_from  


NODE_COUNT = 100
NEW_NODE_COUNT = 101
DATA_ID_COUNT = 10000000

moved_ids = 0
for data_id in xrange(DATA_ID_COUNT):
    data_id = str(data_id)
    hsh = unpack_from(
'>I', md5(str(data_id)).digest())[0]
    node_id = hsh % NODE_COUNT
    new_node_id = hsh % NEW_NODE_COUNT
    
if node_id != new_node_id:
        moved_ids += 
1
percent_moved = 100.0 * moved_ids / DATA_ID_COUNT
print '%d ids moved, %.02f%%' % (moved_ids, percent_moved)

9900989 ids moved, 99.01%

 

通过计算我们发现,为了提高集群1%的存储能力,我们需要移动9900989个数据项,也就是99.01%的数据项!显然,这种算法严重地影响了系统的性能和可扩展性。

clip_image002[4]增加1%的存储能力=移动99%的数据?

 

 这种亏本生意显然做不得,那么怎么办呢?一致性哈希算法就是为了解决这个问题而来。

 

 

2.      一致性哈希算法

      

一致性哈希算法是由D. Darger、E. Lehman和T. Leighton 等人于1997年在论文Consistent Hashing and Random Trees:Distributed Caching Protocols for Relieving Hot Spots On the World Wide Web首次提出,目的主要是为了解决分布式网络中的热点问题。在其论文中,提出了一致性哈希算法并给出了衡量一个哈希算法的4个指标:

 

平衡性(Balance)

       平衡性是指Hash的结果能够尽可能分布均匀,充分利用所有缓存空间

单调性(Monotonicity)

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

分散性(Spread)

       分散性定义了分布式环境中,不同终端通过Hash过程将内容映射至缓存上时,因可见缓存不同,Hash结果不一致,相同的内容被映射至不同的缓冲区。

负载(Load)

       负载是对分散性要求的另一个纬度。既然不同的终端可以将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。

 

Swift使用该算法的主要目的是在改变集群的node数量时(增加/删除服务器),能够尽可能少地改变已存在key和node的映射关系,以满足单调性。一致性哈希一般两种思路:

1.迁移为主要特点(swift初期采用)

2.引入虚结点,减少移动为特点(swift现采用)

       具体步骤如下:

       1.    首先求出每个节点(机器名或者是IP地址)的哈希值,并将其分配到一个圆环区间上(这里取0-2^32)。

       2.    求出需要存储对象的哈希值,也将其分配到这个圆环上。

       3.    从对象映射到的位置开始顺时针查找,将对象保存到找到的第一个节点上。

       其中这个从哈希到位置映射的圆环,我们就可以理解为何使用术语“Ring”来表示了。哈希环空间上的分布如图1所示:

 

clip_image004[4]

1 哈希环空间

  假设在这个环形哈希空间中,Cache5被映射在Cache3和Cache4之间,那么受影响的将仅是沿Cache5逆时针遍历直到下一个Cache(Cache3)之间的对象(它们本来映射到Cache4上)。

clip_image006[4]

2 一致性哈希算法的数据移动

 

   现在,使用该算法在集群中增加一个node,同时要保证每个节点的数据项数量均衡,代码如下所示,其中node_range_starts表示每个node的数据项的开始位置。

from bisect import bisect_left
from hashlib import md5
from struct import unpack_from 

NODE_COUNT = 
100
NEW_NODE_COUNT = 101
DATA_ID_COUNT = 10000000

node_range_starts = []
for node_id in xrange(NODE_COUNT):
    node_range_starts.append(DATA_ID_COUNT /
                             NODE_COUNT * node_id)
new_node_range_starts = []

for new_node_id in xrange(NEW_NODE_COUNT):
    new_node_range_starts.append(DATA_ID_COUNT /
                              NEW_NODE_COUNT * new_node_id)
moved_ids = 
0
for data_id in xrange(DATA_ID_COUNT):
    data_id = str(data_id)
    hsh = unpack_from(
'>I', md5(str(data_id)).digest())[0]
    node_id = bisect_left(node_range_starts,
                          hsh % DATA_ID_COUNT) % NODE_COUNT
    new_node_id = bisect_left(new_node_range_starts,
                          hsh % DATA_ID_COUNT) % NEW_NODE_COUNT
    
if node_id != new_node_id:
        moved_ids += 
1
percent_moved = 100.0 * moved_ids / DATA_ID_COUNT
print '%d ids moved, %.02f%%' % (moved_ids, percent_moved)

4901707 ids moved, 49.02%

      

结果虽然比之前好了些,但是提高1%的性能与移动50%的数据仍不理想。

clip_image008[4]

增加1%能力=移动50%数据?

引入虚拟节点(Partition)

 

考虑到哈希算法在node较少的情况下,改变node数会带来巨大的数据迁移。为了解决这种情况,一致性哈希引入了“虚拟节点”的概念: “虚拟节点”是实际节点在环形空间的复制品,一个实际节点对应了若干个“虚拟节点”,“虚拟节点”在哈希空间中以哈希值排列。

clip_image010[4]

3 虚拟节点

       引入了“虚拟节点”后,映射关系就从【object--->node】转换成了【object--->virtual node---> node】。查询object所在node的映射关系如下图所示。

clip_image012[4]

4 对象、虚结点、节点间的映射关系

 

       对100个node细分为1000个vnode,使用vnode_range_starts来指定vnode的开始范围,vnode2node表示vnode到node的指派,然后增加一个node,完成vnode的重新分配,并计算所移动的数据项:

from bisect import bisect_left
from hashlib import md5
from struct import unpack_from

NODE_COUNT = 
100
DATA_ID_COUNT = 10000000
VNODE_COUNT = 1000

vnode_range_starts = []
vnode2node = []

for vnode_id in xrange(VNODE_COUNT):
    vnode_range_starts.append(DATA_ID_COUNT /
                              VNODE_COUNT * vnode_id)
    vnode2node.append(vnode_id % NODE_COUNT)
new_vnode2node = list(vnode2node)
new_node_id = NODE_COUNT
NEW_NODE_COUNT = NODE_COUNT + 
1
vnodes_to_reassign = VNODE_COUNT / NEW_NODE_COUNT
while vnodes_to_reassign > 0:
    
for node_to_take_from in xrange(NODE_COUNT):
        
for vnode_id, node_id in enumerate(new_vnode2node):
            
if node_id == node_to_take_from:
                new_vnode2node[vnode_id] = new_node_id
                vnodes_to_reassign -= 
1
                if vnodes_to_reassign <= 0:
                    
break
        if vnodes_to_reassign <= 0:
            
break
moved_ids = 0
for data_id in xrange(DATA_ID_COUNT):
    data_id = str(data_id)
    hsh = unpack_from(
'>I', md5(str(data_id)).digest())[0]
    vnode_id = bisect_left(vnode_range_starts,
                         hsh % DATA_ID_COUNT) % VNODE_COUNT
    node_id = vnode2node[vnode_id]
    new_node_id = new_vnode2node[vnode_id]
    
if node_id != new_node_id:
        moved_ids += 
1
percent_moved = 100.0 * moved_ids / DATA_ID_COUNT
print '%d ids moved, %.02f%%' % (moved_ids, percent_moved)

90108 ids moved, 0.90%

      

结果显示,仅移动了0.9%的数据。与前面相比,整个集群的性能大大提高了。

clip_image014[4]增加1%的能力=移动0.9%数据

 

  固化虚节点到数据项的映射

 

由于虚节点个数在集群的整个生命周期中是不会变化的,它与数据项的映射关系不会发生变化,改变的仅是vnode与node的映射关系,所以需对以上代码进行优化。

from struct import unpack_from
from hashlib import md5
from time import time

NODE_COUNT = 
100
DATA_ID_COUNT = 10000000
VNODE_COUNT = 1000

begin = time()
vnode2node = []

for vnode_id in xrange(VNODE_COUNT):
    vnode2node.append(vnode_id % NODE_COUNT)
new_vnode2node = list(vnode2node)
new_node_id = NODE_COUNT
vnodes_to_assign = VNODE_COUNT / (NODE_COUNT + 
1)
while vnodes_to_assign > 0:
    
for node_to_take_from in xrange(NODE_COUNT):
        
for vnode_id, node_id in enumerate(vnode2node):
            
if node_id == node_to_take_from:
                vnode2node[vnode_id] = new_node_id
                vnodes_to_assign -= 
1
                if vnodes_to_assign <= 0:
                    
break
        if vnodes_to_assign <= 0:
            
break
moved_id = 0
for data_id in xrange(DATA_ID_COUNT):
    data_id = str(data_id)
    hsh = unpack_from(
'>I', md5(str(data_id)).digest())[0]
    vnode_id = hsh % VNODE_COUNT
#(1)
    node_id = vnode2node[vnode_id]
    new_node_id = new_vnode2node[vnode_id]
    
if node_id != new_node_id:
        moved_id += 
1
percent_moved = 100.0 * moved_id / DATA_ID_COUNT
print '%d ids moved, %.02f%%' % (moved_id, percent_moved)
print '%d seconds pass ...' % (time() - begin)

90108 ids moved, 0.90%

 

 

  预设合理的虚结点数

 

       现在已构建好了一致性哈希ring的原型。但是存在一个问题,以上例子中,1000个虚结点对应着100个结点,结点变动时,虚结点就需要重新分配到结点。当100个结点扩展到1001个结点时,此时至少有一个结点分配不到虚结点,那么就需要再增加虚结点数,而虚结点是与数据项对应的哈希关系,如果改变了虚节点数,那么就需要重新分配所有的数据项,这将导致移动大量的数据。

       所以在设置虚结点数的时候,需要对系统预期的规模做充分考虑,假如集群的规模不会超过6000个结点,那么可以将虚结点数设置为结点数的100倍。这样,变动任意一个结点的负载仅影响1%的数据项。此时有6百万个vnode数,使用2bytes来存储结点数(0~65535)。基本的内存占用是6*106*2bytes=12Mb,对于服务器来说完全可以承受。

       在此,引入了2个概念:

       在swift中,为了区分vnode和node,将vnode称为partition

 

  位操作代替取模操作

 

       此外,在计算机中使用位操作来确定partition的位置比取模更快。所以,在此引入了partition power的概念。

       继续改进ring的代码,设有65536个node(2^16),有128(2^7)倍个partition数(2^23)。由于MD5码是32位的,使用PARTITION_SHIFT(等于32- PARTITION_POWER)将数据项的MD5哈希值映射到partition的2^23的空间中。

from array import array
from hashlib import md5
from struct import unpack_from

PARTITION_POWER = 
23
PARTITION_SHIFT = 32 - PARTITION_POWER
NODE_COUNT = 
65536
DATA_ID_COUNT = 100000000

part2node = array('H')
for part in xrange(2 ** PARTITION_POWER):
    part2node.append(part % NODE_COUNT)
node_counts = [
0] * NODE_COUNT
for data_id in xrange(DATA_ID_COUNT):
    data_id = str(data_id)
    part = unpack_from(
'>I',
        md5(str(data_id)).digest())[
0] >> PARTITION_SHIFT
    node_id = part2node[part]
    node_counts[node_id] += 
1
desired_count = DATA_ID_COUNT / NODE_COUNT
print '%d: Desired data ids per node' % desired_count
max_count = max(node_counts)
over = 
100.0 * (max_count - desired_count) / desired_count
print '%d: Most data ids on one node, %.02f%% over' % \
    (max_count, over)
min_count = min(node_counts)
under = 
100.0 * (desired_count - min_count) / desired_count
print '%d: Least data ids on one node, %.02f%% under' % \
    (min_count, under)


1525: Desired data ids per node
1683: Most data ids on one node, 10.36% over
1360: Least data ids on one node, 10.82% under

       数据不均衡的原因在于数据项相对于partition数太小了(10^8对2^23),若数据项越多,分布越均衡。

 

保证数据安全,引入replica

 

到目前为止,在集群中的数据在本地节点上只有一份,节点一旦发生故障就可能会造成数据的永久性丢失。因此,Swift中引入replica的概念使用冗余副本来保证数据的安全。replica的默认值为3,其理论依据主要来源于NWR策略。

       NWR是一种在分布式存储系统中用于控制一致性级别的一种策略。在Amazon的Dynamo云存储系统中,就应用NWR来控制一致性。每个字母的涵义如下:

       N:同一份数据的Replica的份数
       W:是更新一个数据对象的时候需要确保成功更新的份数
       R读取一个数据需要读取的Replica的份数

       在分布式系统中,数据的单点是不允许存在的。即线上正常存在的Replica数量是1的情况是非常危险的,因为一旦这个Replica再次错误,就可能发生数据的永久性错误。假如我们把N设置成为2,那么,只要有一个存储节点发生损坏,就会有单点的存在。所以N必须大于2。N约高,系统的维护和整体成本就越高。工业界通常把N设置为3。

       因此,在ring的代码中引入replica,数量设置为3,其中 node_ids记录的是3个replica存放的node id。part2node[part]是根据partition id 找到对应的node id。

from array import array
from hashlib import md5
from struct import unpack_from

REPLICAS = 
3
PARTITION_POWER = 16
PARTITION_SHIFT = 32 - PARTITION_POWER
PARTITION_MAX = 
2 ** PARTITION_POWER - 1
NODE_COUNT = 256
DATA_ID_COUNT = 10000000

part2node = array('H')
for part in xrange(2 ** PARTITION_POWER):
    part2node.append(part % NODE_COUNT)
node_counts = [
0] * NODE_COUNT
for data_id in xrange(DATA_ID_COUNT):
    data_id = str(data_id)
    part = unpack_from(
'>I',
        md5(str(data_id)).digest())[
0] >> PARTITION_SHIFT
    node_ids = [part2node[part]]
    node_counts[node_ids[
0]] += 1
    for replica in xrange(1, REPLICAS):
        
while part2node[part] in node_ids:
            part += 
1
            if part > PARTITION_MAX:
                part = 
0
        node_ids.append(part2node[part])
        node_counts[node_ids[-
1]] += 1
desired_count = DATA_ID_COUNT / NODE_COUNT * REPLICAS
print '%d: Desired data ids per node' % desired_count
max_count = max(node_counts)
over = 
100.0 * (max_count - desired_count) / desired_count
print '%d: Most data ids on one node, %.02f%% over' % \
    (max_count, over)
min_count = min(node_counts)
under = 
100.0 * (desired_count - min_count) / desired_count
print '%d: Least data ids on one node, %.02f%% under' % \
    (min_count, under)


117186: Desired data ids per node
118133: Most data ids on one node, 0.81% over
116093: Least data ids on one node, 0.93% under

      

    结果如上,由于使用了256个node,分布约有1%的波动,比较均匀了。

但是存在有2个问题:

  随机分配映射

       首先part2node是基于顺序分配的,对于给定的node,它所有partition的copies均在另两个node上,若某个node频繁宕机,与它相应的两个node上的数据项需要频繁复制。解决方法是随机分配partition到node的映射。

 

  分区容忍性和引入zone

       其次是当前的集群不满足CAP原理中的分区容忍性(Partition Tolerance)。Gilbert 和Lynch分区容忍性定义如下:

       No set of failures less than total network failure is allowed to cause the system to respond incorrectly

         翻译一下,就是除了全部网络节点发生故障以外,所有子节点集合的故障都不允许导致整个系统的响应故障。

现在为止,这些node都在一个机架上,一旦发生断电,网络故障,那么将丧失这一性质。因此就需要一种机制对机器的物理位置进行隔离。所以引入了zone的概念。

       在ring代码中引入zone_count,把这些node分割到16个zone中去。其中partition的replica不能放在同一个node上或同一个zone内。

from array import array
from hashlib import md5
from random import shuffle
from struct import unpack_from

REPLICAS = 
3
PARTITION_POWER = 16
PARTITION_SHIFT = 32 - PARTITION_POWER
PARTITION_MAX = 
2 ** PARTITION_POWER - 1
NODE_COUNT = 256
ZONE_COUNT = 16
DATA_ID_COUNT = 10000000

node2zone = []
while len(node2zone) < NODE_COUNT:
    zone = 
0
    while zone < ZONE_COUNT and len(node2zone) < NODE_COUNT:
        node2zone.append(zone)
        zone += 
1
part2node = array('H')
for part in xrange(2 ** PARTITION_POWER):
    part2node.append(part % NODE_COUNT)
shuffle(part2node)
node_counts = [
0] * NODE_COUNT
zone_counts = [
0] * ZONE_COUNT
for data_id in xrange(DATA_ID_COUNT):
    data_id = str(data_id)
    part = unpack_from(
'>I',
        md5(str(data_id)).digest())[
0] >> PARTITION_SHIFT
    node_ids = [part2node[part]]
    zones = [node2zone[node_ids[
0]]]
    node_counts[node_ids[
0]] += 1
    zone_counts[zones[0]] += 1
    for replica in xrange(1, REPLICAS):
        
while part2node[part] in node_ids and \
                node2zone[part2node[part]] in zones:
            part += 
1
            if part > PARTITION_MAX:
                part = 
0
        node_ids.append(part2node[part])
        zones.append(node2zone[node_ids[-
1]])
        node_counts[node_ids[-
1]] += 1
        zone_counts[zones[-1]] += 1
desired_count = DATA_ID_COUNT / NODE_COUNT * REPLICAS
print '%d: Desired data ids per node' % desired_count
max_count = max(node_counts)
over = 
100.0 * (max_count - desired_count) / desired_count
print '%d: Most data ids on one node, %.02f%% over' % \
    (max_count, over)
min_count = min(node_counts)
under = 
100.0 * (desired_count - min_count) / desired_count
print '%d: Least data ids on one node, %.02f%% under' % \
    (min_count, under)
desired_count = DATA_ID_COUNT / ZONE_COUNT * REPLICAS

print '%d: Desired data ids per zone' % desired_count
max_count = max(zone_counts)
over = 
100.0 * (max_count - desired_count) / desired_count
print '%d: Most data ids in one zone, %.02f%% over' % \
    (max_count, over)
min_count = min(zone_counts)
under = 
100.0 * (desired_count - min_count) / desired_count
print '%d: Least data ids in one zone, %.02f%% under' % \
    (min_count, under)


117186: Desired data ids per node
118782: Most data ids on one node, 1.36% over
115632: Least data ids on one node, 1.33% under
1875000: Desired data ids per zone
1878533: Most data ids in one zone, 0.19% over
1869070: Least data ids in one zone, 0.32% under

       到目前为止,ring的基本功能都已经有了:一致性哈希ring、partition、partition power、replica、zone。目前还差weight以及将以上代码改写为类封装成module。

 

weight

引入weight的概念,目的是“能者多劳”:解决未来添加存储能力更大的node时,使得可以分配到更多的partition。例如,2T 容量的node的partition数为1T的两倍。

       在ring的构建中,加入了weight属性。本例中weight简单地取1和2两个值,根据每个结点在weight和中的比例分配所需partition数。

from array import array
from hashlib import md5
from random import shuffle
from struct import unpack_from
from time import time


class Ring(object):

    
def __init__(self, nodes, part2node, replicas):
        self.nodes = nodes
        self.part2node = part2node
        self.replicas = replicas
        partition_power = 
1
        while 2 ** partition_power < len(part2node):
            partition_power += 
1
        if len(part2node) != 2 ** partition_power:
            
raise Exception("part2node's length is not an "
                            "exact power of 2")
        self.partition_shift = 
32 - partition_power

    
def get_nodes(self, data_id):
        data_id = str(data_id)
        part = unpack_from(
'>I',
           md5(data_id).digest())[
0] >> self.partition_shift
        node_ids = [self.part2node[part]]
        zones = [self.nodes[node_ids[
0]]]
        
for replica in xrange(1, self.replicas):
            
while self.part2node[part] in node_ids and \
                   self.nodes[self.part2node[part]] in zones:
                part += 
1
                if part >= len(self.part2node):
                    part = 
0
            node_ids.append(self.part2node[part])
            zones.append(self.nodes[node_ids[-
1]])
        
return [self.nodes[n] for n in node_ids]

def build_ring(nodes, partition_power, replicas):
    begin = time()
    parts = 
2 ** partition_power
    total_weight = \
        float(sum(n[
'weight'for n in nodes.itervalues()))
    
for node in nodes.itervalues():
        node[
'desired_parts'] = \
            parts / total_weight * node[
'weight']
    part2node = array(
'H')
  
  for part in xrange(2 ** partition_power):
        
for node in nodes.itervalues():
            
if node['desired_parts'] >= 1:
                node[
'desired_parts'] -= 1
                part2node.append(node['id'])
                
break
        else:
            
for node in nodes.itervalues():
                
if node['desired_parts'] >= 0:
                    node[
'desired_parts'] -= 1
                    part2node.append(node['id'])
                    
break
    shuffle(part2node)
    ring = Ring(nodes, part2node, replicas)
    
print '%.02fs to build ring' % (time() - begin)
    
return ring

def test_ring(ring):
    begin = time()
    DATA_ID_COUNT = 
10000000
    node_counts = {}
    zone_counts = {}
    
for data_id in xrange(DATA_ID_COUNT):
        
for node in ring.get_nodes(data_id):
            node_counts[node[
'id']] = \
                node_counts.get(node[
'id'], 0) + 1
            zone_counts[node['zone']] = \
                zone_counts.get(node[
'zone'], 0) + 1
    print '%ds to test ring' % (time() - begin)
    total_weight = float(sum(n[
'weight'for n in
                             ring.nodes.itervalues()))
    max_over = 
0
    max_under = 0
    for node in ring.nodes.itervalues():
        desired = DATA_ID_COUNT * REPLICAS * \
            node[
'weight'] / total_weight
        diff = node_counts[node[
'id']] - desired
        
if diff > 0:
            over = 
100.0 * diff / desired
            
if over > max_over:
                max_over = over
        
else:
            under = 
100.0 * (-diff) / desired
            
if under > max_under:
                max_under = under
    
print '%.02f%% max node over' % max_over
    
print '%.02f%% max node under' % max_under
    max_over = 
0
    max_under = 0
    for zone in set(n['zone'for n in
                    ring.nodes.itervalues()):
        zone_weight = sum(n[
'weight'for n in
            ring.nodes.itervalues() 
if n['zone'] == zone)
        desired = DATA_ID_COUNT * REPLICAS * \
            zone_weight / total_weight
        diff = zone_counts[zone] - desired
        
if diff > 0:
            over = 
100.0 * diff / desired
            
if over > max_over:
                max_over = over
        
else:
            under = 
100.0 * (-diff) / desired
            
if under > max_under:
                max_under = under
    
print '%.02f%% max zone over' % max_over
    
print '%.02f%% max zone under' % max_under

if __name__ == '__main__':
    PARTITION_POWER = 
16
    REPLICAS = 3
    NODE_COUNT = 256
    ZONE_COUNT = 16
    nodes = {}
    
while len(nodes) < NODE_COUNT:
        zone = 
0
        while zone < ZONE_COUNT and len(nodes) < NODE_COUNT:
            node_id = len(nodes)
            nodes[node_id] = {
'id': node_id, 'zone': zone,
                              
'weight'1.0 + (node_id % 2)}
            zone += 
1
    ring = build_ring(nodes, PARTITION_POWER, REPLICAS)
    test_ring(ring)


0.10s to build ring
162s to test ring
118581: Most data ids on one node,1.19% over
115537: Least data ids on one node, 1.41% under
1878343: Most data ids in one zone, 0.18% over
1870880: Least data ids in one zone, 0.22% under

       每个node上分布的最大波动为1.19%和1.41%,而zone上的波动分布在0.22%以下。

 

总结

       以上就是ring的构建原理分析,引入一致性哈希的原因是为了减少由于增加结点导致数据项移动的数量来提高单调性,引入partition的原因是为了减少由于节点数过少导致移动过多的数据项、引入replica的原因是防止数据单点提高冗余性,引入zone的原因是为了保证分区容忍性、引入weight的原因是为了保证partition分配的均衡。

那么Ring的结构是否就此止步了呢,在Swift开发人员的博客中提到,只要发现更好的数据映射机制,就将Ring推倒重来,所以未来Ring会如何演化,咱们也可以参与其中,促其不断地进化。

 

 

致谢

本文为学习笔记,写于SAE实习期间,代码分析来自gholt的swift开发博文,一致性哈希图片来自于sparkliang的博文,也感谢zzcase在邮件讨论中给予我的诸多帮助

 

参考文章

http://tlohg.com/building-a-consistent-hashing-ring-part-1

http://blog.csdn.net/zzcase/article/details/6709961

http://blog.csdn.net/sparkliang/article/details/5279393

http://blog.csdn.net/x15594/article/details/6270242

http://ultimatearchitecture.net/index.php/2010/06/22/quorum-nwr/

http://ultimatearchitecture.net/index.php/2010/06/22/consistent_hash_algorithn/

http://ultimatearchitecture.net/index.php/2010/06/22/eventually_consistency_base-vs-acid/

http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.23.3738

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深入云存储系统Swift核心组件:Ring实现原理剖析 的相关文章

  • 如何从 Linux 内核模块获取使用计数?

    我对正在开发的内核模块的使用计数有疑问 我想打印它以进行调试 如何从模块代码中获取它 有问题的内核版本 Linux 2 6 32 module refcount http lxr linux no linux v2 6 34 1 inclu
  • 使用 XCode 进行测试时,MacOS 应用程序本地通知未显示

    我尝试将横幅通知生成器添加到我的 macOS swift 应用程序中 但在 XCode 中运行测试时 横幅没有出现 并且通知中心中也没有任何新通知可见 我计算机上的其他应用程序会定期生成通知 我错过了什么 我已按要求授予许可 我的应用程序委
  • 在 Swift 中将单个整数值视为一个范围

    我需要验证字符串的长度 字符计数允许的值为 6 9 个字符 12个字符 15 个字符 所有具有不同字符数的字符串均无效 因此 我想创建一个 Swift 函数 它接受多个范围并计算字符串 extension String func evalu
  • 为什么 Xcode 会为选择器列出自动完成方法?

    自从我升级到最新的 Xcode 以来 自动完成功能就列出了 删除线 方法selector目的 我无法判断这是 Xcode 的错误还是有一些我不知道的新语法更改 如果你把它插入你的 Playground 并删除里面的方法 selector t
  • 如何使用 AVAudioPCMBuffer 播放声音

    我无法使用 AVAudioPCMBuffer 播放声音 尽管我可以使用 AVAudioFile 播放 我收到这个错误 错误 AVAudioBuffer mm 169 AVAudioPCMBuffer initWithPCMFormat fr
  • 检查 Firebase 中是否存在用户名[重复]

    这个问题在这里已经有答案了 我有一个这样的数据库 users UID1 用户名 用户 1 UID2 Username of user 2 UID3 Username of user 3 UID4 Username of user 4 等等
  • C++:Linux平台上的线程同步场景

    我正在为 Linux 平台实现多线程 C 程序 其中我需要类似于 WaitForMultipleObjects 的功能 在搜索解决方案时 我发现有一些文章描述了如何在 Linux 中实现 WaitForMultipleObjects 功能
  • 无需 root 访问权限即可安装 zsh? [关闭]

    Closed 这个问题是无关 help closed questions 目前不接受答案 有可能 以及如何 我确实需要在几台具有 ssh 访问权限 但没有 root 访问权限 的远程计算机上使用此功能 下载 zsh wget O zsh t
  • iOS 11.x 系统颜色

    我读过很多关于如何自定义视图颜色的文章 但没有任何关于检索标准控件 如 iOS 11 x 或以前版本中的导航栏 状态栏和选项卡栏 的系统颜色的文章 UIColor 类有 3 种系统颜色 但它们几乎没有用 例如 调用 UINavigation
  • 将node.js +expressjs应用程序的NODE_ENV设置为ubuntu下的守护进程

    我按照这些说明让守护进程正常工作 http kevin vanzonneveld net techblog article run nodejs as a service on ubuntu karmic http kevin vanzon
  • Swift - 如何使用 malloc?

    我正在尝试将 Objective C 应用程序转换为 Swift 但我不知道如何实现 malloc 可以在 Swift 中使用吗 谢谢 当您使用 Swift 中的 ObjC 或 C 时 您需要实现桥接标头 然后 桥接标头导出的函数可在 Sw
  • 如何反汇编、修改然后重新组装 Linux 可执行文件?

    无论如何 这可以做到吗 我使用过 objdump 但它不会产生我所知道的任何汇编器都可以接受的汇编输出 我希望能够更改可执行文件中的指令 然后对其进行测试 我认为没有任何可靠的方法可以做到这一点 机器代码格式非常复杂 比汇编文件还要复杂 实
  • 为什么docker容器提示“权限被拒绝”?

    我使用以下命令来运行 docker 容器 并从主机映射目录 root database 到容器 tmp install database docker run it name oracle install v root database t
  • 如何缩放 CAShapeLayer

    我很快就成功制作了动画bezier path它包含在一个CAShapeLayer 我唯一的问题是将其实现到其他屏幕尺寸上 有谁知道我如何扩展CAShapeLayer里面有路径吗 即使其变为原始大小的一半 使用这个函数 var shapela
  • 转换 [NSNumber numberWithUnsignedInt:kCVPixelFormatType_32BGRA], (id)kCVPixelBufferPixelFormatTypeKey, nil];到斯威夫特

    这里出现错误 绞尽脑汁 尝试了各种组合 找不到类型 NSDictionary 的初始化程序 该初始化程序接受类型 object Int forKey CFString 的参数列表 configure the pixel format Obj
  • 完整的 C++ i18n gettext()“hello world”示例

    我正在寻找完整的 i18ngettext 你好世界的例子 我已经开始了一个基于的脚本使用 GNU gettext 的本机语言支持教程 https web archive org web 20130330233819 http oriya s
  • 使用Combine,如何在网络请求后取消分配订阅

    如果您使用组合来处理网络请求URLSession 那么你需要保存Subscription 又名 AnyCancellable 否则它会立即释放 从而取消网络请求 稍后 当网络响应被处理后 您想要取消分配订阅 因为保留它会浪费内存 下面是执行
  • 为什么默认情况下不启用 arp 忽略/通告 [关闭]

    Closed 这个问题是无关 help closed questions 目前不接受答案 我有一个需要经验才能回答的具体问题 为什么 arp ignore arp announce 在 Linux 安装 例如 debian 上默认不启用 有
  • 在 Swift 中使用 NSCoding 归档可选结构数组?

    我已经在 Obj C 中完成了大量 NSCoding 归档 但我不确定它如何处理 Swift 中的结构 也不确定它如何处理具有可选值的数组 这是我的代码 public struct SquareCoords var x Int y Int
  • 比较linux中的两个未排序列表,列出第二个文件中的唯一项

    我有 2 个包含号码列表 电话号码 的文件 我正在寻找一种列出第二个文件中第一个文件中不存在的数字的方法 我尝试过各种方法 comm getting some weird sorting errors fgrep v x f second

随机推荐

  • git提交代码会报 vue-cli-service lint found some errors. Please fix them and try committing again

    转载 https blog csdn net j15533415886 article details 92003047 转载 https blog csdn net qq 42180156 article details 10248098
  • web-view 调用微信小程序的方法

    1 1api文件夹下新建api js 获取微信公众号配置授权 后端提供接口 initConfig url gt return API GET api getJsapiSignature url url 1 2 utils文件夹下新建一个js
  • 数据结构day3(2023.7.17)

    一 Xmind整理 二 课上练习 练习1 时间复杂度 时间复杂度 只保留最高阶 f n 3 n 3 n 2 100 n T n O 3 n 3 n 2 100 n O 3 n 3 O n 3 1 gt O 1 常数阶 int t a 1 a
  • 使用百度地图JavaScript实现驾车/公交/步行导航功能

    1 2 3 4 5
  • 【语义分割】12、Fully Attentional Network for Semantic Segmentation

    文章目录 一 背景和动机 二 方法 三 效果 出处 AAAI2022 一 背景和动机 语义分割中 non local NL 的方法起到了很好了捕捉 long range 信息的作用 大致可分为 Channel non local 和 Spa
  • 使用insert into和insert overwrite插入数据的区别【Hive or Odps】

    1 创建一个测试表insert test 标识创建表insert test 默认新增id列 并插入一条数据1 CREATE TABLE insert test AS SELECT 1 AS id 2 插入数据 使用insert into插入
  • 记录:查看ubuntu版本信息。

    cat proc version uname a lsb release a WSL Ubuntu 20 04运行结果 lsb release a 查看Release和Codename uname a 内核版本和操作系统32 64位 cat
  • 32位机内存管理(下)

    既然这样 段描述符表放在那里呢 80386中引入了两个新的寄存器来管理描述符表 一个是48位的全局描述符表寄存器GDTR 一个是16位的局部描述符表寄存器LDTR 那么 为什么有两个描述符表寄存器呢 GDTR指向的描述符表为全局描述符表GD
  • 数据库原理 封锁的粒度

    1 封锁粒度是什么 封锁对象的大小称为封锁的粒度 封锁对象 逻辑单元 物理单元 2 选择封锁粒度的原则 封锁粒度和系统的并发度 系统的开销密切相关 封锁的粒度越大 数据库能够封锁的数据单元就越少 并发度就越小 系统开销也就越小 封锁的粒度越
  • Android三维模型解决方案

    1 原生OpenGL ES 无需多说 虽然OpenGL已经出来这么多年了 但无疑还是现在普及最广的 尤其在移动端 所以用原生OpenGL ES方案可以解决 2 jpct 最推荐 JPCT是一款基于OpenGL技术开发的3D图形引擎 PC环境
  • 数据迁移-jdbc

    1 jdbc工具类 import java io BufferedInputStream import java io FileInputStream import java io InputStream import java util
  • nginx的https和http共存反向代理配置

    一 设置http反向代理 正在上传 重新上传取消 upstream ly com server 192 168 1 100 88 server 192 168 1 101 88 upstream home ly com server 192
  • Git本地项目推送到远程仓库

    目录 1 初始化git 2 添加远程地址并给地址起一个别名 3 合并远程代码 4 推送 5 常见问题 6 无法推送 7 查看远程仓库信息 8 拉取与本地不相关的git项目 9 配置ssh公钥 1 初始化git 进入项目目录 git init
  • np.mgrid

    功能 返回多维结构 常见的如2D图形 3D图形 np mgrid 第1维 第2维 第3维 第n维的书写形式为 a b c c表示步长 为实数表示间隔 该为长度为 a b 左开右闭 或 a b cj cj表示步长 为复数表示点数 该长度为 a
  • R语言实战笔记--第十六章 高级图形进阶

    R语言实战笔记 第十六章 高级图形进阶 标签 空格分隔 R语言 lattice 交互式图形 图形系统简介 本文为R语言实战中最后一章 介绍的是图形的高级进阶 主要讲述了两个包 lattice和ggplot2 以及交互式图形的做法 极大扩展了
  • NacosSpringCloud

    Nacos尝试配置SpringCloud 新来了一个项目 有并发要求 选择了springcloud这套 至于eureka已经闭源了 就没有考虑 最后选择了阿里的nacos 服务发现和配置中心都集中在注册中心 配置支持动态加载 中文文档丰富
  • 视频质量算法有几种的介绍,视频质量技术及编码

    首先介绍SSIM是一种全参考 Full Rerence 视频质量评价算法 全参考视频图像质量评价指标 它分别从亮度 对比度 结构三方面度量视频图像相似性 而全参考评价算法必须同时知道原始视频和失真视频 也就是想计算结构相似度 就得先找到两个
  • 打开win7的系统更新服务器失败,win7的windows update无法启动,手动在服务里启动提示“错误2:系统找不到指定文件...

    您好 该问题有可能是系统文件损坏导致的 建议您尝试以下方法操作 方法一 建议您暂时卸载掉电脑上安装的第三方杀毒软件 再打开控制面板 操作中心 疑难解答 点使用Windows Update 解决问题 根据提示自动修复更新是出现的问题 方法二
  • 关于HttpClient请求获取数据

    httpClient请求获取网站数据 今天一网友问我 他写的httpClient请求为什么获取不到数据 他写的代码如下 StringBuffer buffer new StringBuffer String url1 http api ji
  • 深入云存储系统Swift核心组件:Ring实现原理剖析

    深入云存储系统Swift核心组件 Ring实现原理剖析 简介 OpenStack是一个美国国家航空航天局和Rackspace合作研发的开源云计算项目 并成为Apache下的一个重要开源项目 目前已经发展到了180家公司参与其中 OpenSt