Ceph CRUSH 算法

1 简介

   大规模分布式存储集群中会有数千台机器并且在不断增长中,将数据均匀的分布在所有硬盘中,一个简单的策略就是将数据写到最空的盘中,但这样通常会造成新盘读写瓶颈(因为新数据一般也是热数据),同时需要一个中心节点存储所有文件的元数据(GFS),中心节点在随着系统容量增长会逐渐成为系统瓶颈
  为了实现更合理的资源利用,可以利用哈希算法的特性,将数据块随机均匀的分布到整个集群中.这样解决上一段中提出的问题.但同时新的问题出现,但系统扩容或设备失效时,旧的数据会全部失效,整个集群会处于不可用状态.这是不可接受的
  一致性 hash算法解决了扩容/宕机带来的数据迁移问题,并且总体性能已经很优秀了.但还有更深入的问题,比如由于数据是均匀的分布在整个集群中,当任意节点失效时都会影响到所有用户数据的完整性.并且一致性 hash 算法完全不可预测, 所以两地三中心的数据分布期望也完全不能实现.

  
  Ceph 对一个分布式存储系统提出了很多要求,所以它的数据分布算法至少要满足以下情况:

  • 实现数据的随机分布, 并在读取时能快速索引
  • 能够高效地重新分布数据,在设备加入、删除和失效时最小化数据迁移
  • 更够根据设备的物理位置合理地控制数据的失效域
  • 支持常见的镜像、磁盘阵列、纠删码等数据安全机制
  • 支持不同存储设备的权重分配,来表示其容量大小或性能

  
  CRUSH 算法在设计时就考虑了这些情况,如下图所示,CRUSH 算法根据输入 x 得到随机的 n 个有序的位置,并保证在相同的元数据下,对于输入的 x 的输出总是相同的.而所有节点只要获取到相同的元数据就可以计算出相同的结果,所以是去中心化的,可以完全利用到每个存储节点自身的计算能力.

  
  CRUSH 元数据包含了:

  • CURSH Map 集群中所有设备或 OSD 存储节点的位置信息和权重信息
  • OSD Map 各个 OSD 的运行时状态, 让 CRUSH 感知存储节点的失效、删除和加入情况.
  • CRUSH Rule 由用户定义,保证算法选择的位置能够合理分布在不同的失效域中.

2 映射过程

2.1 概念

  数据映射的方式决定了存储系统的性能和扩展性(Pool, PG) -> OSD set 的映射由 4 个因素决定:

  1. CRUSH 算法
  2. OSD Map: 在运行时期,Ceph的Monitor会在OSD Map中维护一种所有OSD设备的运行状态,并在集群内同步。其中,OSD运行状态的更新是通过OSD-OSD和OSD-Monitor的心跳完成的。任何集群状态的变化都会导致Monitor中维护的OSDMap版本号(Epoch)递增,这样Ceph客户端和OSD服务就可以通过比较版本号大小来判断自己的Map是否已经过时,并及时进行更新。
  3. CRUSH Map: CRUSH Map 是一种有向无环图(DAG),所有叶子节点是 OSD, 用来描述 OSD的物理组织和层次结构.默认会构建根节点和主机两种桶,根据需求可以自行创建复杂的桶-即 Bucket Class,是一种逻辑的组织策略,例如不同机架隶属于不同的 Bucket Class - RACK 下.
  4. CRUSH Rules: 数据映射的策略。这些策略可以灵活的设置object存放的区域。比如可以指定 pool1中所有objects放置在机架1上,所有objects的第1个副本放置在机架1上的服务器A上,第2个副本分布在机架1上的服务器B上。 pool2中所有的object分布在机架2、3、4上,所有Object的第1个副本分布在机架2的服务器上,第2个副本分布在机架3的服器上,第3个副本分布在机架4的服务器上。

2.2 流程

  1. 创建 Pool 和 它的 PG. Pool 创建 PG 后就被 Monitor 根据 CRUSH 算法映射到 OSD 上.PG 是逻辑概念,不会随着 OSD 的增加/减少而变化,Object 到 PG 的映射是稳定的
  2. Ceph Client 通过 hash 算法计算出存放 Object 的 PG 的 ID:
    a. 客户端输入 Pool ID 和 Object ID (例如 pool = "pool1" and object-id = "test1")
    b. hash(object-id) & mask -> pgid (例如 58) ( pg num = 2n, mask = 2n - 1, hash保证数据均匀分布,mask 保证不溢出)
    c. 对 Pool ID 取 hash (例如 4)
    d. Ceph 将两个结果组合起来(例如 4.58) 得到 PG 的完整 ID
    e. 即 PG-id = hash(pool-id).hash(object-id)%PG-number
  3. 客户端通过 CRUSH 算法得到 Object 应该被保存到哪个 PG 中的哪个 OSD 上(这个关系是已经确定的)


  

3 CRUSH 算法

  给定一个输入 x,CRUSH 算法将输出一个确定有序的存储目标向量 R.当输入 x,CRUSH 利用强大的多重整数 hash 函数根据集群 map、定位规则、以及 x 计算出独立的完全确定可靠的映射关系. CRUSH算法通过每个设备的权重来计算数据对象的分布。对象分布是由cluster map和data distribution policy决定的。cluster map描述了可用存储资源和层级结构(比如有多少个机架,每个机架上有多少个服务器,每个服务器上有多少个磁盘)。data distribution policy由 placement rules组成。rule决定了每个数据对象有多少个副本,这些副本存储的限制条件(比如3个副本放在不同的机架中)。

  
  CRUSH Rule 通过用户定义的规则来指导 CRUSH 算法的具体执行.其场景主要如下所示:

  • 数据备份的数量: 规则需要指定能够支持的备份数量
  • 数据备份的策略: 纠删码的各个分片之间具有顺序,所以 CRUSh 算法需要了解各个关联的副本之间是否存在顺序性
  • 选择存储设备的类型: 规则需要能够选择不同的存储设备类型来满足不同的需求
  • 确定失效域: 为了保证整个存储集群的可靠性,规则需要根据 CRUSH Map 中的设备组织结构选择不同的失效域,并依次执行 CRUSH 算法.

  
  Ceph 默认的规则只能保证集群数据备份在不同的主机中.实际情况往往更加精细和复杂,这就需要用户根据失效域自行配置规则,保存在 CRUSH Map 中,

//CRUSH Rule 的定义

rule <规则名称> {
  ruleset <唯一的规则 ID>
  type <备份策略: replicated/erasure>
  min_size <规则支持的最少备份数量>
  max_size <规则支持的最多备份数量>
  
  //选择设备范围, 确定失效域
  step take ...
  step choose ...
  ......
  step emit
}

  
  

3.1 CRUSH Rule 的 step take 与 step emit

  • step take: CRUSH Rule 执行步骤中的第一步.通过桶名称(对应 CRUSH Map 中的一个子树)来确定规则的选择范围,同时也可以选择 Device Class 来确定所选择的设备类型.可以认为是将集群中不符合OSD 剔除.
  • step emit: 表示步骤结束,输出选择的位置

3.2 step choose 与 CRUSH 算法原理

  step choose 对应 CRUSH 算法的核心实现.每一 step choose 需要确定一个对应的失效域,以及在当前失效域中选择子项的个数.由于数据备份策略的不同(镜像/纠删码),step choose 还需要确定选择出来的备份位置的排序策略.其定义如下:

step  <选择方式: choose/chooseleaf>
        <选择备份的策略: firstn/indep>
        <选择个数: n>
        type  <失效域所对应的 Bucket Class>

   此外 CRUSH Map 中桶定义也能影响 CRUSH 算法的执行过程.例如,CRUSH 算法需要考虑桶中子项的权重来决定它们被选中的概率,同时,在 OSD Map 中的运行状态发生变化时,尽量减少数据迁移.具体的资源数选择算法是由桶定义里面的 Bucket Type 来确定的.
   桶定义还能决定 CRUSH 算法在执行时所选择的 hash 算法.并且当 hash 算法选择到不正常的 OSD 时,CRUSH 算法有自己的一套机制来解决选择冲突和选择失败问题.

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

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

step take root
step chooseleaf firstn/indep 0 type rack
step emit

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

step take root
step choose firstn/indep 2 type power
step chooseleaf firstn/indep 2 type host
step emit

n 的值不同有特殊的含义:

  • n 等于 0: 表示选择与备份数量一致的桶
  • n 小于 0: 表示选择备份数量减去 n 个桶
  • n 大于 0: 表示选择 n 个桶

  chooseleaf 可以被看做 Choose 的一种简写成分, 他会在选择完指定的 Bucket Class 后继续递归知道选择出 OSD 设备.例如,让数据分布在不同的机架中也可以写成:

step take root
step choose firstn/indep 0 type rack
step choose firstn/indep 1 type osd
step emit
  1. 选择备份的策略 1 : firstn
       firstn 对应以镜像的方式选择备份副本的选择备份策略.镜像备份无需关注副本间的顺序,副本之间的地位是平等的.其内部实现可以理解为 CRUSH 算法维护了一个基于 hash 算法选择出来的设备队列,当一个设备在 OSD Map 中标记为失效时,该设备上的备份也会被认为失效.这个设备会被移除这个虚拟的设备队列,后续的设备会作为替补.firstn 的字面意思是选出虚拟队列中前 n 个设备来保存数据,这样的设计可以保证在设备失效和恢复时,能够最小化数据迁移量.

  1. 选择备份的策略 2: indep

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

  
  虚拟队列是通过计算索引值来实现的.对于 firstn 策略,第 2 个设备失效时,第 2 个镜像会重新指向索引值为 2+1 的设备,即第 n 个镜像会指向索引值为 n+f 的设备,其中f 为失效的设备个数.
   对于 indep 策略,当第 2 个设备失效,第 2 个镜像会指向第 2 + 1 * 6 的设备 h,也就是说,第 n 个镜像位置会指向 n + f * m,f 为设备失效计数, m 为总备份数,这样就使得总体顺序是有效的.即 n = ((i - 1) % m) + 1 , i 为 当前设备索引.

  1. 选择桶的子元素的方式: Bucket Type
      在上一步我们确定了选择的索引值,但CRUSH 算法并不是按照索引值从对应的同种直接选出子桶或子设备的,而是提供了多个选择,让用户能够根据不同情况进行配置.子元素的选择算法通过 Bucket Type 来配置,分别由 Uniform、List、Tree、Straw 和 Straw2 5种方式.

   根据算法实现可以将子元素选择算法分为三大类.

  首先是Uniform,它假定整个集群的设备容量是均匀的,并且设备数量极少变化.他不关心子设备中配置的权重,直接通过哈希算法将数据均匀的分布到集群中,时间复杂度 O(1),优点是计算速度快,缺点是适用范围有限.

  其次是分治算法.List 会逐一检查各个元素,并根据权重确定选中对应子元素的概率,时间复杂度 O(n),优点是在集群规模不断增加时能最小化数据迁移,缺点是移除旧节点时会导致数据重新分配.Tree使用了二叉搜索树,让搜索到各子元素的概率与权重一致.时间复杂度 O(logn),优点是较好适应集群规模的增减,缺点是 Ceph 实现有缺陷,不推荐使用.分治算法的问题在于各子元素的选择概率是全局相关的,所以子元素的增加、删除和权重的改变都会在一定程度上影响全局的数据分布,由此带来的数据迁移量并不是最优的。

  第三类算法解决了上面提出的问题.Straw会让所有子元素独立的互相竞争,类似于抽签机制,让子元素的签长基于权重分布,并引入一定的伪随机性.时间服复杂度为 O(n),是默认的桶类型.Straw 并没有能够完全解决最小数据迁移量问题.这是因为子元素签长的计算仍然会依赖于其他子元素的权重.Straw2的提出解决了 Straw 存在的问题,在计算子元素签长时不会依赖于其他子元素的状况,保证数据分布遵循权重分布,并且在集群规模变化时拥有最佳的表现.

3.3 CRUSH 算法流程总结

  将设计目标映射到整个流程可以让我们更加轻松的理解整个 CRUSH 算法.

  • 为了去除中心节点,使得集群拥有无限增长的能力,所以 CRUSH 算法拥有:
    • 一个固定的类似 Hash 的分配逻辑即相同输入的输出总是相同的.在 CRUSH Rule 的 step choose 和 Bucket type 都使用了 hash 算法,所以不论在那个节点计算,算出的结果总是相同的,即总能找到正确的数据写入/读取位置.
  • 为了确定失效域,即实现类似两地三中心的策略.所以:
    • 建立 CRUSH Map 的物理结构拓扑图,然后再在 step choose 中加以限制的选择,例如可以限制在 A 数据中心选择 1 个,在 B 数据中心选择 2 个
  • 为了实现设备不同权重,所以
    • 在 Bucket Type 中实现了 Straw和 Straw2.简单描述,使用 权重的立方乘以f(wi)由输入x,副本数目r,以及 bucket 项 i计算的 hash 值得到的数字最大的 bucket 被选中,因为 hash 值是随机的,并且所有 bucket hash 值的期望是相同的,所以这时权重越大选中的概率越大.

C(x, r) = Max(f(wi) * Hash(x, r, i))

总结

  就我的理解 CRUSH 算法在思想上和一致性 hash 是相同的,大部分差异都是基于工程现实做的取舍.所以若难以理解 CRUSH 的话,可以先去深入理解一下一致性 hash.然后很多 CRUSH 算法分析博客都写得比较复杂和繁长,还是需要多看几篇文章总结提炼出属于自己的东西.

  限于本文的作者水平,文中的错误在所难免,恳请大家批评指正.

引用

陈小跑: Ceph源码解析:CRUSH算法

英特尔亚太研发有限公司: Linux开源存储全栈详解:从Ceph到容器存储

posted @ 2022-03-24 17:05  哪吒young  阅读(1169)  评论(0)    收藏  举报