背景
Ceph是在2006年由Sage Weil作为博士论文中的项目开发的,随着近些年OpenStack将其作为默认的块存储后端,逐渐流行起来并称为开源社区中的明星项目。Ceph是一个统一的存储系统,既支持传统的块存储和文件存储协议,也支持新兴的对象存储协议,这使得Ceph能够满足当下绝大部分的存储场景需求。
架构
Ceph虽然已经创建10余年了,但是设计理念在当前依然不过时,其将高可扩展性、高可靠性和高性能作为核心设计理念。RADOS是Ceph的核心支撑组件,Ceph通过RADOS和其接口库librados构建了三大核心应用组件:RGW对象存储、RBD块存储、CephFS文件存储,通过RADOS和librados可以开发更多类型的存储应用。
RADOS
RADOS是Reliable Autonomic Distributed Object Store的简写,是Ceph底层的高可靠分布式对象存储系统。RADOS构建了一个随机读写的对象存储系统,上层的对象存储、块存储和文件存储都映射到底层的RADOS对象存储中,RADOS对象存储主要包含Collection和Object概念。
-
Collection:相当于"目录",在RADOS中称为PG,是Pool的一个分片
-
Object:相当于"文件",其中主要包括三类数据:
-
Data:相当于"文件数据"
-
Attr:相当于"文件属性",小的KV对
-
OMap:不受限制的KV对
-
每个Object都有一个唯一Id、二进制数据和一组KV对元信息:
RADOS中包括三类进程:
-
OSD:存储数据的进程,负责数据复制、均衡和修复
-
Monitor:集群状态管理,使用Paxos实现元信息高可用。元信息包括OSD Map、MDS Map和Monitor Map(Monitor中Paxos集群自身信息)。
-
Manager:维护Placement Group和Host的详细信息,卸载Monitor上很大部分针对这部分的只读请求来提升扩展性
逻辑分片
Ceph对集群中的所有存储资源进行池化管理,将资源池抽象成Pool概念,每个Pool拥有一个唯一的Pool Id,并可以设定不同的属性(比如副本数、复制方式、存储介质等)。每个Pool可以再进行切分,每个分片称为PG(Placement Group的简写),PG的数量通常设置为2的幂次方,可以调整PG数量进行扩容(通常不会对PG数量进行调整)。RADOS中存储单元是对象,每个对象拥有一个唯一ObjectId,每个对象归属于某个PG。
上图中Ceph的数据切片和分布分为两层:
-
第一层:Object到PG的映射是一个静态逻辑映射,通过一个伪随机Hash函数输入ObjectId和PoolId就可以得出对应的PGId。
-
第二层:PG到OSD的映射是一个动态物理映射,通过CRUSH算法输入PGId和集群拓扑信息计算得到PG的副本的OSD分布。
虽然RADOS是以Object作为读写管理单元,但是底层数据却是以PG进行组织的。PG映射方法称为stable_mod,其逻辑如下:
if ((hash & (2^n - 1)) < pg_num)
return (hash & (2^n - 1))
else
return (hash & (2^(n-1) - 1))
2^n - 1为PG的掩码,PG数量应当尽量设置为2的幂次方,这样数据分布更均匀。如果PG数量不是2的幂次方,即2^(n-1) < PG < 2^(n),按照上面的稳定取模算法将[PG, 2^n)之间的数据重新分布到[2^n-PG, 2^(n-1))之间,下图即为n=4,PG=12时数据分布,PGId 4-7数据会比其他PG多。
除了上面的PG_NUM之外,RADOS中还有一个PGP_NUM,理解这两个概念的差异对理解PG分裂至关重要。PG_NUM是对象所属Pool中的PG数量,决定逻辑放置;PGP_NUM是PG所属父PG对应Pool中的PG数量,决定物理放置。通常情况下PG_NUM=PGP_NUM,但是在PG分裂的时候这两个值不同,先调大PG_NUM调小每个分片的数据量但物理放置不变,再调大PGP_NUM调整PG的物理放置将新分裂出来的PG负载均衡到新的OSD上。具体可以参考Ceph社区中对PG和PGP的一段描述:
PG = Placement Group
PGP = Placement Group for Placement purpose
pg_num = number of placement groups mapped to an OSD
When pg_num is increased for any pool, every PG of this pool splits into half, but they all remain mapped to their parent OSD.
Until this time, Ceph does not start rebalancing. Now, when you increase the pgp_num value for the same pool, PGs start to migrate from the parent to some other OSD, and cluster rebalancing starts. This is how PGP plays an important role.
Ceph的PG分裂其实分为两阶段:
-
第一个阶段是调整PG数量,对应OSD上的PG会根据Hash值左移原地分为几个新的PG,这个过程影响对象到PG的映射;
-
第二个阶段是调整PGP数量,开始将新创建出来的PG进行负载均衡,这个过程影响PG的放置策略。
引入PGP_NUM也是为了让分裂更平滑,如果直接使用新增孩子PG的PGId作为CRUSH算法输入,将会导致大量新增孩子PG在OSD之前迁移。所以在分裂过程中,我们可以使用父PG数量PGP_NUM作为CRUSH算法输入,这样可以在数据不用迁移的情况快速找到对应的Object数据。这样不管PG是否分裂,CRUSH算法以PGP_NUM作为输入总能找到对应的PG。这个过程跟传统的Table分裂不同,Table分裂有一个全局的中控负责分裂并记录分裂的起止点,但是Ceph的整个数据读写过程中都是基于Hash计算的,所以采取这种两阶段的根据Object的Hash值分裂的方式。
物理分布
RADOS没有在Monitor/Manager中维护PG在OSD间的物理分布元信息,而是通过CRUSH算法解决的。CRUSH算法的全称为:Controlled、Scalable、Decentralized Placement of Replicated Data,即可控的、可扩展的、分布式的副本数据放置算法。CRUSH算法是一个稳定的副本分布计算算法,实现PG到OSD的稳定伪随机映射,即使有一个或多个设备加入或离开集群,大部分PG到OSD的映射关系是不变的,CRUSH仅移动部分数据来维持数据分布的均衡性。CRUSH还支持设置权重来控制每个存储设备上分配的数据量,权重的设置依据可以是设备的容量或性能等方面。
CRUSH算法中PG到OSD的寻址过程可以用下面的函数表示:
CRUSH(PGId, ClusterMap, PlacementRule) -> (OSDx, OSDy, OSDz)
ClusterMap定义了OSD集群具有层次关系的静态拓扑结构,这种层次关系的拓扑表示使得CRUSH算法能够实现机架感知,即将副本分布在不同的机房、不同机架上,实现数据的高可靠性。层级化的Cluster Map中有一些概念:
-
Device:最基本的存储设备,也就是OSD,一个OSD对应一个磁盘存储设备。
-
Bucket:设备的容器,可以递归的包含多个设备或者子类型的Bucket。Ceph中默认有Root、Datacenter、Room、Row、Rack、Host六种Bucket类型,用户可以自定义新的类型。每个Device都设置了权重(一般与存储空间相关),Bucket的权重就是子Bucket的权重之和。
Placement Rule决定了一个PG的副本如何选择的规则,允许用户设定副本在集群中的分布。Placement Rule定义一般包括三部分:take选择bucket、choose选择osd、emit输出结果。其定义格式如下:
tack {bucket}
choose
choose firstn {num} type {bucket-type}
chooseleaf firstn {num} type {bucket-type}
If {num} == 0, choose pool-num-replicas buckets (all available).
If {num} > 0 && < pool-num-replicas, choose that many buckets.
If {num} < 0, it means pool-num-replicas - {num}.
emit
第二步choose可以迭代多次选择,比如在同一Row下选择三个不同Tor的osd可以先choose某一个Row,再choose三个Tor,最后choose一个osd。
step take root
step choose firstn 1 type row
step choose firstn 3 type tor
step choose firstn 1 type osd
step emit
Placement Rule也可以实现主副本在SSD上,其他副本在HDD上:
step take ssd
step chooseleaf firstn 1 type host
step emit
step take hdd
step chooseleaf firstn -1 type host
step emit
CRUSH支持多种Bucket随机选择算法,比如Uniform、List、Tree、Straw、Straw2,默认使用的Straw算法比较容易应对Item的增加和删除。几种算法的对比如下:
OSD选择过程由CRUSH_HASH(x, r, i)函数控制。其中x为PGId,r为选择的副本数,i为osd的Id。以Straw算法为例,它是一个伪随机抽签算法,每个osd在选择时都乘以一个osd相关的随机数,最终选择乘积最大的osd,这样在海量pick计算下基本保证权重大的OSD被更大概率选出来:
foreach item in bucket
draw = CRUSH_HASH(PG_ID, OSD_ID, r)
osd_straw = (draw & 0xffff) * osd_weight
check and restore high_osd_straw
return high_osd_straw
PG多个副本在执行Bucket选择算法的时候只需要简单将上面的Hash算法中r参数递增即可算出对应的OSD。针对不同r输入可能会出现的冲突,比如某个OSD被重复选择,或者是某个OSD已经离线或者是过载,这些情况下就需要重新选择,当出现冲突需要重新选择的时候,把参数r顺序增加重新计算hash函数就可以得到新的hash值。下面就是一个pg 1.1副本选择过程中osd2冲突重新选择的case:
CRUSH算法在小规模集群中会有一定的数据不均衡现象,另外在增加新设备的时候一定会在新旧设备间进行数据迁移。考虑到即使是使用专门的表来管理每个PG到OSD的映射关系,在一个相当大的集群情况下这个表也是很小的(MB级别),很多存储系统中副本的物理分布元信息是保存在Master中的,只需要在Client端缓存这部分元信息,就不会影响整个存储系统的读写性能。另外CRUSH算法中集群拓扑信息变化依然会造成一部分数据的迁移,使用集中映射表可以只迁移最小需要迁移的数据。CDS的设计中逻辑数据分片部分就参考了RADOS的实现,但是物理数据分布就是直接将分片的副本位置信息保存在Master中。
Ceph中没有单独的根据容量的负载均衡流程,而是在OSD设备新加入时进行自动触发根据CRUSH算法的数据迁移,使得集群中数据分布自动达到均衡。数据迁移的基本单位是PG,当新OSD加入时数据是全空的,按照下面的修复流程将部分PG的副本在其上进行修复来实现数据迁移到新OSD上。
读写流程
RADOS的读写遵循上面介绍的两阶段方案,具体读写流程的伪码如下:
locator = object_name
obj_hash = hash(locator)
pg = obj_hash % num_pg
osds_for_pg = crush(pg) # returns a list of osds
primary = osds_for_pg[0]
replicas = osds_for_pg[1:]
RADOS通过上面的CRUSH算法找到Object所属的PG之后,直接将读写请求发送给PG的主OSD,写入和读取都有多副本和EC两种场景,从Client视角读写都是发送给主OSD。
多副本场景由Client直接写入主OSD,主OSD在三副本全部完成写入才返回给Client。具体流程如下:
在早期版本中OSD使用FileStore时,写入Journal和写入Data会进行两次ACK(当前默认的BlueStore中只有一次ACK),具体流程如下:
EC场景下写入流程跟多副本类似,也是由Client先写入主OSD,再由主OSD进行EC编码,将生成的数据块和编码块写入全部OSD中,当全部OSD都写入成功之后再返回。写入过程中如果长度不对齐通过Read-Modify-Write方式读取之前的数据合并新的更新再写入,并对Append写入中不足条带宽度的进行补零。具体流程如下:
修复流程
Ceph的故障处理流程主要分为三大步骤:
-
感知集群状态:首先Ceph要能通过某种方法,及时感知集群故障,确定集群中节点的状态,判定哪些节点离开了集群,为确定哪些数据的副本受到故障影响提供权威依据。
-
确定受故障影响的数据:Ceph根据新的集群状态计算和判定副本缺失的数据。
-
恢复受影响的数据。
Ceph集群分为MON集群和OSD集群两大部分。其中MON集群通过Paxos算法组成一个决策者集群,共同作出关键集群事件的决策和广播。“OSD节点离开”和“OSD节点加入”就是其中两个关键的集群事件。OSD节点加入和离开事件是通过OSD的心跳机制实现的,OSD定期向Mon集群汇报心跳,同一个PG的OSD之间也定期进行故障检测,OSD节点的状态信息保存在Mon集群上的OSD Map中。Mon集群通过OSD节点心跳汇报和OSD节点间故障探测汇报来判断OSD节点是在线还是离线。
判定OSD节点离线后,Mon集群修改OSDMap并增加其epoch版本信息,并通过消息机制将最新的OSDMap随机分发给一个OSD,客户端(对等OSD)处理IO请求的时候发现自身的OSDMap版本过低,会向MON请求最新的OSDMap。每个OSD中PG的另外两个副本可能在集群任意OSD中,借此经过一段时间的传播,最终整个集群的OSD都会接收到OSDMap的更新。(这个过程是一种Lazy触发更新方式,防止OSD大规模故障时集群产生突发修复流量)
OSD在收到OSDMap更新消息后会扫描该OSD下所有的PG,清理已经不存在的PG(已经被删除等情况),对PG进行初始化,如果该OSD上的PG是Primary PG的话,PG将进行Peering操作。在Peering的过程中,PG会根据PGLog检查多个副本的一致性,并尝试计算PG的不同副本的数据缺失,最后得到一份完整的对象缺失列表,用作后续进行Recovery操作时的依据(Recovery是一个后台过程,确保PG中全部对象在Acting Set节点上的副本都存在)。对于无法根据PGLog计算丢失数据的PG,需要通过Backfill操作拷贝整个PG的数据来恢复。需要注意的是,在这Peering过程完成前,PG的数据都是不可靠的,因此在Peering过程中PG会暂停所有客户端的IO请求。Peering的过程主要包括三个步骤:
-
GetInfo:PG的主OSD通过发送消息获取所有从OSD的pg_info信息。
-
GetLog:根据各个副本获取的pg_info信息的比较,选择一个拥有权威日志的OSD。如果主OSD不是拥有权威日志的OSD,就从该OSD上拉取权威日志。主OSD完成拉取权威日志后也就拥有了权威日志。
-
GetMissing:主OSD拉取其他OSD的PG日志。通过与本地权威日志的比较,来计算该OSD上缺失的object信息,作为后续recover操作过程的依据。
Peering完成后,PG进入Active状态,并根据PG的副本状态将自己标记为Degraded/Undersized状态,在Degraded状态下,PGLog存储的日志数量默认会从3000条扩展到10000条记录,提供更多的数据记录便于副本节点上线后的数据恢复。进入Active状态后,PG可用并开始接受数据IO的请求,并根据Peering的信息决定是否进行Recovery和Backfill操作。Primary PG将根据对象的缺失列表进行具体对象的数据拷贝,对于Replica PG缺失的数据Primary 会通过Push操作推送缺失数据,对于Primary PG缺失的数据会通过Pull操作从副本获取缺失数据。在恢复操作过程中,PG会传输完整4M大小的对象。对于无法依靠PGLog进行Recovery的,PG将进行Backfill操作,进行数据的全量拷贝。待各个副本的数据完全同步后,PG被标记为Clean状态,副本数据保持一致,数据恢复完成。Peering的细节可以参考官方文档、《Ceph原理与实现》和《Ceph源码分析》。
由于Ceph的IO流程必须要通过Primary PG进行,一旦Primary PG所在的OSD宕机,IO将无法正常进行。Primary OSD宕机后,MON通过CRUSH算法得到的新的Primary OSD上没有数据就会触发Backfill恢复数据,为了保证恢复过程中不会中断正常的业务IO,MON会分配PG Temp临时处理IO请求,在数据恢复完成后再移除PG Temp。比如刚开始PG的Acting Set(在任集合)为[0, 1, 2],osd0为主osd,当osd0发生故障CRUSH重新计算出该PG的Acting Set为[3, 1, 2],此时osd3为主osd但是新加入其上没有任何数据,就向Monitor申请一个临时的PG [1, 2, 3],将osd1作为临时PG的主osd,当osd3完成Backfill之后再删除临时PG,将Acting Set改为[3, 1, 2]。
Cache Tier
RADOS实现了以Pool未基础的自动分层存储机制,第一层叫Cache Tier设置为Cache Pool,使用SSD高速存储设备;第二层Storage Tier设置为Data Pool,使用HDD大容量低速存储设备,可以使用EC模式降低存储空间。Cache Tirer层和Storage Tier层之间数据根据活跃度自动迁移。通过Cache Tier可以提高关键数据或者热点数据的性能,同时降低存储成本。
Cache Tier支持一下几种模式:
-
write back:读写请求都直接发给cache pool,cache pool中的数据自动flush到data pool,cache pool中miss的时候从data pool中load,适合于大量修改的应用场景
-
read proxy:当读的对象不在cache pool中,cache pool层向data pool发送请求,适用于从write back模式向none模式过渡
-
read forward:当读的对象不在cache pool中,直接返回给client再由client直接读data pool
-
write proxy:当写的对象在cache pool中miss的时,不等待数据从data pool加载到cache pool,直接发送写入到data pool
-
read only:也叫write-around或read cache,读请求直接发给cache pool,写请求直接发给data pool。cache pool一般设置为单副本,适合一次写入多次读取的场景
BlueStore
BlueStore是Ceph从Luminous版本开始默认的ObjectStore存储后端,之前一直是基于本地文件系统构建的FileStore,社区开发了一个NewStore但是没有最终Release,FileStore和NewStore的问题:
-
由于底层文件系统使用了Journal,导致数据最终被写了两遍,这意味着Ceph牺牲了一半的磁盘吞吐量。
-
Journaling of Journal问题,这个在上述Write Behaviors论文中有讲。Ceph的FileStore做了一遍日志,而Linux文件系统自身也有日志机制,实际上日志被多做了一遍。
-
对于新型的LSM-Tree类存储,如RocksDB、LevelDB,由于数据本身就按照日志形式组织,实际上没有再另加一个单独的WAL的必要。
-
更好地发挥SSD/NVM存储介质的性能。与磁盘不同,基于Flash的存储有更高的并行能力,需要加以利用。CPU处理速度逐渐更不上存储,因而需要更好地利用多核并行。存储中大量使用的队列等,容易引发并发竞争耗时,也需要优化。另一方面,RocksDB对SSD等有良好支持,它为BlueStore所采用。
BlueStore是Block NewStore的简写,整体架构分为三个部分:BlockDevice、BlueFS和RocksDB。BlockDevice为最底层的块设备,BlueStore抛弃了本地文件系统直接使用AIO读写裸块设备,由于AIO只支持directIO,所以对BlockDevice的写操作直接写入磁盘,并且需要按照page对齐。BlueStore基于RocksDB和BlockDevice实现的Ceph的对象存储,其所有的元数据都保存在RocksDB这个KV存储系统中,包括对象的集合、对象、存储池的omap信息,磁盘空间分配记录等都保存RocksDB里, 其对象的数据直接保存在BlockDevice上,不使用本地文件系统,直接接管裸设备,并且只使用一个原始分区。BlueStore的整体架构如下图所示:
通过Allocator(分配器)实现对裸设备的管理,直接将数据保存到设备上;同时针对 metadata 使用 RocksDB 进行保存,底层基于裸盘自行封装了一个BlueFS并使用BlueRocksEnv来对接RocksDB。
BlueFS是为支持RocksDB而实现的文件系统,RocksDB中文件的读写必须实现rocksdb::EnvWrapper接口,BlueFS通过BlueRocksEnv在裸盘上直接实现了一个轻量文件系统来支持RocksDB。BlueFS将存储空间划分为三层:慢速(Slow)空间、高速(DB)空间、超高速(WAL)空间。BlueFS主要有三部分数据:Superblock、Journal和Data。Superblock主要存放BlueFS的全局信息以及日志的信息,其位置固定在BlueFS的头部;Journal中存放日志记录,一般会预分配一块连续区域,写满以后从剩余空间再进行分配;Data为实际的文件数据存放区域,每次写入时从剩余空间分配一块区域。SuperBlock和Journal组成LogBase架构,SuperBlock是元信息的Base,Journal是Log。
BlueFS以尽量简单为目的设计,专门用于支持RocksDB,并不需要支持POSIX接口,只需要通过RocksEnv适配RocksDB即可。总的来说它有这些特点:
-
目录结构方面,BlueFS只有扁平的目录结构,没有树形层次关系;用于放置RocksDB的db.wal/,db/,db.slow/文件。这些文件可以被挂载到不同的硬盘上,例如db.wal/放在NVMe高速设备上;db/包含热SST数据可以放在SSD上;db.slow/放在HDD磁盘上。
-
数据写入方面,BlueFS不支持覆写,只支持追加(Append-only)。块分配粒度较粗,约1MB。有垃圾回收机制定期处理被浪费掉的空间。
-
对元数据的操作记录到日志,每次挂载时重放日志,来获得当前的元数据。元数据生存在内存中,并没有持久化在磁盘上,不需要存储诸如空闲块链表之类的。当日志过大时,会进行重写Compact。
BlueStore的写入分三种:
-
写入Object新分配的区域:无需Journal,直接写入Data,写完后写Meta记录索引位置
-
写入已存在Blob的新位置:无需Journal,直接写入Data,写完后写Meta记录索引位置
-
覆盖写入已存在Blob的位置:需要引入Journal进行延迟写,Journal中需要包含数据。如果写入长度较小通过写Journal进行写合并;如果写入长度较大,根据磁盘块大小将写入数据分为三块:首尾非对齐部分和中间块对齐部分。中间块部分可以按照ROW写入新位置再更新Meta中的索引记录;首尾非对齐部分写入Journal,即先写入日志盘成功后才去更新数据盘,数据盘更新完成后才释放日志。
对于对象存储不存在Overwrite场景,因此BlueStore对对象存储有很大的性能提升。对于Overwrite场景中的Journal,并不是基于文件系统实现的Journal,而是写入RocksDB借助其WAL实现Journal。这里以Simple Write表示新写/对齐写(COW)等不需要WAL的场景,以Deferred Write表示需要写WAL的场景(RMW-Read Modify Write)。对于用户或OSD层面的一次IO写请求,到BlueStore这一层可能是Simple Write,也可能是Deferred Write,还有可能是Simple Write和Deferred Write组合的场景。
Simple Write
SimpleWrite的写入流程,先把数据写入新的block,然后更新k/v元信息:
Deferred Write
Deferred Write的写入流程,WAL直接封装在RocksDB的k/v事务中,通过RocksDB写日志来commit k/v操作;日志写完成后再执行一个事务通过RMW实现磁盘上数据的更新;最后再删除第一步写入的RocksDB中k/v:
Simple Write + Deferred Write
Simple Write和Deferred Write组合的情况,处理流程相当于将两者组合,只是将Simple Write中更新k/v元信息跟Deferred Write中的WAL写RocksDB合并到一个RocksDB事务中,其他的阶段处理流程跟上面两种写入相同:
RGW
对象存储以对象作为数据存储单元,舍弃了文件系统元数据管理的特性,将所有对象以扁平方式进行存储。RGW是Ceph作为对象存储的网关系统,在RADOS之上构建了一个Http代理层实现对象存储的逻辑。
数据模型
对象存储的数据模型是一种用户、存储桶和对象的层次关系:
-
用户:对象存储应用的使用者,一个用户可以有一个或多个存储桶
-
存储桶:对象存储的容器,同一属性对象的管理单元
-
对象:数据组织和存储的基本单位,一个对象包括数据和元数据
用户是为了实现对象存储系统的认证、鉴权和配额,RGW将用户信息保存在一个RADOS对象。
一个存储桶也对应一个RADOS对象,一个存储桶包含的信息分为两类:一类是对RGW网关透明的用户自定义元数据,一类是RGW网关关注的存储策略、索引对象数目等信息。创建存储桶的时候,会同步创建一个或多个索引对象用于保存该存储桶下的对象列表,以支持List Bucket操作,因此对象的上传和删除都必须更新索引对象。Ceph最初一个存储桶只有一个索引对象,索引对象的更新成为对象上传和删除的瓶颈点,新的Ceph版本中将索引对象进行切片成多个索引对象,这样可以极大提升对象的写性能。索引对象分片会导致List Bucket操作变慢,Ceph使用并行读取索引对象再进行Merge的方式进行优化。
对象的上传分为两种方式:整体上传和分段上传,其中分段上传又叫三步上传。RGW限制整体上传一个对象大小不能超过5GB,超过这个大小必须走分段上传。上传过程中RGW进行认证、鉴权和限速。限速过程中多个RGW网关中有一个QoS缓存,定期进行Read-Modify-Write方式更新本地QoS。
整体上传
整体上传时,当对象大小小于RGW分块大小时,用户上传的对象只对应一个RADOS对象,该对象以应用对象名称命名,应用对象元数据保存在这个RADOS对象的扩展属性中。当用户上传的对象大于RGW分块大小时,就会被拆分成多个分块:一个大小等于分块大小的首对象,多个大小等于条带大小的中间对象,一个小鱼等于条带大小的尾对象。其中首对象以应用对象名称命名,并称为head_obj,保存了应用对象前rgw_max_chunk_size大小的数据和应用对象的元信息数据和manifest信息。中间对象和尾对象保存剩余的数据,命名为"shadow_"+"."+"32随机字符串"+"_"+"条带编码"。
RGW支持对象的多版本,整体上传时head_obj指向具体某个版本的head_obj。
分段上传
分段上传时,RGW网关按照条带大小将每个分段分成多个RADOS对象,每个分段的第一个RADOS对象名称为"_multipart_"+"用户上传对象名称"+"分段上传Id"+"分段编号",其余对象名称为"_shadow_"+"用户上传对象名称"+"分段上传Id"+"分段编号"。当所有分片上传结束后,RGW会另外生成一个RADOS对象,用于保存应用对象元数据和所有分段的manifest。
分段上传也支持对象的多版本,head_obj指向某一个版本的分段上传head_obj。
RBD
RBD是Ceph三大存储服务组件之一,是RADOS Block Device的简写,也是当前Ceph最稳定、应用最广泛的存储接口。上层应用访问RBD块设备有两种途径:librbd和krbd。RBD的架构与RGW和CephFS架构有很大不同,RBD块设备由于元信息比较少且访问不频繁,因此不需要守护进程将元信息加载到内存中进行元信息访问加速。
元数据
RBD块设备在Ceph中被称为image,image由元数据和数据两部分组成,其中元数据保存在多个特殊的RADOS对象中,数据被自动条带化保存在多个RADOS对象中。除了Image自身的元数据之外,在Image所属的存储池中还有一组特殊的RADOS对象记录Image关联关系(比如快照和克隆信息)或附加信息等RBD管理元数据。
Image的核心元数据保存在rbd_id.<name>、rbd_header.<id>、rbd_object_map.<id>这三个对象中,其中rbd_id保存name和id之间的映射,rbd_header保存容量、功能、快照、条带参数等元信息,rbd_object_map保存image中对应的block是否存在。因为EC存储池不支持omap,为了让rbd的数据支持EC,rbd的元数据和数据可以设置不同的存储池。数据存储在rbd_data.<pool_id>.<id>前缀的对象中。
Ceph从H版本引入object-map,让Client端感知Image中对象数据的分布,在容量统计、快照、克隆等场景下无需遍历整个Image的全部数据对象,执行时间大大缩短。
快照
RBD快照是为Image创建一个只读快照,只需要保存少量的快照元信息,其底层数据IO实现完全依赖于RADOS快照实现,数据对象克隆生成快照对象的COW过程对RBD客户端无感知,RADOS层根据RBD客户端发起的数据对象IO所携带的SnapContext信息决定是否要进行COW操作,RBD快照完全在OSD端Lazy实现。
RADOS层支持单个RADOS对象的快照操作,一个RADOS对象由一个head对象和可能多个Clone对象组成。OSD端使用SnapSet结构来保存对象的快照信息,其中clone_overlap字段记录clone对象和head对象的数据内容重叠区间,该字段可用于对象数据恢复时减少OSD之间的数据传输。
下面以一个Image的快照示例进行描述:
初始Image,写入了前8M数据:
创建第一个快照Snap1,只更新rbd_header元数据对象中的snap_seq和snapshot_<snap_id>:
对Obj0写入触发RADOS快照的COW,生成obj0-clone1对象,数据写入head对象:
创建第二个快照snap2,只更新rbd_header中快照元信息:
对obj0、obj1、obj2进行写操作,触发RADOS对象的COW生成clone对象,将数据写入head对象:
创建第三个快照Snap3,修改rbd_header中快照相关元数据:
对obj1、obj2、obj3写入,触发RADOS对象COW,生成clone对象,新数据写入head对象:
克隆
RBD克隆是在RBD快照的基础上实现的可写快照,RBD的实现也是使用COW机制,但是不是依赖RADOS的对象快照实现,而是在RBD客户端实现克隆,RADOS层完全不感知Image之间的克隆关系。克隆Image的过程相当于新建Image,并在Image元数据中的Parent字段记录其父快照。
RBD打开克隆Image时,会读取到Parent元数据,构建出Image之间的依赖关系,对克隆Image数据访问时,先访问克隆Image的数据对象,如果不存在会尝试访问父快照的数据对象,由于克隆关系可能存在多层,因此该过程可能会一直回溯到最顶层的Parent。
RBD克隆Image读取流程如下:
RBD克隆Image写入流程:
CephFS
CephFS是在Ceph的分布式对象存储RADOS之上构建的POSIX兼容文件系统。文件元数据与文件数据存储在单独的Pool中,文件元信息和文件数据都映射到Ceph 存储集群内的对象。客户端可以把此文件系统挂载为内核对象或用户空间文件系统( FUSE )。元信息服务器MDS把所有文件系统元数据(目录、文件所有者、访问模式等)永久存储在RADOS对象中,同时将元数据驻留在内存中。 MDS 存在的原因在于简单的文件系统操作像列出ls、cds等高频文件系统元信息读取操作可以无需读取OSD,将其缓存到单独的内存实现可以提供更高的读写性能。
VFS
在介绍MDS之前,先来了解一下文件系统的基础知识。Linux操作系统中通过VFS(Virtual FileSystem)定义文件系统需要实现的POSIX语义接口,来屏蔽不同文件系统之间的差异。
VFS为了适配不同类型的文件系统,定义了4种基本数据类型:
-
SuperBlock:用于保存文件系统的元信息
-
Inode:用于保存文件系统中一个文件系统对象的元信息(目录、文件等)
-
Dentry:文件系统中某个Inode的链接,链接上具有name属性,通过Inode之间的链接实现目录树
-
File:进程打开文件系统中文件的操作句柄,和一个进程相关联
文件系统中为了实现文件的共享引入了链接概念,其中包括硬链接和软链接:
-
硬链接:多个文件名指向同一个Inode,一份物理存储内容在文件系统中有多个名字,内部通过引用计数机制实现只有全部名字的文件都删除后Inode的数据才删除。另外为了防止出现目录环目录不能用来创建硬链接,硬链接不能跨文件系统。
-
软链接:软链接创建一个新的Inode,Inode存储的内容是另外一个文件路径的指向。可以跨文件系统进行软链接指向,没有引用技术管理机制删除软链接文件不影响源文件,删除源文件会导致软链接文件指向出错。
MDS
元数据的访问占整个文件系统的80%,MDS的设计直接影响了Cephfs文件系统的性能。Inode的元信息保存在业界有两种方式,BSD's FFS和C-FFS。BSD模式是单独Hash表保存Inode,通过Inode+DentryName进行索引;C模式是将Inode嵌入到Dentry中。
考虑bash下一个ls操作对应VFS接口的伪码:
foreach item in ${readdir}
getattr($item)
ls既需要拿到目录下面child item的名字,同时也需要遍历查询每个child item的属性,比如是一个目录、文件还是软链接之类的。C模式中Inode元信息嵌入Dentry中就只需要一次读取就能拿到Child Item的Name和Inode属性,BSD模式就需要读取多次读取,一次是获取Child Item的Name,再多次读取Child Inode的属性。C模式的文件系统要比BSD模式快10-300%,但是对硬链接支持不好。BSD模式Inode单独存放的设计,只需要创建多个Dentry指向Inode即可;C模式默认Dentry和Inode之间是一一对应,需要一些扩展机制来实现硬链接。
MDS中Dentry的元数据包括文件名字、Child Inode的属性,Dentry自身也是一个Inode用于被父Dentry进行引用。下图是MDS元数据的结构,Child Inode放在Dentry附近便于快速读取元信息,每个Dentry对应Metadata Pool中的一个RADOS对象,如果dentry下面的child inode特别多存储空间超过4M,就需要进行切片,形成诸如${inode}.00000、${inode}.00001的对象。
每个Inode在RADOS中的内容包括两部分,一部分利用RADOS对象的xattr保存Inode的Parent和Layout信息,一部分使用OMap保存Child Inode信息。文件的元信息保存在上级目录的Dentry中。
为了兼容硬链接中多个Dentry指向一个Inode的Case,MDS将第一个指向Inode的Dentry称之为Primary Dentry,后续指向这个Inode的Dentry称之为Remote Dentry。Inode对象会保存在Primary Dentry定义的目录中,Remote Dentry并没有像Primary Dentry一样保存Inode到Remote Dentry定义的目录中,这样会导致缓存多份Inode数据。MDS构建了一个内存Anchor表来保存,key是Inode,value包括parent和ref。通过Archor表可以实现Inode到根目录的回溯,也可以实现从根目录到某一个Inode的快速查找。通过内存Anchor表可以避免多次读取RADOS对象,提升元信息读写性能。下图是Anchor表示例,其中Path是虚构出来用来便于理解和展示的。
对目录进行重命名操作时,可能会影响整个链上的Inode,需要一个事务来保证整个链上的Inode同时进行更新,将旧的Ref技术减少,新的Ref计数增加,并确保Anchor表跟实际目录结构的一致性。如果Ref技术到零了,表示没有链接引用了可以从Anchor表中删除。
MDS不像Monitor一样使用Paxos实现多副本高可用,而是使用主备模式进行高可用。每个MDS集群创建一个Journal来保证元信息数据一致性,这个Journal也是保存在RADOS对象中的,其内存只是对应的缓存。元信息更新时MDS持久化写入RADOS Journal对象之后才返回,所以主MDS故障之后不会造成数据丢失。
MDS的高可用有冷备和热备两种方式:
-
冷备:备份的mds上只是跟Monitor保持心跳起到一个进程备份的作用,并不缓存元数据。当主mds挂掉后,冷备的mds replay元数据到缓存,需要消耗一点时间。
-
热备:除了进程备份,元数据缓存还时刻通过RADOS Journal对象与主mds保持同步,当主mds挂掉后热备的mds直接变成主mds,中间切换时间窗口较小。
主备MDS之间数据同步通过底层RADOS对象实现的Journal。MDS定期向Monitor集群发送心跳,Monitor长时间未检测到MDS心跳则认为MDS异常;备MDS从主MDS写入的RADOS对象中同步元数据到本机缓存,主MDS故障时备MDS收到MDSMap信息判断进行切主。Monitor集群故障时,MDS收到MonMap消息判断停止服务。MDS主备切换流程如下:
MDS的处理流程是:boot -> replay -> reconnect -> rejoin -> active。
-
replay:mds从rados中的journal对象恢复内存,包括inode table、session map、openfile table、snap table、purge queue等
-
reconnect:client在收到mdsmap变化消息后,client发送reconnect消息给mds,携带caps、openfile和path等信息。mds处理这些请求后重建与合法client的session,并对在cache中的inode重建caps,否则记录
-
rejoin:重新打开open file记录在cache中,并对reconnect阶段记录的caps进行处理
Cephfs支持LazyIO放松部分POSIX语义,通过Buffer Write和Read Cache提升多客户端读写性能,这对HPC场景非常帮助。再就是社区有一个实验特性是multiple file systems,基于主备MDS的CephFS已经能够稳定运行在生产环境了,通过多FS实例可以实现类CFS的云上多租户NAS产品。
读写流程
CephFS中的所有文件数据都存储为RADOS对象,CephFS客户端可以直接访问RADOS对文件数据进行操作,MDS只处理元数据操作。在Ceph中建立了一套独特的系统来管理客户端对Inode的操作权限,称为capabilities,简称CAPS。与其他网络文件系统(例如NFS或SMB)的主要区别之一是授予的CAPS非常精细,并且多个客户端可能在同一个inode上拥有不同的CAPS。
每个文件的Caps元数据可以按照内容划分为5个部分:
-
p:Pin,指该inode存在内存中
-
A:Auth,即对于mode、uid、gid的操作能力
-
L:Link,即inode和dentry相关的count的操作能力
-
X :Xattrs,即对于扩展属性的操作能力
-
F:File,即对文件大小(size)、文件数据和mtime的操作能力
每个部分最多对应有6种对应操作能力:
-
s :shared 共享能力,即改数据可由多客户端获得,1对多的模型
-
x :exclusive 独占能力,只有该客户端
-
r :read 具有读能力
-
w:write 具有写能力
-
c :cache 读具有缓存能力,可以在客户端缓存读数据
-
b :buffer 客户端有缓存写的能力,即写的数据可以缓存在本地客户端
下面举几个示例:
-
AsLsXs:所有客户端可以读和本地缓存相关的元数据状态
-
AxLxXx :只有该客户端可以读和改变相关的元数据状态
-
Fs:可以在本地缓存和读取mtime和文件大小的能力
-
Fx:拥有在本地写mtime和文件大小的能力
-
Fr:可同步地从OSD中读取数据
-
Fc:可从缓存读取客户端中的数据
-
Fw:拥有同步地写入OSD中数据的能力
-
Fb:拥有优先写入缓存的能力,即先入objectcacher然后异步写入OSD
Caps由MDS进行管理,Client端向MDS发送对Caps的更新和请求,MDS内部使用一些锁机制进行管理。MDS可以授权(Grant)Client使用某些Caps,也可以取消(Revoke)某些Caps。在取消Caps的过程中,Client端需要做一些相应的动作,比如之前是Fb需要将写缓存刷到OSD上,之前是Fc需要将读缓存丢掉等。下面是一个Client修改权限的时序图示例:
Ceph中Client要读/写CephFS文件,客户端需要有相应inode的“文件读/写”功能。如果客户端没有需要的功能caps,它发送一个“cap消息”给MDS,告诉MDS它想要什么,MDS将在可能的情况下向客户发布功能 caps。一旦客户端有了“文件读/写”功能,就可以直接访问 RADOS 来读/写文件数据。如果文件只由一个客户端打开,MDS还会向唯一的客户端提供“文件缓存/缓冲区”功能。“文件缓存”功能意味着客户端缓存可以满足文件读取要求。“文件缓冲区”功能意味着可以在客户端缓存中缓冲文件写。
CepgFS Client 访问示例
-
Client 发送 open file 请求给 MDS
-
MDS 返回 file node, file size, capability 和 stripe 信息
-
Client 直接 READ/WRITE 数据到 OSDs(如果无 caps 信息需要先向 MDS 请求 caps)
-
MDS 管理 Client 对该 file 的 capabilities
-
Client 发送 close file 请求给 MDS,释放 file 的 capabilities,更新 file 的详细信息
这里需要注意的一点是MDS并不会将Caps持久化到RADOS Journal对象中,所以在MDS故障重启的时候在reconnect阶段收集Client端上保存的Caps。
总结
优点:
-
Ceph的RADOS设计中的Pool→PG→Object三级模型非常巧妙,可以节省大量的元信息存储,同时PG分裂支持Pool数据规模扩展。
-
CacheTier设计能够非常灵活的调节成本和性能,Cache前后端不同的Pool可以采用不同的存储介质和复制方式。
-
RADOS支持EC的读写,除了WriteFull、Append外也支持Overwrite,相比多副本降低存储成本。
-
Ceph RBD中object-map设计非常有参考价值,可以显著减少Image稀疏时遍历数据块的开销,同时Clone在Client端实现也非常巧妙。(CDS的快照保存在BOS中,不方便借鉴Lazy快照和Client端克隆的设计)
缺点:
-
Ceph的CRUSH算法虽然灵活的,但是经常有非预期的数据迁移,不如Master集中管理PG的OSD分布方便。
-
Ceph写入都是要求全部副本写入成功才返回,在故障或者是数据迁移触发的Peering过程中无法写入,用户体验不好。