Ceph体系结构

RADOS(Reliable, Autonomous, Distributed Object Storage),一个可无限扩展的集群,这一层本身是一个完整的对象存储系统,所有存储在Ceph系统中的用户数据最终都是由这一层来存储

RADOS之上直接有librados(提供操作RADOS的基础库,在此基础上可以开放各种存储应用,以及RADOS GW、RBD等),Ceph FS(一个POSIX兼容的分布式FS)

RADOS物理上由OSD集群和Monitor集群组成,其逻辑结构为Object——PG——OSD

Object—->PG采用hash映射,PG—->OSD采用CRUSH映射

RADOS对Object有最大尺寸限定,所以一个File可能被划分为多个Object,作为RADOS的对象,而不是“应用意义上的对象”

一个PG负责组织若干个Object,一个Object只能被映射到一个PG中,即Object和PG是多对一关系

一个PG会被映射到多个OSD(副本,备份),一个OSD承载多个PG,即PG和OSD是多对多关系

存储池由若干PG和OSD构成,也是逻辑结构。

为什么引入PG?因为object潜在的命名空间巨大,如果一个osb出现故障,需要遍历所有object来寻找所有该承载的object。引入一层PG中间层,PG数量是一定的,OSD数量是一定的,那么一个OSD故障了,只需要遍历该OSD承载的PG,对PG进行迁移即可

Ceph客户端(RBD客户端、RADOS客户端、Ceph FS客户端/MDS)可以直接OSD集群、Monitor集群进行通信

OSD集群向Monitor上报状态信息(满足一些情况即上报,比如增加OSD、异常),Monitor集群负责所有OSD状态的发现与记录,并形成集群运行图的主副本,包括成员、状态、变更、以及ceph存储集群的整体健康状况;随后这份集群运行图被扩散至全体OSD及客户端(OSD使用集群运行图进行数据维护,客户端使用集群运行图进行数据寻址) 注意,信息是OSD主动上报的,图是由Monitor维护再扩散的

集群运行图:Monitor Map、OSD Map、PG Map、CRUSH Map、MDS Map等

CRUSH Map里包含了存储设备列表故障域树状结构(设备的分组信息,如设备、主机、机架、行、房间等)和存储数据时如何利用此树状结构的规则。根节点是default,所有非叶子结点称为桶(bucket,id为负),叶子结点即为OSD(id为正),选择OSD的时候就是要在这棵树上进行选择,有五种算法进行子节点选择:Uniform、List、Tree、Straw、Straw2

由于Object->PG采用hash映射,客户端可以直接通过hash得到目标Object所在的PG信息,然后查询OSD Map(由monitor写好扩散到所有OSD了)就可以得到该PG分布信息,然后客户端就可以直接与Primary OSD进行通信了,不需要Monitor干预。仅当客户端需要获取最新OSD Map的时候才与Monitor通信

OSD和Monitor之间存在心跳机制,OSD定时向Monitor上报PG信息、OSD本身信息。

OSD的操作命令(osd scrub/deep scrub、pg/scrub、deep scrub等)是客户端通过Monitor传递给OSD的(注意是OSD操作命令,不包含读写等通信)

Ceph的写操作采用Primary-Replica模型,客户端向对应OSD Set发起写请求,Primary收到Object的写请求后将数据发送给set里其它副本,当这个数据被保存到所有OSD上的时候(可以选择到内存缓冲区先第一次确认),Primary才应答Object的写请求,保证副本的一致性。序号最靠前的OSD为Primary。读操作则直接与Primary通信(也可以设置为其它OSD以减轻压力)。

Cache Tiering:ceph设计为分层代理模式,一个缓存层(高速ssd等)、一个storage层(低速hdd等),由一个Objecter(对象管理器,位于osdc即OSD客户端模块)决定往哪里存储对象。Objecter决定何时把缓存内的对象“刷回”Storage层。有以下几种模式:写回模式(写:直接写缓存并应答客户端,缓存再自己写Storage层;读:命中直接在缓存层读,未命中重定向到Storage,且最近有访问可以提升到缓存)、forward模式、readonly模式、readforward模式、readproxy模式、proxy模式。

块存储:ceph在基于RADOS对象存储+librados之上通过RBD提供了一个标准的块设备接口,提供基于块设备的访问模式。ceph中的块设备称为Image,可以在ceph上自由创建块设备并可调整,将数据条带化存储到集群的多个OSD中国。条带化能够将多个磁盘驱动器合并成一个卷,这样读写就比单盘快很多(因为是不同设备并行读写,和RAID一个原理),ceph的块设备就对应于LVM的逻辑卷。建立块设备之后,就可以映射到内核中,成为一个虚拟的块设备;或者通过librdb、librados访问块设备(后者一般是虚拟机场景)。

Image数据被分为一个个大小相等的数据片(默认为4MB,比如1GB的image分为256个),每个数据片都以RADOS对象的形式存储在RADOS集群中。且对于一个块设备,RBD在RADOS上还有元数据,主要有rbd_id.<:name>rbd_header.<:id>rbd_object_map.<:id>rbd_data.<:obj_prefix>.<:obj_no>

Ceph FS兼容POSIX的分布式存储文件系统需要对应客户端来访问Ceph FS,目前有Ceph FS FUSE(用户空间文件系统)Ceph FS kernel(集成在内核的Kernel Module,内核空间)两种。Ceph FS FUSE基于libcephfs,而后者又基于librados。Ceph FS要求Ceph集群内至少有一个MDS(元数据服务器),将元数据从OSD分离出来,提高服务性能、减轻存储集群负载。MDS的管理,Multi Active MDS略。

后端存储ObjectStore

ObjectStore完成了实际的数据存储,封装了所有对底层存储的IO操作。主要有以下几种ObjectStore

MemStore:数据全放内存,主要测试用

KStore:数据全放KVDB

FileStore:基于Linux文件系统,每个Object被FileStore看作是一个文件。目前支持的FS有XFS、ext4、Btrfs;通过journal机制支持事物的原子性(一个环形缓冲区),journal是必经路径,是影响性能的瓶颈,也显然导致写数据量增倍,通常用ssd optane等额外的块设备。

BlueStore:直接管理裸设备。在用户态实现了BlockDevice,使用Linux AIO直接对裸设备进行IO,实现了Allocate对裸设备进行空间管理。元数据与数据可以分开存储在不同设备中,元数据以KV的形式保存在KV数据库里(默认RocketDB,基于BlueStore实现的小文件系统BlueFS,不是裸设备);使用SPDK、PMDK

SeaStore:下一代ObjectStore。特点:

①专门为NVMe设计

②SPDK访问NVMe,不再用Linux AIO

③使用SeaStar编程模型进行优化,以及使用share-nothing机制避免锁竞争

④网络驱动使用DPDK来实现零拷贝

⑤Flash设备重写必须先擦除,不知道哪些数据有效,而FS知道,所以垃圾回收功能提到SeaStore来做

CRUSH

Hash——->一致性Hash——->CRUSH

一致性hash组成一个地址环,对服务器地址进行hash,使得服务器固定,环上的数据存储在其右边第一个服务器上,这样就解决了扩容/节点掉线等带来的数据迁移问题

但一致性hash的问题:①所有数据均匀分布,一个结点失效,导致所有用户数据完整性问题②一致性hash无法感知存储节点的实际物理分布能力,无法合理控制数据的失效域

所以提出了CRUSH

CRUSH元数据:CRUSH Map保存了OSD的物理组织结构(Tree)、OSD Map保存了各OSD设备的运行时状态

定义桶(bucket)

默认情况下会创建两种桶(root和主机),所有OSD都放在对应的主机桶中,用户根据自己的物理基础设施部署情况建立对应的桶,比如一个DC、一个机房、一个机架作为一个桶,这里的DC、机房就叫bucket class(即bucket种类)

bucket的class不是自定义的,一定属于某个类型!但name和id可以自定义(即下面的Bucket Class 名称)

要先新建Bucket结构

eg:

ceph osd crush add-bucket dc0 datacenter
//ceph osd crush add-bucket <bucket name> <bucket type>

bucket class有:osd(不属于bucket) 、host(服务器)、chassis(机壳)、rack(机架)、row(行)、pdu、pod、room(房间)、datacenter(数据中心)、region(区域)、root(根)

再对该Bucket进行详细描述

//Bucket Class的定义
type <Bucket Class ID> <Bucket Class 名称>
//桶的定义
<Bucket Class 名称> <桶名>{
	id <负数 id>
	alg <Bucket Type: Uniform/List/Straw/Straw2>
	hash <哈希算法:0/1>
	item <子桶名或设备名1> weight <权重1>
	item <子桶名或设备名2> weight <权重2>
	......
}

shadow crush map:crush map的叶子结点即为osd device,device有三种类型(class)——驱动器/SSD/NVMe。ceph内部会为每个device class维护一个shadow crush map,在用户指定规则中指定一个device class(ceph可以自动识别),比如ssd之后,crush会自行基于对应的shadow crush map执行。

alg 则是核心,其定义了这个Bucket的选择算法,即确定在该bucket的children列表中如何选出一个合适的item。默认用straw2算法。straw类算法的思想就是抽签,让bucket下的每个item随机抽一根签(一个数值),再乘以每个item的权重,然后互相比较选出签最长的那个item。抽签算法的特点是,新增item,新选出的item要么是新item,要么是现在的item,不会是其它item,增强了稳定性,即我们期望集群设备的增删仅影响到必要的结点,不要在正常运行的OSD上做无意义的数据迁移。

#算法实现
def bucket_choose(in_bucket, pgid, trial):
	for index, item in enumerate(in_bucket.children):
		draw = crush_hash(item.id, pgid, trial)
		draw *= item.weight
		if index == 0 or draw > high_draw:
			high_item = item
			high_draw = draw
	return high_item

在选择的时候还有一个is_avalible来判断指定的item是否可用,如果不可用应该抛弃重选

一个Bucket Class 对应一个失效域(故障域 failure domain),CRUSH确保同一故障域最多只会被选中一次

除了默认CRUSH Rule以外,需要自己定义更精细的Rule

rule <规则名称> {
	ruleset <唯一的规则ID>
	type <备份策略:replicated/erasure>
	min_size: <规则支持的最少设备数量>
	max_size: <规则支持的最多设备数量>
	//选择设备范围,确定失效域
    step take <桶名称> [class <Device Class>]#通过桶名称来确定规则的选择范围,对应CRUSH Map中的某一棵子树
    //核心:每一个step choose需要确定一个对应的失效域,以及在当前失效域中选择的子项个数
    step <选择方式:choose/chosseleaf>
    	 <选择备份的策略:firstn/indep>
    	 <选择个数:n>  //即要在这个Bucket中选择多少个OSD
    	 type <失效域锁对应的Bucket Class>
    ...
    step emit  #步骤结束,输出选择的位置
}

每一步step choose都配置一个选择区域,直到最后step choose到OSD,最后可以用chooseleaf,会自动递归choose到OSD节点

选择个数n=0的表示选择域备份数量一致的桶;n=负数的时候表示选择备份数量-n个桶;n=正数的时候表示选择n个桶

firstn:适用镜像备份;indep:适用纠删码备份

CRUSH测试工具:crushtool、crush小程序

调整CRUSH算法:

①Tunables

②主OSD设备的亲和性

③自定义CRUSH Rule,以控制主OSD设备选择范围(即上面的内容)

eg:

rule ssd-primary{
	ruleset 5
	type replicated
	min_size 5
	max_size 10
	//从SSD桶中选择一个主OSD
	step take ssd
	step chooseleaf firstn 1 type host
	step emit
	//从platter桶中选出其他OSD
	step take platter
	step chooseleaf firstn -1 type host
	step emit
}

这就从两个选择域选出了一个主OSD一个从OSD了

用户可以建立存储池,不同的存储池可以关联不同的CRUSH规则,指定不同的备份数量,以镜像或纠删码的方式保存数据,或是指定不同的存取权限和压缩方式。

一言以蔽之,CRUSH:依赖于Hash实现的纯伪随机算法

计算独立性(每次计算独立,不依赖集群分配情况或选择结果)、稳定性(straw算法)、可预测性(CRUSH map离线计算即可预测出PG分布情形,且与集群内实际使用一致)

也有一些问题:假失败、故障额外迁移、使用率不均衡

来源:CRUSH算法的原理与实现

Ceph可靠性

cluster map是一个增量图,每一次OSD状态改变或其它改变都会使得epoch++。cluster map的更新也是增量更新。

Monitor集群通过心跳机制检测OSD是否离线,Monitor集群判断某个OSD节点离线之后,会将最新的OSDMap通过消息机制随机分发给一个OSD,客户端和对等OSD处理 IO请求的时候发现自身OSD版本过低,会向Monitor请求新的OSDMap,经过一段时间的扩散,最终整个集群都会收到OSDMap的更新。

OSD的map在扩散的时候,只与相邻OSD(或Client)通信,属于lazy update,当双方通信谁发现对方版本低就进行update,这样慢慢整个集群就完成了OSDMap更新【问题:这样会不会太慢?见论文Map Propagation有细节】

CRUSH保证稳定性、高可用:数据在集群内自动复制,副本分布到不同的失败域,且映射保证稳定性

Monitor采用集群保证集群结构高可靠性,通过Paxos算法组成一个决策者集群,对关键集群事件作出广播

集群高可用:RBD mirror(两个Ceph集群实时备份,通过主机群开启RDB日志机制,远端集群读RDB日志完成备份)、RBD Snapshot(数据快照到灾备中心)做数据冗余备份

OSD端数据冗余:多副本(强一致性,多个副本写入完毕后回答客户端)、纠删码

CRC进行比特数据通信的完整性校验

数据落盘时记录日志应对突发情况

PG层使用PG日志(pglog)来应对副本之间的数据同步(保证多副本数据一致性)和恢复问题。pglog就在OSD上,一个OSD会管理多个pglog,同一个PG下的不同OSD上的pglog是一致的

不同对象的并发控制(多对象进入同一PG):采用pglog加锁

同一对象的并发控制:网络层(TCP保证)、消息层(Ceph信息编号)、PG层(消息队列来的请求划分为多shared,消息来的时候就对pglog,保证有序)、对象读写锁(由PG层保证,不必单独提供)、ObjectStore层(FileStore里先通过单线程来控制持久化写到journal的顺序性,通过op的seq保证仿佛finish队列的顺序性,并且整个处理过程通过FIFO来保证出入队列的顺序性【?】)、副本层(PG层保证)

Scrub机制:OSD状态异常(如磁盘坏道),需要一种机制主动去检查副本之间的数据是否一致(Scrub、Deep Scrub)【-】

Ceph 缓存

Ceph缓存分为客户端缓存(RBD层在客户端的缓存)、Cache Tiering(在OSD端进行数据缓存),后者更下层。

Ceph缓存有两种模式,对应Cpeh块设备的两种实现方式,内核支持块设备用Ceph社区写的KRBD(内核模块)驱动,可以使用内核的缓存;用户态块设备则用librdb库实现。

用户态不能从内核缓冲中受益,所以Ceph实现了自己的用户态块设备缓存机制RBDCache,位于librbd库中,目前只能使用内存作为缓存。两种模式:写透(数据一致性问题),写回(安全问题)

Linux内核中的缓存主要有Page Cache和Buffer Cache。前者为FS服务,后者则处于下层的块设备层,是块设备的缓存。RBDCache可以认为是块设备内部自带的缓存,也存在掉电等传统缓存有的问题,要用fsync等来完成数据回写。(linux内核提供fsync,librdb提供flush)

KVM write barrier可以在写透与写回之间折中,即安全又高效。它可以根据每个事务来进行数据持久化。写透相当于每次写都会发起fsync,而KVM write barrier相当于每完成一个事务发起一次fsync

RBDCache具体结构略【】

ceph前端

ceph前端可以有RBD方式(RADOS Block Device;块设备访问)、RGW方式(RADOS GateWay;对象存储方式访问,需要对象存储网关)、CephFS方式(Ceph FileSystem;文件系统,需要一个MDS服务器)、以及直接使用librados库(RBD、RGW基于librados库)

注意!RGW/Swift中所说的bucket和Ceph中CRUSH所说的bucket不是一个东西!后者属于ceph架构,更底层,而RGW本身不属于ceph架构!

Ref.

本文为《Linux开源存储全栈详解》一书的读书笔记


且听风吟