深入云存储系统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实现原理剖析 的相关文章

  • Xcode 12 根本没有调用动态链接 Firebase 函数?

    因此 我有一个正在运行的动态链接 当我单击它时它会打开应用程序 但不会发生动态链接的处理 这是因为下面看到的应用程序功能从未输入过 我不知道为什么 func handleIncomingDynamicLink dynamicLink Dyn
  • 嵌入清单文件以要求具有 mingw32 的管理员执行级别

    我正在 ubuntu 下使用 i586 mingw32msvc 交叉编译应用程序 我很难理解如何嵌入清单文件以要求 mingw32 具有管理员执行级别 对于我的例子 我使用了这个hello c int main return 0 这个资源文
  • 来自守护程序的错误响应:加入会话密钥环:创建会话密钥:超出磁盘配额

    我尝试在我的服务器上安装 docker 使用本教程 https docs docker com install linux docker ce ubuntu 我想远程运行 docker 镜像并使用 portainer Web 界面来管理一切
  • 在 Swift 中获取 UIImage 的主颜色

    我正在尝试在 Swift 中获取 UIImage 的主颜色 并尝试移植这段代码 https stackoverflow com questions 13694618 objective c getting least used and mo
  • tableView.dequeueReusableCellWithIdentifier() 导致应用程序挂起

    原帖 我们最近将我们的应用程序转换为 Swift 2 0 和 iOS9 我看到的一个奇怪的问题是调用 tableView dequeueReusableCellWithIdentifier 会导致应用程序挂在模拟器中 The code fu
  • Firestore 更新后仅获取文档一次

    我有一个 tableView 它从 Firestore 集合中获取所有文档 并且我只想在用户刷新 tableView 后将最后一个文档添加到 Firestore 时获取一次 然后我想删除侦听器 以便当用户刷新 tableView 时仅获取文
  • 如何才能将 TCP 连接返回到同一端口?

    机器是 RHEL 5 3 内核 2 6 18 有时我在 netstat 中注意到我的应用程序有连接 建立了 TCP 连接本地地址 and 国外地址是一样的 其他人也报告了同样的问题 症状与链接中描述的相同 客户端连接到本地运行的服务器的端口
  • 我如何从子视图导航到 mainviewcontroller

    我刚刚开始使用 swift 我创建了一个子视图 上面有一个按钮 我想使用该按钮将我带到我的主视图控制器 我对不同的按钮使用了相同的功能 但是在同一文件中具有一个功能允许该按钮工作 代码如下 var playAgainButton UIBut
  • 在 docker 中重定向命令输出

    我想为我的服务器做一些简单的日志记录 它是一个在 Docker 容器中运行的小型 Flask 应用程序 这是 Dockerfile Dockerfile FROM dreen flask MAINTAINER dreen WORKDIR s
  • Tomcat Intellij Idea:远程部署

    RackSpace 云服务器 Ubuntu 12 04 Intellij Idea 11 1 2 Windows 8 Tomcat 7 0 26 JDK 6 在 Intellij Idea 上 当我尝试在远程 Tomcat 7 服务器上运行
  • vmsplice() 和 TCP

    在原来的vmsplice 执行 有人建议 http lwn net Articles 181169 如果您的用户态缓冲区是管道中可容纳的最大页面数的 2 倍 则缓冲区后半部分成功的 vmsplice 将保证内核使用缓冲区的前半部分完成 但事
  • UIButton 导致无法识别的选择器发送到实例

    我正在尝试使用 for 循环创建多个按钮 但在使用 sender 函数时遇到问题 我有以下代码 func setUpButtons for i in 1 3 let btn UIButton UIButton frame CGRect x
  • 如何使用 NSUserDefaults 在 Swift 中存储自定义类的数组?

    我有一个名为的自定义类Person当某人输入信息时 它会存储有关某人的各种属性 class Person Person dictionary variable var name String var age String var html
  • 当我输入字符时,SwiftUI 中的 TextField 失去焦点

    当我在文本字段中输入字符时遇到问题 在练习集视图 我必须重新单击文本框才能输入另一个字符 如果我从文本字段中删除绑定 我可以流畅地输入文本 我认为这与我的演讲者班级和更新集函数重新创建一个集合实例 因为我必须替换数组中两层深处的一些值 Co
  • 找不到包“gdk-pixbuf-2.0”

    我正在尝试在 Amazon Linux 发行版实例上构建 librsvg 我已经通过 yum 安装了大部分依赖项 其中一些在实例上启用的默认 yum 存储库中不可用 因此必须从头开始构建它们 我已经走了很远 但还停留在最后一点 跑步时sud
  • 减少 CoreData 的调试输出?

    我正在开发一个使用 CoreData 的 iOS macOS 项目 它工作正常 但它会向控制台输出大量调试信息 这使得控制台无法使用 因为我的打印语句隐藏在所有与 CoreData 相关的内容中 我有一个非常简单的 CoreData 设置
  • Swift:使具有相同“形状”的两种类型符合通用协议

    我有两种不同的类型 它们代表相同的数据 并且具有完全相同的 形状 这两种不同的类型是代码生成的 我被迫处理它们 但是 我想让它们符合一个通用的协议 这样我就可以对这两种类型一视同仁 这是一个例子 假设这是我所坚持的两种代码生成类型 stru
  • 什么是 WKWebView 中的 WKErrorDomain 错误 4

    fatal error LPWebView encounters an error Error Domain WKErrorDomain Code 4 A JavaScript exception occurred UserInfo 0x7
  • FileOutputStream.close() 中的设备 ioctl 不合适

    我有一些代码可以使用以下命令将一些首选项保存到文件中FileOutputStream 这是我已经写了一千遍的标准代码 FileOutputStream out new FileOutputStream file try BufferedOu
  • Xcode 8 / Swift 3:“UIViewController 类型的表达式?未使用”警告

    我有以下函数 它之前编译得很干净 但在 Xcode 8 中生成警告 func exitViewController navigationController popViewController animated true UIViewCon

随机推荐

  • 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