引言
那么问题来了,把一份数据存到一群 Server 中分几步?
Ceph 的答案是:两步。
- 计算 PG
- 计算 OSD
计算 PG
首先,要明确 Ceph 的一个规定:在 Ceph 中,一切皆对象。
不论是视频,文本,照片等一切格式的数据,Ceph 统一将其看作是对象,因为追其根源,所有的数据都是二进制数据保存于磁盘上,所以每一份二进制数据都看成一个对象,不以它们的格式来区分他们。
那么用什么来区分两个对象呢?对象名。也就是说,每个不同的对象都有不一样的对象名。于是,开篇的问题就变成了:
把一个对象存到一群 Server 中分几步?
这里的一群 Server,由 Ceph 组织成一个集群,这个集群由若干的磁盘组成,也就是由若干的 OSD 组成。于是,继续简化问题:
把一个对象存到一堆 OSD 中分几步?
Ceph 中的逻辑层
Ceph 为了保存一个对象,对上构建了一个逻辑层,也就是池(pool),用于保存对象,这个池的翻译很好的解释了 pool 的特征,如果把 pool 比喻成一个中国象棋棋盘,那么保存一个对象的过程就类似于把一粒芝麻放置到棋盘上。
Pool 再一次进行了细分,即将一个 pool 划分为若干的 PG (归置组 Placement Group),这类似于棋盘上的方格,所有的方格构成了整个棋盘,也就是说所有的 PG 构成了一个 pool 。
现在需要解决的问题是,对象怎么知道要保存到哪个 PG 上,假定这里我们的 pool 名叫 rbd,共有 256 个 PG,给每个 PG 编个号分别叫做0x0, 0x1, ...0xF, 0x10, 0x11... 0xFE, 0xFF。
要解决这个问题,我们先看看我们拥有什么?1、不同的对象名。2、不同的 PG 编号。这里就可以引入 Ceph 的计算方法了 : HASH。
对于对象名分别为bar和foo的两个对象,对他们的对象名进行计算即:
- HASH('bar') = 0x3E0A4162
- HASH('foo') = 0x7FE391A0
- HASH('bar') = 0x3E0A4162
对对象名进行 HASH 后,得到了一串十六进制输出值,也就是说通过 HASH 我们将一个对象名转化成了一串数字,那么上面的第一行和第三行是一样的有什么意义? 意义就是对于一个同样的对象名,计算出来的结果永远都是一样的,但是HASH算法的确将对象名计算得出了一个随机数。
有了这个输出,我们使用小学就会的方法:求余数!用随机数除以PG的总数256,得到的余数一定会落在[0x0, 0xFF]之间,也就是这256个PG中的某一个:
- 0x3E0A4162 % 0xFF ===> 0x62
- 0x7FE391A0 % 0xFF ===> 0xA0
于是乎,对象bar保存到编号为0x62的PG中,对象foo保存到编号为0xA0的PG中。对象bar永远都会保存到PG 0x62中! 对象foo永远都会保存到PG 0xA0中!
现在又来了一亿个对象,他们也想知道自己会保存到哪个PG中,Ceph说:“自己算”。于是这一亿个对象,各自对自己对象名进行HASH,得到输出后除以PG总数得到的余数就是要保存的PG。
求余的好处就是对象数量规模越大,每个PG分布的对象数量就越平均。
所以每个对象自有名字开始,他们要保存到的PG就已经确定了。
那么爱思考的小明同学就会提出一个问题,难道不管对象的高矮胖瘦都是一样的使用这种方法计算PG吗,答案是,YES! 也就是说Ceph不区分对象的真实大小内容以及任何形式的格式,只认对象名。毕竟当对象数达到百万级时,对象的分布从宏观上来看还是平均的。
这里给出更Ceph一点的说明,实际上在Ceph中,存在着多个pool,每个pool里面存在着若干的PG,如果两个pool里面的PG编号相同,Ceph怎么区分呢? 于是乎,Ceph对每个pool进行了编号,比如刚刚的rbd池,给予编号0,再建一个pool就给予编号1,那么在Ceph里,PG的实际编号是由pool_id+.+PG_id组成的,也就是说,刚刚的bar对象会保存在0.62这个PG里,foo这个对象会保存在0.A0这个PG里。其他池里的PG名称可能为1.12f, 2.aa1,10.aa1等。
Ceph中的物理层
理解了刚刚的逻辑层,我们再看一下Ceph里的物理层,对下,也就是我们若干的服务器上的磁盘,通常,Ceph将一个磁盘看作一个OSD(实际上,OSD是管理一个磁盘的程序),于是物理层由若干的OSD组成,我们的最终目标是将对象保存到磁盘上,在逻辑层里,对象是保存到PG里面的,那么现在的任务就是打通PG和OSD之间的隧道。PG相当于一堆余数相同的对象的组合,PG把这一部分对象打了个包,现在我们需要把很多的包平均的安放在各个OSD上,这就是CRUSH算法所要做的事情:CRUSH计算PG->OSD的映射关系。
加上刚刚的对象映射到PG的方法,我们将开篇的两步表示成如下的两个计算公式:
- 池ID + HASH('对象名') % pg_num ===> PG_ID
- CRUSH(PG_ID) ===> OSD
使用HASH代替CRUSH?
在讨论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算法。




