Ceph分布式存储实践应用之深刻Ceph实现原理

1. Crush算法与做用

CRUSH算法,全称Controlled Replication Under Scalable Hashing (可扩展哈希下的受控复制),它是一个可控的、可扩展的、分布式的副本数据放置算法, 经过CRUSH算法来计算数据存储位置来肯定如何存储和检索数据。算法

  • 保障数据分布的均衡性后端

    让数据可以均匀的分不到各个节点上面,同时让数据访问的读写操做在各个节点和磁盘上保持负载均衡。缓存

  • 集群的灵活伸缩性服务器

    能够方便快速的增长或删除节点,对失效的节点自动检测处理,可以自动实现数据的均衡,而且尽量少的迁移改动数据。网络

  • 支持更大规模的集群架构

    可以作到数据分布算法维护较小的元数据与计算量, 随着集群规模的不断增长,保持较小的数据分布式算法开销,不能呈线性增加。负载均衡

2. Crush算法说明

PG到OSD的映射的过程算法称为CRUSH 算法,它是一个伪随机的过程,能够从全部的OSD中,随机性选择一个OSD集合,可是同一个PG每次随机选择的结果是不变的,实质上映射的OSD集合是固定的。框架

CRUSH使Ceph客户机可以直接与OSDs通讯,而不是经过集中的服务器或代理。经过算法肯定的数据存储和检索方法,从而避免了单点故障、性能瓶颈和对其可伸缩性的物理限制。异步

Crush Map将系统的全部硬件资源描述成一个树状结构,而后再基于这个结构按照必定的容错规则生成一个逻辑上的树形结构,树的末级叶子节点device也就是OSD,其余节点称为bucket节点,根据物理结构抽象的虚拟节点,包含数据中心抽象、机房抽象、机架抽象、主机抽象。分布式

file

3. Crush算法原理

  1. Ceph的存储结构

    Ceph为了保存对象,会先构建一个池(pool),把pool能够比喻成一个仓库,一个新对象的保存就相似于把一个包裹放到仓库里面。

    为了更好的提高效率,Pool能够划分为若干的PG(Placement Group)归置组,这相似于仓库里面有不一样的储物架,全部的储物架组成了一个完整的仓库,全部的PG也就构建成了一个pool。

  2. PG的分配存储

    对象是如何保存至哪一个PG上?假设Pool名称为rbd,共有256个PG,每一个PG编个号分别叫作0x0, 0x1, 0x2,... 0xFF。 具体该如何分配?这里能够采用Hash方式计算。

    假设有两个对象名, 分别为bar和foo的,根据对象名作Hash计算:

    HASH(‘bar’) = 0x3E0A4162

    HASH(‘foo’) = 0x7FE391A0

    经过Hash获得一串随机的十六进制的值, 对于一样的对象名,计算出的结果可以永远保持一致,但咱们预分配的是256个PG,这就须要再进行取模处理, 所得的结果会落在【0x0,0xFF】区间:

    0x3E0A4162 % 0xFF ===> 0x62

    0x7FE391A0 % 0xFF ===> 0xA0

    实际在Ceph中, 存在不少个Pool,每一个Pool里面存在若干个PG,若是两个Pool里面的PG编号相同,该如何标识区分?Ceph会对每一个pool再进行编号,好比上面名称为rbd的Pool,给定ID编号为0, 再新增一个Pool,ID编号设定为1,那么一个PG的实际编号是由pool_id + . + pg_id组成。由此推论, 刚才的bar对象会保存在编号为0的pool,pg编号为62里面。

  3. OSD的分配存储

    Ceph的物理层,对应的是服务器上的磁盘,Ceph将一个磁盘或分区做为OSD,在逻辑层面,对象是保存至PG内,如今须要打通PG与OSD之间的联系, Ceph当中会存在较多的PG数量,如何将PG平均分布各个OSD上面,这就是Crush算法主要作的事情: 计算PG -> OSD的映射关系。

    上述所知, 主要两个计算步骤:

    POOL_ID(对象池) + HASH(‘对象名称’) % pg_num(归置组)==> PG_ID (完整的归置组编号)

    CRUSH(PG_ID)==> OSD (对象存储设备位置)

  4. 为何须要采用Crush算法

    若是把CRUSH(PG_ID)改为 HASH(PG_ID)% OSD_NUM 可否适用? 是会存在一些问题。

    1)若是挂掉一个OSD,全部的OSD_NUM 余数就会发生变化,以前的数据就可能须要从新打乱整理, 一个优秀的存储架构应当在出现故障时, 可以将数据迁移成本降到最低, CRUSH则能够作到。

    2)若是增长一个OSD, OSD_NUM数量增大, 一样会致使数据从新打乱整理,可是经过CRUSH能够保障数据向新增机器均匀的扩散, 且不须要从新打乱整理。

    3)若是保存多个副本,就须要可以获取多个OSD结果的输出, 可是HASH方式只能获取一个, 可是经过CEPH的CRUSH算法能够作到获取多个结果。

  1. Crush算法如何实现

    每一个OSD有不一样的容量,好比是4T仍是800G的容量,能够根据每一个OSD的容量定义它的权重,以T为单位, 好比4T权重设为4,800G则设为0.8。

    那么如何将PG映射到不一样权重的OSD上面?这里能够直接采用CRUSH里面的Straw抽签算法,这里面的抽签是指挑取一个最长的签,而这个签值得就是OSD的权重。若是每次都存储在容量最大的OSD上,很容易将该节点塞满, 这就须要采起相似随机权重的算法来作实现。

    ![file](/img/bVcQ9tP)

    主要步骤:

    • 计算HASH: CRUSH_HASH( PG_ID, OSD_ID, r ) ==> draw

      把r当作一个常数,将PG_ID, OSD_ID一块儿做为输入,获得一个HASH值。

    • 增长OSD权重: ( draw &0xffff ) * osd_weight ==> osd_straw

      将计算出的HASH值与OSD的权重放置一块儿,这样就可以获得每一个OSD的签长, 权重越大的,数值越大。

    • 遍历选取最高的权重:high_draw

    Crush目的是随机跳出一个OSD,而且要知足权重越大的OSD,挑中的几率越大,为了保障随机性,将每一个OSD的权重都乘以一个随机数也就是HASH值,再去结果最大的那个。若是样本容量足够大, 随机数对选中的结果影响逐渐变小, 起决定性的是OSD的权重,OSD的权重越大, 被挑选的几率也就越大,这样可以作到数据的有效分布。

    Crush所计算出的随机数,是经过HASH得出来,这样能够保障相同的输入, 会得出一样的输出结果。 因此Crush并非真正的随机算法, 而是一个伪随机算法。

    这里只是计算得出了一个OSD,在Ceph集群中是会存在多个副本,如何解决一个PG映射到多个OSD的问题?

    将以前的常量r加1, 再去计算一遍,若是和以前的OSD编号不同, 那么就选取它;若是同样的话,那么再把r+2,再从新计算,直到选出三个不同的OSD编号。
    ![file](/img/bVZkcX)

    假设常数r=0,根据算法(CRUSH_HASH & 0xFFFF) * weight 计算最大的一个OSD,结果为osd.1的0x39A00,也就是选出的第一个OSD,而后再让r=1, 生成新的CRUSH_HASH随机值,取得第二个OSD,依次获得第三个OSD。

4. IO流程图

file

步骤:

  1. client链接monitor获取集群map信息。
  2. 同时新主osd1因为没有pg数据会主动上报monitor告知让osd2临时接替为主。
  3. 临时主osd2会把数据全量同步给新主osd1。
  4. client IO读写直接链接临时主osd2进行读写。
  5. osd2收到读写io,同时写入另外两副本节点。
  6. 等待osd2以及另外两副本写入成功。
  7. osd2三份数据都写入成功返回给client, 此时client io读写完毕。
  8. 若是osd1数据同步完毕,临时主osd2会交出主角色。
  9. osd1成为主节点,osd2变成副本。

5. Ceph 通讯机制

网络通讯框架三种不一样的实现方式:

  • Simple线程模式

    • 特色:每个网络连接,都会建立两个线程,一个用于接收,一个用于发送。
    • 缺点:大量的连接会产生大量的线程,会消耗CPU资源,影响性能。
  • Async事件的I/O多路复用模式

    • 特色:这种是目前网络通讯中普遍采用的方式。新版默认已经使用Asnyc异步方式了。
  • XIO方式使用了开源的网络通讯库accelio来实现

    • 特色:这种方式须要依赖第三方的库accelio稳定性,目前处于试验阶段。

消息的内容主要分为三部分:

  • header //消息头类型消息的信封
  • user data //须要发送的实际数据

    • payload //操做保存元数据
    • middle //预留字段
    • data //读写数据
  • footer //消息的结束标记

file

步骤:

  • Accepter监听peer的请求, 调用 SimpleMessenger::add_accept_pipe() 建立新的 Pipe, 给 SimpleMessenger::pipes 来处理该请求。
  • Pipe用于消息的读取和发送。该类主要有两个组件,Pipe::Reader,Pipe::Writer用来处理消息读取和发送。
  • Messenger做为消息的发布者, 各个 Dispatcher 子类做为消息的订阅者, Messenger 收到消息以后, 经过 Pipe 读取消息,而后转给 Dispatcher 处理。
  • Dispatcher是订阅者的基类,具体的订阅后端继承该类,初始化的时候经过 Messenger::add_dispatcher_tail/head 注册到 Messenger::dispatchers. 收到消息后,通知该类处理。
  • DispatchQueue该类用来缓存收到的消息, 而后唤醒 DispatchQueue::dispatch_thread 线程找到后端的 Dispatch 处理消息。

6. Ceph RBD 块存储 IO流程图

file

osd写入过程:

  1. 采用的是librbd的形式,使用librbd建立一个块设备,向这个块设备中写入数据。
  2. 在客户端本地经过调用librados接口,而后通过pool,rbd,object、pg进行层层映射,在PG这一层中,能够知道数据是保存在哪三个OSD上,这三个OSD分别为主从的关系。
  3. 客户端与primary OSD创建SOCKET 通讯,将要写入的数据传给primary OSD,由primary OSD再将数据发送给其余replica OSD数据节点。

7. Ceph 心跳和故障检测机制

问题:

故障检测时间和心跳报文带来的负载, 如何权衡下降压力?

  1. 心跳频率过高则过多的心跳报文会影响系统性能。
  2. 心跳频率太低则会延长发现故障节点的时间,从而影响系统的可用性。

故障检测策略应该可以作到:

及时性:节点发生异常如宕机或网络中断时,集群能够在可接受的时间范围内感知。

适当的压力:包括对节点的压力,和对网络的压力。

容忍网络抖动:网络偶尔延迟。

扩散机制:节点存活状态改变致使的元信息变化须要经过某种机制扩散到整个集群。

file

OSD节点会监听public、cluster、front和back四个端口

  • public端口:监听来自Monitor和Client的链接。
  • cluster端口:监听来自OSD Peer的链接。
  • front端口:客户端链接集群使用的网卡, 这里临时给集群内部之间进行心跳。
  • back端口:在集群内部使用的网卡。集群内部之间进行心跳。
  • hbclient:发送ping心跳的messenger(送信者)。

file

Ceph OSD之间相互心跳检测

  • 同一个PG内OSD互相心跳,他们互相发送PING/PONG信息。
  • 每隔6s检测一次(实际会在这个基础上加一个随机时间来避免峰值)。
  • 20s没有检测到心跳回复,加入failure队列。

本文由mirson创做分享,如需进一步交流,请加QQ群:19310171或访问www.softart.cn

相关文章
相关标签/搜索