作为一个分布式存储系统,如何合理地分布数据非常重要。Ceph系统的许多特性,如去中心化、易扩展性和负载均衡等都和它采用的CRUSH数据分布算法是密不可分的。经过不断地实践和优化,Ceph作为开源云存储重要的后端之一,被大家认可,这也离不开CRUSH算法的先进设计。

CRUSH算法的基本特性

为一个分布式存储系统实现数据分布算法不简单,至少需要考虑下述情况。

· 实现数据的随机分布,并在读取时能快速索引。

· 能够高效地重新分布数据,在设备加入、删除和失效时最小化数据迁移。

· 能够根据设备的物理位置合理地控制数据的失效域。

· 支持常见的镜像、磁盘阵列、纠删码等数据安全机制。

· 支持不同存储设备的权重分配,来表示其容量大小或性能。

CRUSH算法在设计时就考虑了上述5种情况。如下图所示,CRUSH算法根据输入x得到随机的n个(图中为3个)有序的位置即OSD.k,并保证在相同的元数据下,对于输入x的输出总是相同的。也就是说,Ceph只需要在集群中维护并同步少量的元数据,各个节点就能独立计算出所有数据的位置,并且保证输出结果对于同样的输入x是相同的。而CRUSH算法的计算过程无须任何中心节点介入,这种去中心化设计在理论上能够承受任何节点的失效问题,降低了集群规模对性能的不利影响。

CRUSH元数据包含了CRUSH Map、OSDMap和CRUSH Rule。其中,CRUSH Map保存了集群中所有设备或OSD存储节点的位置信息和权重设置,使CRUSH算法能够感知OSD的实际分布和特性,并通过用户定义的CRUSH Rule来保证算法选择出来的位置能够合理分布在不同的失效域中。而OSDMap保存了各个OSD的运行时状态,能够让CRUSH算法感知存储节点的失效、删除和加入情况,产生最小化的数据迁移,提高Ceph在各种情况下的可用性和稳定性。关于这些元数据概念和算法细节会在后续内容介绍,同时也会重点介绍Ceph与CRUSH算法相关的设计,以及一些CRUSH算法的具体用法。

 

CRUSH算法中的设备位置及状态

在介绍CRUSH算法的细节之前,我们有必要先介绍一下CRUSH元数据是如何维护OSD设备信息的。其中,CRUSH Map主要保存了OSD的物理组织结构,而OSDMap保存了各OSD设备的运行时状态。CRUSH算法即通过一系列精心设计的哈希算法去访问和遍历CRUSH Map,按照用户定义的规则选择正常运行的OSD来保存数据对象。

1.CRUSH Map与设备的物理组织

CRUSH Map本质上是一种有向无环图(DAG),用来描述OSD的物理组织和层次结构。其结构如下图所示,所有的叶子节点表示OSD设备,而所有的非叶子节点表示桶。桶根据层次来划分可以定义不同的类型(CRUSH Type或Bucket Class),如根节点、机架、电源等。

在默认情况下,Ceph会创建两种类型的桶,分别是根节点和主机,然后把所有的OSD设备都放在对应的主机类型桶中,再把所有主机类型桶放入一个根节点类型桶中。在更复杂的情况下,例如,要防止由于机架网络故障或电源失效而丢失数据,就需要用户自行创建桶的类型层次并建立对应的CRUSH Map结构。

查看当前集群的CRUSH Map可以使用如下命令:

其中,负数ID的行表示CRUSH桶,非负数ID的行表示OSD设备,CLASS表示OSD设备的Device Class,TYPE表示桶类型,即CRUSH Type。

2.CRUSH Map 叶子

CRUSH Map中所有的叶子节点即OSD设备,每个OSD设备在CRUSH Map中具有名称、Device Class和全局唯一的非负数ID。其中,默认的Device Class有硬盘驱动器、固态硬盘和NVMe 3种,用于区分不同的设备类型。Ceph可以自动识别OSD的Device Class类型,当然也可以由用户手动创建和指定。当前Ceph内部会为每个Device Class维护一个Shadow CRUSH Map,在用户规则中指定选择某一个Device Class,比如固态硬盘时,CRUSH算法会自行基于对应的Shadow CRUSH Map执行。可以使用以下命令查看Device Class和Shadow CRUSH Map:

3.CRUSH Map桶

CRUSH Map中所有的非叶子节点即桶,桶也具有名称、Bucket Class和全局唯一的负数ID。属于同一种Bucket Class的桶往往处于CRUSH Map中的同一层次,其在物理意义上往往对应着同一类别的失效域,如主机、机架等。

作为保存其他桶或设备的容器,桶中还可以定义具体的子元素列表、对应的权重(Weight)、CRUSH算法选择子元素的具体策略,以及哈希算法。其中,权重可以表示各子元素的容量或性能,当表示为容量时,其值默认以TB为单位,可以根据不同的磁盘性能适当微调具体的权重。CRUSH算法选择桶的子元素的策略又称为Bucket Type,默认为Straw方式,它与CRUSH算法的实现有关,我们只需要知道不同的策略与数据如何重新分布、计算效率和权重的处理方式密切相关,具体的细节后续会进行介绍。桶中的哈希算法默认值为0,其意义是rjenkins1,即Robert Jenkin's Hash。它的特点是可以保证即使只有少量的数据变化,或者有规律的数据变化也能导致哈希值发生巨大的变化,并让哈希值的分布接近均匀。同时,其计算方式能够很好地利用32位或64位处理器的计算指令和寄存器,达到较高的性能。在CRUSH Map中,Bucket Class与桶的具体定义如下:

4.OSDMap与设备的状态

在运行时期,Ceph的Monitor会在OSDMap中维护一种所有OSD设备的运行状态,并在集群内同步。其中,OSD运行状态的更新是通过OSD-OSD和OSD-Monitor的心跳完成的。任何集群状态的变化都会导致Monitor中维护的OSDMap版本号(Epoch)递增,这样Ceph客户端和OSD服务就可以通过比较版本号大小来判断自己的Map是否已经过时,并及时进行更新。

OSD设备的具体状态可以是在集群中(in)或不在集群中(out),以及正常运行(up)或处于非正常运行状态(down)。其中OSD设备的in、out、up和down状态可以任意组合,只是当OSD同时处于in和down状态时,表示集群处于不正常状态。在OSD快满时,也会被标记为full。我们可以通过以下命令查询OSDMap的状态,或者手动标记OSD设备的状态:

CRUSH中的规则与算法细节

在了解了CRUSH Map是如何维护OSD设备的物理组织,以及OSDMap是如何维护OSD设备的运行时状态后,我们就更容易理解CRUSH Rule是如何做到个性化的数据分布策略,以及CRUSH算法的实现机制了。

1.CRUSH Rule基础

仅仅了解了OSD设备的位置和状态,CRUSH算法还是无法确定数据该如何分布。由于具体使用需求和场景的不同,用户可能会需要截然不同的数据分布方式,而CRUSH Rule就提供了一种方式,即通过用户定义的规则来指导CRUSH算法的具体执行。其场景主要如下所示。

· 数据备份的数量:规则需要指定能够支持的备份数量。

· 数据备份的策略:通常来说,多个数据副本是不需要有顺序的;但是纠删码不一样,纠删码的各个分片之间是需要有顺序的。所以CRUSH算法需要了解各个关联的副本之间是否存在顺序性。

· 选择存储设备的类型:规则需要能够选择不同的存储设备类型来满足不同的需求,比如高速、昂贵的固态硬盘类型设备,或者低速、廉价的硬盘驱动器类型设备。

· 确定失效域:为了保证整个存储集群的可靠性,规则需要根据CRUSH Map中的设备组织结构选择不同的失效域,并依次执行CRUSH算法。

Ceph集群通常能够自动生成默认的规则,但是默认规则只能保证集群数据备份在不同的主机中。实际情况往往更加精细和复杂,这就需要用户根据失效域自行配置规则,保存在CRUSH Map中,代码如下:

其中,规则能够支持的备份数量是由min_size和max_size确定的,type确定了规则所适用的备份策略。Ceph在执行CRUSH算法时,会通过ruleset对应的唯一ID来确定具体执行哪条规则,并通过规则中定义的step来选择失效域和具体的设备。

2.CRUSH Rule的step take与step emit

CRUSH Rule执行步骤中的第一步和最后一步分别是step take与step emit。step take通过桶名称来确定规则的选择范围,对应CRUSH Map中的某一个子树。同时,也可以选择Device Class来确定所选择的设备类型,如固态硬盘或硬盘驱动器,CRUSH算法会基于对应的Shadow CRUSH Map来执行接下来的step choose。step take的具体定义如下:

step emit非常简单,即表示步骤结束,输出选择的位置。

3.step choose与CRUSH算法原理

CRUSH Rule的中间步骤为step choose,其执行过程即对应CRUSH算法的核心实现。每一step choose需要确定一个对应的失效域,以及在当前失效域中选择子项的个数。由于数据备份策略的不同(如镜像与纠删码),step choose还要确定选择出来的备份位置的排列策略。其定义如下:

待选的每一个子项都有各自的权重值,如果按照权重随机选择就没办法保证每次都选择相同的子项,CRUSH算法的精髓就是使用伪随机方法选择需要的子项,并且只要每次的输入相同通过该伪随机方法选择出来的子项也是固定的。具体过程如下:

(1)、CRUSH_HASH( PG_ID, OSD_ID, r ) ===> draw

(2)、( draw &0xffff ) * osd_weight ===> osd_straw

(3)、pick up high_osd_straw

r作为一个常数,第一行实际上就是将PG_ID, OSD_ID和r一起当做CRUSH_HASH的输入,求出一个十六进制输出,这和HASH(对象名)到PG的映射完全类似,只是多了两个输入。对于相同的三个输入,计算得出的draw的值是一定相同的。

CRUSH算法最终是希望得到一个随机数,也就是这里的draw,然后拿这个随机数去乘以OSD的权重,这样把随机数和OSD的权重关联在一起,就得到了每个OSD的实际签长,而且每个签都不一样长(极大概率),就很容易从中挑一个最长的。选中一个OSD后,r=r+1 再重复执行上面三步选择第二个OSD,依次类推选出规定个数的OSD。

在前面的CRUSH Map图这棵树状结构中,每个节点都有了自己的权重,每个节点的权重由下一层节点的权重累加而得,因此根节点root的权重就是这个集群所有的OSD的权重之和,有了这么多权重之后,我们就可以使用crush算法一层层向下选择。

RULE一般分为三步走 : take-->choose N-->emit。Take这一步负责选择一个根节点,这个根节点不一定是root,也可以是任何一个Bucket。choose N做的就是按照每个Bucket的weight以及每个choose语句选出符合条件的Bucket,并且,下一个choose的选择对象为上一步得到的结果。emit就是输出最终结果,相当于出栈。

此外,CRUSH Map中的桶定义也能影响CRUSH算法的执行过程。例如,CRUSH算法还需要考虑桶中子项的权重来决定它们被选中的概率,同时,在OSDMap中的运行状态发生变化时,尽量减少数据迁移。具体的子元素选择算法是由桶定义里面的Bucket Type来确定的。

桶定义还能决定CRUSH算法在执行时所选择的哈希算法。哈希算法往往会导致选择冲突的问题。类似地,当哈希算法选择出OSD设备后,可能会发现其在OSDMap被标记为不正常的运行状态。这时,CRUSH算法需要有自己的一套机制来解决选择冲突和选择失败问题。

1)选择方式、选择个数与失效域

在step的配置中,可以定义在当前步骤下选择的Bucket Class,即失效域,以及选择的具体个数n。例如,让数据备份分布在不同的机架中,代码如下:

或者是让数据分布在两个电源下面的两个主机中,代码如下:

其中,当选择个数n的值为0时,表示选择与备份数量一致的桶;当n的值为负数时,表示选择备份数量减去n个桶;当n的值为正数时,即选择n个桶。

chooseleaf可以被看作choose的一种简写方式,它会在选择完指定的Bucket Class后继续递归直到选择出OSD设备。例如,让数据备份分布在不同的机架的规则也可以写成:

2)选择备份的策略:firstn与indep

CRUSH算法的选择策略主要和备份的方式有关。firstn对应的是以镜像的方式备份数据。镜像备份的主要特征是各数据备份之间是无顺序的,即当主要备份失效时,任何从属备份都可以转为主要备份来进行数据的读/写操作。其内部实现可以理解为CRUSH算法维护了一个基于哈希算法选择出来的设备队列,当一个设备在OSDMap中标记为失效时,该设备上的备份也会被认为失效。这个设备会被移出这个虚拟队列,后续的设备会作为替补。firstn的字面意思是选出虚拟队列中前n个设备来保存数据,这样的设计可以保证在设备失效和恢复时,能够最小化数据迁移量。firstn策略如下图所示。

而indep对应的是以纠删码的方式来备份数据的。纠删码的数据块和校验块是存在顺序的,也就是说它无法像firstn一样去替换失效设备,这将导致后续备份设备的相对位置发生变化。而且,在多个设备发生临时失效后,无法保证设备恢复后仍处于原来的位置,这就会导致不必要的数据迁移。indep通过为每个备份位置维护一个独立的(Independent)虚拟队列来解决这个问题。这样,任何设备的失效就不会影响其他设备的正常运行了;而当失效设备恢复运行时,又能保证它处于原来的位置,降低了数据迁移的成本。indep策略如下图所示。

虚拟队列是通过计算索引值来实现的。简单来讲,对于firstn策略,当第2个设备b(索引值为2)失效时,第2个镜像位置会重新指向索引值为2+1的设备,即第r个镜像位置会指向索引值为r+f的设备,其中f为前序失效设备的个数。而对于indep策略,如图7-17所示,当第2个设备b失效时,第2个镜像位置会指向索引值为2+1×6的设备h,也就是说,第r个镜像位置会指向索引值为r+f×n的设备,其中f为设备失效计数,n为总备份数。

3)选择桶的子元素的方式:Bucket Type

CRUSH算法在确定了最终选择的索引值后,并不是按照索引值从对应的桶中直接选出子桶或子设备的,而是提供了多个选择,让用户能够根据不同情况进行配置。子元素的选择算法通过Bucket Type来配置,分别有Uniform、List、Tree、Straw和Straw2 5种。它们各自有不同的特性,可以在算法复杂度、对集群设备增减的支持,以及对设备权重的支持3个维度进行权衡。

根据Bucket Type的算法实现可以将子元素的选择算法分为三大类。首先是Uniform,它假定整个集群的设备容量是均匀的,并且设备数量极少变化。它不关心子设备中配置的权重,直接通过哈希算法将数据均匀分布到集群中,所以也拥有最快的计算速度O(1),但是其适用场景极为有限。

其次是分治算法。List会逐一检查各个子元素,并根据权重确定选中对应子元素的概率,拥有O(n)的复杂度,优点是在集群规模不断增加时能够最小化数据迁移,但是在移除旧节点时会导致数据重新分布。Tree使用了二叉搜索树,让搜索到各子元素的概率与权重一致。它拥有O(logn)的算法复杂度,并且能够较好地处理集群规模的增减。但是Tree算法在Ceph实现中仍有缺陷,已经不再推荐使用。

分治算法的问题在于各子元素的选择概率是全局相关的,所以子元素的增加、删除和权重的改变都会在一定程度上影响全局的数据分布,由此带来的数据迁移量并不是最优的。第三类算法的出现即为了解决这些问题。Straw会让所有子元素独立地互相竞争,类似于抽签机制,让子元素的签长基于权重分布,并引入一定的伪随机性,这样就能解决分治算法带来的问题。Straw的算法复杂度为O(n),相对List耗时会稍长,但仍是可以被接受的,甚至成为了默认的桶类型配置。然而,Straw并没有能够完全解决最小数据迁移量问题,这是因为子元素签长的计算仍然会依赖于其他子元素的权重。Straw2的提出解决了Straw存在的问题,在计算子元素签长时不会依赖于其他子元素的状况,保证数据分。

布遵循权重分布,并在集群规模变化时拥有最佳的表现。

 

CRUSH算法在Ceph中的应用

Ceph并不是直接通过CRUSH算法将数据对象一一映射到OSD中的,这样做将非常复杂与低效。而且,Ceph用户并不需要了解实际的CRUSH算法是怎么运行的,只需要关心数据保存在哪里即可。Ceph通过两个逻辑概念,即存储池和PG,很好地将CRUSH算法的复杂性隐藏起来,并向用户提供了一个直观的对象存储接口,即RADOS。

1.存储池

Ceph中的存储池是由管理员创建的,用于隔离和保存数据的逻辑结构。不同的存储池可以关联不同的CRUSH规则,指定不同的备份数量,以镜像或纠删码的方式保存数据,或是指定不同的存取权限和压缩方式。用户还可以以存储池为单位做快照,甚至建立上层应用,比如块存储、文件存储和对象存储。至于存储池本身是如何保证数据安全性和分布的,这是由存储池关联的CRUSH规则决定的,用户不需要深入了解这些细节,只需要记住数据名称,选择合适的存储池即可在RADOS中执行读/写操作。创建一个存储池的命令如下:

2.PG

存储池在创建时需要指定PG的数量。PG是存储池中保存实际数据对象的逻辑概念。Ceph中的数据对象总是唯一地保存在某个存储池中的一个PG里,或者存储池通过维护固定数量的PG来管理和保存所有的用户数据对象。

和存储池概念不同的是,用户无法决定对象具体保存在哪个PG中,这是由Ceph自行计算的。PG的意义在于为存储空间提供了一层抽象,它在概念上对应操作系统的虚拟地址空间,将数据对象的逻辑位置和其具体保存在哪些OSD设备的细节相互隔离。这样能够实现将各存储池的虚拟空间相互隔离,还能更好地利用和维护OSD设备的物理空间。其具体实现如下图所示,Ceph首先将数据对象通过哈希算法分布到不同的逻辑PG中,再由CRUSH算法将PG合理分布到所有的OSD设备中。也就是说,CRUSH算法实际上是以PG而不是数据对象为单位确定数据分布和控制容灾域的,而在逻辑上一组PG组成了一个连续的虚拟空间来保存存储池中的数据对象。

PG概念的出现增加了Ceph整体实现的复杂度,但是它的优点也显而易见。Ceph中能够保存的数据对象的数量是非常多的,以PG为单位给数据对象分组,也以PG为单位管理大块的数据,例如执行数据的恢复、校验和故障处理等操作,其日常维护开销就不会随着数据对象数量的增加而明显增加。这就能够在设计上减少大规模数据量的计算资源开销,包括CPU、内存和网络资源。

虽然更少的PG数目能够降低资源开销,提高处理性能,但是过少的PG数目容易导致数据分布不均匀,以及更差的容灾性能。所以如何配置存储池的PG数目,对整个Ceph集群意义重大。一般来说,每个OSD上维护50~100个PG时,集群能够有更平衡的表现。但是在复杂的存储池配置下往往难以计算具体的值,好在Ceph社区提供了pgcalc工具来协助配置所有的PG数目。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐