在Swift 1.8.0(Grizzly)的新特性中有这么一条:Added support for a region tier above zones,即swift可以允许开发者将zones组织成一个组进行管理了,这个组就是region。
这两天在看Swift G版Ring部分的源码的时候,也发现device字典中增加了region属性,因此决定仔细的理解一下这个层次概念。
首先推荐两篇文章,我对region的理解从这两篇文章中获益颇多:
第一篇是Swift的重要贡献者、region特性的贡献者SwiftStack的文章:A Globally Distributed OpenStack Swift Cluster;
第二篇不止介绍了region的概念,更将region层次下的proxy server等工作流程进行了详细描述。(这篇需要FQ-。-)
The Ring without Multi-regions
标准的Swift ring是一个允许你将存储设备划分为buckets或者zones的数据结构。在介绍包含regions概念的Ring之前,让我们先来回顾一下没有添加region概念版本的Ring结构:
在Essex版本中,Ring builder严格的保证同一个对象的不同的副本分布在不同的zones中,否则Ring的创建过程无法完成。因此,Swift developer就必须在集群中部署至少和副本数一样多的zones以保证Ring可以被成功创建。
在Folsom版本中,Ring文件的结构被进行了改动,并重新定义了ring balancing的算法,从而极大的提高了Ring创建的效率。在这个版本中,原本严格的保证副本分布在不同zones中的策略被替换成了另一种更为灵活的算法,这个新的算法将zones、nodes、devices组织,从而形成tiers的结构进行分配。
The Ring with Multi-tiers
以下代码是swift 1.7.6(Folsom)中构造tiers结构的代码(common.ring.utils.py)。
def tiers_for_dev(dev): """ Returns a tuple of tiers for a given device in ascending order by length. :returns: tuple of tiers """ t1 = dev[\'zone\'] t2 = "{ip}:{port}".format(ip=dev.get(\'ip\'), port=dev.get(\'port\')) t3 = dev[\'id\'] return ((t1,), (t1, t2), (t1, t2, t3))
Folsom版本的ring balancer将同一个对象的副本分配到尽可能远的位置(as-far-as-possible or as-unique-as-possible)。当然,最完美的方案是将这些副本放置到不同的zones中([zone] tier),但是如果只有一个zone可以使用时,那么就尽量将副本分配到不同的nodes中([zone ip:port] tier),相似的,当如果只有一个node可以使用时,那么就尽量将副本分配到这个节点上的不同devices([zone ip:port device] tier)。
这种“as-unique-as-possible”的算法,具有支持 geographically distributed cluster 的潜质(每向上一层,就增大了一个地理范围),它可以方便的将一个小的集群扩展成一个大集群。因此,我们可以通过增加另一个region tier到这个层次中实现集群的地理分布支持。一个region本质上是一组共享相同位置的zones,这组zones可以是一个机架,也可以是一个数据中心。
下图是“as-unique-as-possible”策略下,不同规模Swift中副本存放策略的示意图,其中绿色圆点代表一个副本,椭圆代表一个disk,一个立方体代表一个node...
The Ring with Multi-regions -> A Globally Distributed Cluster
通过多tiers结构的Ring的介绍,region的概念就非常好理解了。
为了创建一个global cluster,SwiftStack为swift增加了region的概念。Region扩展了Tier的层次,是一个比zone更大的区域,一组zones构成的tier是一个region。
一个全球的、支持副本的集群可以通过将storage nodes部署在不同的region中进行创建。同一个region中的zones之间的延迟是相对较低的,Proxy nodes会对距离它近的region具有亲和性(local affinity,优先访问),并根据storage nodes所在的region采用“乐观写”的方式将数据写入最近region的storage nodes。如果需要的话,客户端可以通过选项执行一个跨区域(忽略local affinity)的读/写操作。
如下,为swift 1.8.0 中构造tiers结构的代码(common.ring.utils.py)。
def tiers_for_dev(dev): """ Returns a tuple of tiers for a given device in ascending order by length. :returns: tuple of tiers """ t1 = dev[\'region\'] t2 = dev[\'zone\'] t3 = "{ip}:{port}".format(ip=dev.get(\'ip\'), port=dev.get(\'port\')) t4 = dev[\'id\'] return ((t1,), (t1, t2), (t1, t2, t3), (t1, t2, t3, t4))
从以上代码中,我们可以清晰的看到region被增加到了最顶层的tier,并作为设备dev的一个新属性,实现为dev字典的一个新key-value对。
默认情况下,swift集群的region为1,从而保证cluster中一定存在一个region,并用“()”表明一个region,作为tier tree的root。以下代码为tier tree的构建过程,代码的doc string将包含region层次的tier tree结构描述的非常清楚,为了便于理解,所以我就一并贴上来了。
def build_tier_tree(devices): """ Construct the tier tree from the zone layout. The tier tree is a dictionary that maps tiers to their child tiers. A synthetic root node of () is generated so that there\'s one tree, not a forest. Example: region 1 -+---- zone 1 -+---- 192.168.101.1:6000 -+---- device id 0 | | | | | +---- device id 1 | | | | | +---- device id 2 | | | +---- 192.168.101.2:6000 -+---- device id 3 | | | +---- device id 4 | | | +---- device id 5 | +---- zone 2 -+---- 192.168.102.1:6000 -+---- device id 6 | | | +---- device id 7 | | | +---- device id 8 | +---- 192.168.102.2:6000 -+---- device id 9 | +---- device id 10 region 2 -+---- zone 1 -+---- 192.168.201.1:6000 -+---- device id 12 | | | +---- device id 13 | | | +---- device id 14 | +---- 192.168.201.2:6000 -+---- device id 15 | +---- device id 16 | +---- device id 17 The tier tree would look like: { (): [(1,), (2,)], (1,): [(1, 1), (1, 2)], (2,): [(2, 1)], (1, 1): [(1, 1, 192.168.101.1:6000), (1, 1, 192.168.101.2:6000)], (1, 2): [(1, 2, 192.168.102.1:6000), (1, 2, 192.168.102.2:6000)], (2, 1): [(2, 1, 192.168.201.1:6000), (2, 1, 192.168.201.2:6000)], (1, 1, 192.168.101.1:6000): [(1, 1, 192.168.101.1:6000, 0), (1, 1, 192.168.101.1:6000, 1), (1, 1, 192.168.101.1:6000, 2)], (1, 1, 192.168.101.2:6000): [(1, 1, 192.168.101.2:6000, 3), (1, 1, 192.168.101.2:6000, 4), (1, 1, 192.168.101.2:6000, 5)], (1, 2, 192.168.102.1:6000): [(1, 2, 192.168.102.1:6000, 6), (1, 2, 192.168.102.1:6000, 7), (1, 2, 192.168.102.1:6000, 8)], (1, 2, 192.168.102.2:6000): [(1, 2, 192.168.102.2:6000, 9), (1, 2, 192.168.102.2:6000, 10)], (2, 1, 192.168.201.1:6000): [(2, 1, 192.168.201.1:6000, 12), (2, 1, 192.168.201.1:6000, 13), (2, 1, 192.168.201.1:6000, 14)], (2, 1, 192.168.201.2:6000): [(2, 1, 192.168.201.2:6000, 15), (2, 1, 192.168.201.2:6000, 16), (2, 1, 192.168.201.2:6000, 17)], } :devices: device dicts from which to generate the tree :returns: tier tree """ tier2children = defaultdict(set) for dev in devices: for tier in tiers_for_dev(dev): if len(tier) > 1: tier2children[tier[0:-1]].add(tier) else: tier2children[()].add(tier) return tier2children
更多的关于在multi-regions下swift中各个操作的算法细节,可以参见我在文章开头处给出的两个blogs链接,其中都有非常详细、图文并茂的介绍,我就不重复造轮子啦 =D