stable_mod与crush 算法

Ceph Stable_mod 与 CRUSH算法心得

最近在研究Ceph 关于 pg 和 osd 的一些算法。在参考了徐小胖大神的文章后 link. 以及《Ceph之RADOS设计原理与实现》后得到一些总结,本文部分采用了《大话Ceph–CRUSH那点事儿》的文章架构及内容,再此基础上进行了一定的补充。

引言

那么问题来了,把一份数据存到集群中分几步?

Ceph的答案是:两步。

  1. 计算PG
  2. 计算OSD

首先,要明确Ceph的一个规定:在Ceph中,一切皆对象。

不论是视频,文本,照片等一切格式的数据,Ceph统一将其看作是对象,因为追其根源,所有的数据都是二进制数据保存于磁盘上,所以每一份二进制数据都看成一个对象,不以它们的格式来区分他们。

那么用什么来区分两个对象呢?对象名。也就是说,每个不同的对象都有不一样的对象名。于是,开篇的问题就变成了:

把一个对象存到集群中分几步?

Ceph组织成一个集群,这个集群由若干的磁盘组成,也就是由若干的OSD组成。于是,继续简化问题:

把一个对象存到一堆OSD中分几步?

计算PG

​ 假设一个存储池有100个pg。那么pg在存储池中编号为0、1、2、3…99

​ 首先有Ceph客户端负责生成一个字符串形式的对象名,然后基于对象名(对象名+命名空间)计算得到一个32位的哈希值。

​ 我们很容易想到拿PG数取模的方式 :

​ Hash(Object)mod pg_num

​ 这样Object必然计算出一个[0,99]之间的PG编号。那么计算PG采用 mod 算法可行吗?

​ 当集群规模增大,pg数量必然增加。也就是pg_num已经发生了变化, Hash(Object)mod pg_num 的值将会完全不同,那么几乎所有的对象都会在pg中进行大量的无规则转移。这将带来长时间的业务停顿已经性能的消耗。显然,为了避免造成长时间的业务停顿,我们应该尽可能的减少对象移动。

解决思路:如果将PG看成一个集合,那么容易理解我们总是期望集合中的元素有尽可能多的共同只处,以便为之指定一些通用的运算规则。

x mod 2^n = x&(2^n-1)

​ 我们对pg数量总是设定为2^n

​ 假定存储池中的某个对象,经过计算后32位哈希值为0x4979FA12,并且该存储池的pg_num为256 ,则由:

​ 0x4979FA12 mod 256 = 18

​ 并且我们很容易验证该存储池内哈希值如下的其他对象,通过模运算同意会被映射至PGID为18的PG之上。

​ 0x4979FB12 mod 256 =18

​ 0x4979FC12 mod 256 =18

​ 0x4979FD12 mod 256 =18

​ …

​ 可以发现被映射到18的Hash(Object)的后8位是相同的。

​ 可见我们仅仅使用了这个“全精度“32位 哈希值的后8位。因此,如果pg_num可以写成2n的形式(例如这里的256可以写成28,即是2的整数次幂),则每个对象执行PG映射时,其32位哈希值中仅有低8位是有意义的。所以归属于同一个PG的对象,其32位哈希值中低n位都是相同的。基于此,我们将2^n-1 成为pg_num的掩码,其中n位掩码的位数。

​ 但是现实总不会如你愿,假设pg_num不是2的整数次幂,就像上面举例pg=100,即无法写成2^n的形式 。例如,假定pg_num=12,此时n=4,容易验证如下输入经过模运算后结果一致,但是它们只有低2位是相同的。

可能有人会问这个n是怎么来的,其实n就是PG数二进制表示中1最高位的1。ceph中如果设置PG数目为a,那么程序会得到一个最小的n,使 a <= 2^n,例如:(a=12, n=4), (a=16, n=4), (a=17, n=5),以此类推。

​ 0x00 mod 12 = 0

​ 0x0C mod 12 = 0

​ 0x18 mod 12 = 0

​ 0x24 mod 12 = 0

​ 因此需要针对普通的取模操作加以改进,以保证针对不同输入,当计算结果相等时,维持”输入具有尽可能多的、相同的低位“这个相对有规律的特性。

引入stable_mod算法

​ 一种改进的方案是使用掩码的方式来代替取模操作。例如PG数目对应的最高比特位为n,则其掩码为2^n -1, 将某个对象映射到PG时,直接使用 object & (2^n -1)即可。这种直接与的方式其实就是取object二进制的后 n 位作为pg number,这种方式不仅高效,而且运算速度快。但是 这种方案存在一个潜在的问题,如果运维人员设置的PG数是2的幂次方的话,那么这种方案比较完美。如果PG数目不是2的幂次方的话这种映射会产生空穴,即将某些对象映射到一些实际上不存在的PG上。如下图所示:

img

​ 上图中如果如果PG数目为12,但是n=4,直接hash的话会产生 0~15 共计16中不同的结果,但是12 ~ 15这4个PG是不存在的,为了解决这个问题ceph做了降级方案。因为 n 为PG数的最高位,因此可以得到如下的方程式。

2^(n-1) < PG数 <= 2^n

​ 由此可得 [0,2^(n-1)]内PG一定是存在的,可以通过 object & 2^(n-1) 来将那些实际上不存在的PG重新映射到这个小的区间内,如下图所示:

img

​ 如上图所示,ceph会将映射出不存在的PG重新映射到前面存在的区间中。这样,退而求其次,如果pg_num不是2的整数次幂,我们只能保证相对每个PG而言,映射至该PG的所有对象,其哈希值低N-1位都是相同的。

​ 所以如果ceph集群设置的PG数目不是2的幂次方的话,可能会造成中间某一段的PG所承载的数据高于其它PG,例如:PG数为12时会造成4,5,6,7四个PG所承载的对象数是其它PG的两倍,所以集群在稳定时PG数最好的2的幂次方。

改进后的映射方法称为stable_mod,其逻辑代码如下:

/* 
x: 对象key值
b: pg 数
bmask: 掩码
*/
static inline int ceph_stable_mod(int x, int b, int bmask)
{
    if ((x & bmask) < b)
        return x & bmask;
    else
        return x & (bmask >> 1);
}

​ 任然已pg_num等于12为例,可以验证:如果采用stable_mod方式,在保证结果相等的前提下,如下输入的低n-1=3位都是相同的。

​ 仍然以pg_num=12为例,可以验证:如果采用stable_mod方式,在保证结果相等的前提下,如下输入的低n-1=3位都有相同的。

​ 0x05 stable_mod 12 = 5

​ 0x0D stable_mod 12 =5

​ 0x15 stable_mod 12 =5

​ 0x1D stable_mod 12 =5

综上,无论pg_num是否为2的整数次幂,采用stable_mod都可以产生一个相对有规律的对象到PG的映射结果,这是PG分裂的一个重要理论。

​ 好,当我们采用了stable_mod算法来计算pg,我们再来考虑pg数量增加这个问题。假定某个存储池老的pg_num为24,新的pg_num为26。

​ 之前我们已经验证了pg内所以对象的哈希值后四位都相同(假定其PGID=Y.X,其中X=0bX3X2X1X0)为例,容易验证其中所有对象的哈希值都可以分布如图1-6所示的4中类型(按前面的分析,分裂前后,对象哈希值中只有低6位有效,图中只展示这6位)
在这里插入图片描述

​ 依次针对这4种类型的对象PG(Y.X)使用新的pg_num(2^4 -> 2^6)再次执行stable_mod,结果如表1-2所示
在这里插入图片描述

​ 为此,我们需要创建3个新的PG来分别转移部分来自老PG中的对象,参考表1-2,容易验证这3个新的PG在存储池中的编号可以通过(m*16)+X (m=1,2,3)得到,同时由于概率上每种类型的对象数量相等,因此完成对象转移后每个PG承载的数据任然可以保持均衡。

​ 针对所有老的PG重复上诉过程,最终可以将存储池中的PG数量调整为原来的4倍。又因为在调整PG数量的过程中,我们综上基于老PG(称为祖先PG)产生新的孩子PG,并且新的孩子PG中最初的对象全部来源于老PG,所有这个过程形象的称为PG分裂。

计算OSD

在讨论CRUSH算法之前,我们来做一点思考,可以发现,上面两个计算公式有点类似,为何我们不把

  • CRUSH(PG_ID) ===> OSD
    改为
  • HASH(PG_ID) %OSD_num ===> OSD

我可以如下几个由此假设带来的副作用:

  • 如果挂掉一个OSD,OSD_num-1,于是所有的PG % OSD_num的余数都会变化,也就是说这个PG保存的磁盘发生了变化,对这最简单的解释就是,这个PG上的数据要从一个磁盘全部迁移到另一个磁盘上去,一个优秀的存储架构应当在磁盘损坏时使得数据迁移量降到最低,CRUSH可以做到。
  • 如果保存多个副本,我们希望得到多个OSD结果的输出,HASH只能获得一个,但是CRUSH可以获得任意多个。
  • 如果增加OSD的数量,OSD_num增大了,同样会导致PG在OSD之间的胡乱迁移,但是CRUSH可以保证数据向新增机器均匀的扩散。

所以HASH只适用于一对一的映射关系计算,并且两个映射组合(对象名和PG总数)不能变化,因此这里的假设不适用于PG->OSD的映射计算。因此,这里开始引入CRUSH算法。

引入CRUSH算法

千呼万唤始出来,终于开始讲CRUSH算法了,如果直接讲Sage的博士论文或者crush.c的代码的话,可能会十分苦涩难懂,所以我决定尝试大话一把CRUSH,希望能让没有接触过CRUSH的同学也能对其有所理解。

首先来看我们要做什么:

  • 把已有的PG_ID映射到OSD上,有了映射关系就可以把一个PG保存到一个磁盘上。
  • 如果我们想保存三个副本,可以把一个PG映射到三个不同的OSD上,这三个OSD上保存着一模一样的PG内容。

再来看我们有了什么:

  • 互不相同的PG_ID。
  • 如果给OSD也编个号,那么就有了互不相同的OSD_ID。
  • 每个OSD最大的不同的就是它们的容量,即4T还是800G的容量,我们将每个OSD的容量又称为OSD的权重(weight),规定4T权重为4,800G为0.8,也就是以T为单位的值。

现在问题转化为:如何将PG_ID映射到有各自权重的OSD上。这里我直接使用CRUSH里面采取的Straw算法,翻译过来就是抽签,说白了就是挑个最长的签,这里的签指的是OSD的权重。

那么问题就来了,总不至于每次都挑容量最大的OSD吧,这不分分钟都把数据存满那个最大的 OSD了吗?是的,所以在挑之前先把这些OSD搓一搓,这里直接介绍CRUSH的方法,如下图(可忽视代码直接看文字):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y9JCE2Lc-1615883775067)(http://www.xuxiaopang.com/images/1478244507156.png)]

  • CRUSH_HASH( PG_ID, OSD_ID, r ) ===> draw
  • ( draw &0xffff ) * osd_weight ===> osd_straw
  • pick up high_osd_straw

第一行,我们姑且把r当做一个常数,第一行实际上就做了搓一搓的事情:将PG_ID, OSD_ID和r一起当做CRUSH_HASH的输入,求出一个十六进制输出,这和HASH(对象名)完全类似,只是多了两个输入。所以需要强调的是,对于相同的三个输入,计算得出的draw的值是一定相同的。

这个draw到底有啥用?其实,CRUSH希望得到一个随机数,也就是这里的draw,然后拿这个随机数去乘以OSD的权重,这样把随机数和OSD的权重搓在一起,就得到了每个OSD的实际签长,而且每个签都不一样长(极大概率),就很容易从中挑一个最长的。

说白了,CRUSH希望随机挑一个OSD出来,但是还要满足权重越大的OSD被挑中的概率越大,为了达到随机的目的,它在挑之前让每个OSD都拿着自己的权重乘以一个随机数,再取乘积最大的那个。那么这里我们再定个小目标:挑个一亿次!从宏观来看,同样是乘以一个随机数,在样本容量足够大之后,这个随机数对挑中的结果不再有影响,起决定性影响的是OSD的权重,也就是说,OSD的权重越大,宏观来看被挑中的概率越大。

这里再说明下CRUSH造出来的随机数draw,前文可知,对于常量输入,一定会得到一样的输出,所以这并不是真正的随机,所以说,CRUSH是一个伪随机算法。下图是CRUSH_HASH的代码段,我喜欢叫它搅拌搅拌再搅拌得出一个随机数:

Alt text

如果看到这里你已经被搅晕了,那让我再简单梳理下PG选择一个OSD时做的事情:

  • 给出一个PG_ID,作为CRUSH_HASH的输入。
  • CRUSH_HASH(PG_ID, OSD_ID, r) 得出一个随机数(重点是随机数,不是HASH)。
  • 对于所有的OSD用他们的权重乘以每个OSD_ID对应的随机数,得到乘积。
  • 选出乘积最大的OSD。
  • 这个PG就会保存到这个OSD上。

现在趁热打铁,解决一个PG映射到多个OSD的问题,还记得那个常量r吗?我们把r+1,再求一遍随机数,再去乘以每个OSD的权重,再去选出乘积最大的OSD,如果和之前的OSD编号不一样,那么就选中它,如果和之前的OSD编号一样的话,那么再把r+2,再次选一次,直到选出我们需要的三个不一样编号的OSD为止!

当然实际选择过程还要稍微复杂一点,我这里只是用最简单的方法来解释CRUSH在选择OSD的时候所做的事情。

下面我们来举个例子,假定我们有6个OSD,需要从中选出三个副本:

osd_idweightCRUSH_HASH(CRUSH_HASH & 0xffff)* weight
osd.040xC35E90CB0x2432C
osd.140xA67DE6800x39A00
osd.240xF9B1B2240x2C890
osd.340x424544700x111C0
osd.440xE950E2F90x38BE4
osd.540x8A8445380x114E0

这是r = 0的情况,这时候,我们选出(CRUSH_HASH & 0xFFFF) * weight的值最大的一个,也就是osd.10x39A00,这就是我们选出的第一个OSD。
然后,我们再让r = 1,再生成一组CRUSH_HASH的随机值,乘以OSD的weight,再取一个最大的得到第二个OSD,依此得到第三个OSD,如果在此过程中,选中了相同的OSD,那么将r再加一,生成一组随机值,再选一次,直到选中三个OSD为止。

​ 看似完美,但引入PG分裂及之后,如果仍然直接使用PGID作为CRUSH输入,据此计算新增的孩子PG在OSD之间的映射结果,由于此时每个PG的PGID都不相同,必然触发大量新增孩子PG在OSD之间的迁移。考虑到分裂之前PG与其保持相同的副本分布,这样当分裂完成之后,整个集群的PG分布任然是均衡的。为此,每个存储池除了记录当前的PG数量之外看,为了应对PG分裂,还需要额外记录分裂之前祖先PG的数量,后者称为PGP 。

​ 最终,利用CRUSH执行PG映射时,我们不是直接使用PGID,而是转而使用其在存储池内的唯一编号先针对pgp_num执行stable_mod,再与存储池标识一起哈希之后作为CRUSH的特征输入,即可保证每个孩子PG与其祖先产生相同的CRUSH计算结果。(因为此时祖先和孩子PG产生的CRUSH特征输入都相同),进而保证两者产生相同的副本分布。

  • CRUSH_HASH( PG_ID stable_mod pgp_num, OSD_ID, r ) ===> draw
  • ( draw &0xffff ) * osd_weight ===> osd_straw
  • pick up high_osd_straw

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值