Ceph是一个极其复杂的统一分布式存储系统,运维操作门槛高、稳定性不错,性能差强人意,虽然各大厂都在自研分布式存储,但Ceph是不可或缺的参考对象。本文参考了Ceph源代码以及网上各路大神文章,如有侵权,联系删除。简要分析Ceph的架构、重要的模块以及基于Seastar的未来规划,使读者对Ceph有一个大致清晰的认识。
Ceph是由学术界(Sage Weil博士论文)在2006年提出的一个开源的分布式存储系统的解决方案,最早致力于下一代高性能分布式文件存储,经过十多年的发展,还提供了块设备、对象存储S3的接口,成为了统一的分布式存储平台,进而成为开源社区存储领域的明星项目,得到了广泛的实际应用。
Ceph是一个可靠的、自治的、可扩展的分布式存储系统,它支持文件存储、块存储、对象存储三种不同类型的存储,满足存储的多样性需求。整体架构如下:
此处以RBD块设备为例简要介绍Ceph的IO流程。
Ceph的IO通常都是异步的,所以往往伴随着各种回调,以FileStore为例看下ObjectStore层面的回调:
分布式系统中通常需要考虑对象读写的顺序性和并发性,如果两个对象没有共享资源,那么就可以并发访问,如果有共享资源就需要加锁操作。对于同一个对象的并发读写来说,通常是通过队列、锁、版本控制等机制来进行并发控制,以免数据错乱,Ceph中对象的并发读写也是通过队列和锁机制来保证的。
PG
Ceph引入PG逻辑概念来对对象进行分组,不同PG之间的对象是可以并发读写的,单个PG之间的对象不能并发读写,也即理论上PG越多并发的对象也越多,但对于系统的负载也高。
不同对象的并发控制
落在不同PG的不同对象是可以并发读写的,落在统一PG的不同对象,在OSD处理线程中会对PG加锁,放进PG队列里,一直等到调用queue_transactions把OSD的事务提交到ObjectStore层才释放PG的锁,也即
对于同一个PG里的不同对象,是通过PG锁来进行并发控制,不过这个过程中不会涉及到对象的IO,所以不太会影响效率。
同一对象的并发控制
同一对象的并发控制是通过PG锁实现的,但是在使用场景上要分为单客户端、多客户端。
所以接下来主要讨论单客户端下同一对象的异步并发更新。
Message层顺序性
PG层顺序性
从Message层取到消息进行处理时,OSD处理OP时划分了多个shard,每个shard可以配置多个线程,PG通过哈希的方式映射到不同的shard里面。OSD在处理PG时,从拿到消息就会PG加了写锁,放入到PG的OpSequencer队列,等到把OP请求下发到ObjectStore端才释放写锁。对于同一个对象的并发读写通过对象锁来控制。
对同一个对象进行写操作会加write_lock,对同一个对象的读操作会加read_lock,也就是读写锁,读写是互斥的。写锁从queue_transactions开始到数据写入到Pagecache结束。
对同一个对象上的并发写操作,实际上并不会发生,因为放入PG队列是有序的,第一次写从PG取出放到ObjectStore层之后就会释放锁,然后再把第二次写从PG取出放入到ObjectStore层,取出写OP放到ObjectStore层都是调的异步写的接口,这就需要ObjectStore层来保证两次写的顺序性了。
ObjectStore层顺序性
ObjectStore支持FileStore、BlueStore,也都需要保证IO顺序性。对于写请求,到达ObjectStore层之后,会获取OpSequencer(每个PG一个,用来保证PG内OP顺序)。
FileStore:对于写事务OP来说(都有一个唯一递增的seq),会按照顺序放进writeq队列,然后write_thread线程通过Libaio将数据写入到Journal里面,此时数据已经是on_disk但不可读,已完成OP的seq序号按序放到journal的finisher队列里(因为Libaio并不保证顺序,会出现先提交的IO后完成,因此采用op的seq序号来保证完成后处理的顺序),如果某个op之前的op还未完成,那么这个op会等到它之前的op都完成后才一起放到finisher队列里,然后把数据写入到Pagecache和sync到数据盘上。
BlueStore:bluestore在拿到写OP时会先通过BlockDevice提供的异步写(Libaio/SPDK/io_uring)接口先把数据写到数据盘,然后再通过RocksDB的WriteBatch接口批量的写元数据和磁盘分配器信息到RocksDB。由于也是通过异步写接口写的,也需要等待该OP之前的OP都完成,才能写元数据到RocksDB。
在Ceph的设计和实现中,自动数据迁移、自动数据均衡等各种特性都是以PG为基础实现的,PG是最复杂和最难理解的概念,Ceph也基于PG实现了数据的多副本和纠删码存储。基于PG LOG的一致性协议也类似于Raft实现了强一致性。
PG有20多种状态,状态的多样性也反映了功能的多样性和复杂性。PG状态的变化通过事件驱动的状态机来驱动,比如集群状态的变化,OSD加入、删除、宕机、恢复 、创建Pool等,最终都会转换为一系列的状态机事件,从而驱动状态机在不同状态之间跳转和执行处理。
故障检测:Ceph分为MON集群和OSD集群两部分,MON集群管理者整个集群的成员状态,将OSD的信息存放在OSDMap中,OSD定期向MON和Peer OSD 发送心跳包,声明自己处于在线状态。MON接收来自OSD的心跳信息确认OSD在线,同时也接收来自OSD对于Peer OSD的故障检测。当MON判断某个OSD节点离线后,便将最新的OSDMap通过心跳随机的发送给OSD,当Client或者OSD处理IO请求时发现自身的OSDMap版本低于对方,便会向MON请求最新的OSDMap,这种Lasy的更新方式,经过一段时间的传播之后,整个集群都会收到最新的OSDMap。
确定恢复数据:OSD在收到OSDMap的更新消息后,会扫描该OSD下所有的PG,如果发现某些PG已经不属于自己,则会删掉其数据。如果该OSD上的PG是Primary PG的话,将会进行PG Peering操作。在Peering过程中,会根据PGLog检查多个副本的一致性,并计算PG的不同副本的数据缺失情况,PG对应的副本OSD都会得到一份对象缺失列表,然后进行后续的Recovery,如果是新节点加入、不足以根据PGLog来Recovery等情况,则会进行Backfill,来恢复整份数据。
数据恢复:在PG Peering过程中会暂停所有的IO,等Peering完成后,PG会进入Active状态,此时便可以接收数据的IO请求,然后根据Peering的信息来决定进行Recovery还是Backfill。对于Replica PG缺失的数据Primary PG会通过Push来推送,对于Primary PG自身缺少的数据会通过Pull方式从其他Replicate PG拉取。在Recovery过程中,恢复的粒度是4M对象,对于无法通过PGlog来恢复的,则进行Backfill进行数据的全量拷贝,等到数据恢复完成后,PG的状态会标记为Clean即所有副本数据保持一致。
PG的Peering是使一个PG内的所有OSD达成一致的过程,相关重要概念如下:
主要包含三个步骤:
Peering进行之后,如果Primary检测到自身或者任意一个Peer需要修复对象,则进入Recovery状态,为了影响外部IO,也会限制恢复的速度以及每个OSD上能够同时恢复的PG数量。Recovery一共有两种状态:
通常总是先执行Pull再执行Push,即先修复Primary再修复Replica,因为Primary承担了客户端的读写,需要优先进行修复,修复情况大致如下:
在恢复对象时,由于PGLog并未记录关于对象修改的详细信息(offset、length等),所以目前对象的修复都是全量对象(4M)拷贝,不过社区已经支持 部分对象修复。
同时在恢复对象时,由于ObjectStore支持覆盖写,所以在对象上新的写不能丢弃老的对象,需要等老的对象恢复完之后,才能进行该对象新的写入,不过社区已经支持 异步恢复。
Ceph提供存储功能的核心组件是RADOS集群,最终都是以对象存储的形式对外提供服务。但在底层的内部实现中,Ceph的后端存储引擎在近十年来经历了许多变化。现如今的Ceph系统中仍然提供的后端存储引擎有FileStore、BlueStore。但该三种存储引擎都是近年来才提出并设计实现的。Ceph的存储引擎也先后经历了EBOFS-->FileStore/btrfs-->FileStore/xfs-->NewStore-->BlueStore。同时Ceph需要支持文件存储,所以其存储引擎提供的接口是类POSIX的,存储引擎操作的对象也具有类似文件系统的语义,也具有其自己的元数据。
FileStore是Ceph基于文件系统的最早在生成环境比较稳定的单机存储引擎,虽然后来出现了BlueStore,但在一些场景中仍然不能代替FileStore,比如在全是HDD的场景中FileStore可以使用NVME盘做元数据和数据的读写Cache,从而加速IO,BlueStore就只能加速元数据IO。
FileStore是基于文件系统的,为了维护数据的一致性,写入之前数据会先写Journal,然后再写到文件系统,会有一倍的写放大。不过Journal也起到了随机写转换为顺序写、支持事务的作用。
引用网上图片,如有侵权,联系删除。对象的元数据使用KV形式保存,主要有两种保存方式:
有些文件系统不支持扩展属性,或者扩展属性大小有限制。一般情况下xattr保存一些比较小且经常访问的元数据,omap保存一些大的不经常访问的元数据。
同时ObjectStore使用Transaction类来实现相关的操作,将元数据和数据封装到bufferlist里面,然后写Journal。大致包含OP_TOUCH、OP_WRITE、OP_ZERO、OP_CLONE等42种 事务操作。提供的对外接口大致有:
ObjectStore本身的接口:mount、umount、fsck、repair、mkfs等。
Object本身的接口:read、write、omap、xattrs、snapshot等。
在FileStore的实现中,根据不同的日志提交方式,有两种不同的日志类型:
日志处理有三个阶段:
在机器异常宕机的情况下,Journal中的数据不一定全部都sync到了数据盘上,有可能一部分还在Pagecache,此时便需要在OSD重启时保证数据的一致性,对Journal做replay。FileStore将已经sync到数据盘的序列号记录在commit_op_seq中,replay的时候从commit_op_seq开始即可。
但是在replay的时候,部分op可能已经sync到数据盘中,但是commit_op_seq却没有体现,序列化比其小,此时如果仍然replay,可能会出现非幂等操作,导致数据不一致。
假设一个事务包含如下3个操作:
假设上述操作都做完也已经持久化到数据盘上了,然后立马进程或者系统崩溃,此时sync线程还未来得及更新commit_op_seq,重启回放时,第二次执行clone操作就会clone到a新的数据版本,就会发生不一致。
FileStore在对象的属性中记录最后操作的三元组(序列号、事务编号、OP编号),因为journal提交的时候有一个唯一的序列号,通过这个序列号, 就可以找到提交时候的事务,然后根据事务编号和OP编号最终定位出最后操作的OP。对于非幂等的操作,操作前先检查下,如果可以继续执行就执行操作,执行完之后设置一个guard。这样对于非幂等操作,如果上次执行过, 肯定是有记录的,再一次执行的时候check就会失败,就不继续执行。
Ceph早期的单机对象存储引擎是FileStore,为了维护数据的一致性,写入之前数据会先写Journal,然后再写到文件系统,会有一倍的写放大,而同时现在的文件系统一般都是日志型文件系统(ext系列、xfs),文件系统本身为了数据的一致性,也会写Journal,此时便相当于维护了两份Journal;另外FileStore是针对HDD的,并没有对SSD作优化,随着SSD的普及,针对SSD优化的单机对象存储也被提上了日程,BlueStore便由此应运而出。
BlueStore最早在Jewel版本中引入,用于在SSD上替代传统的FileStore。作为新一代的高性能对象存储后端,BlueStore在设计中便充分考虑了对SSD以及NVME的适配。针对FileStore的缺陷,BlueStore选择绕过文件系统,直接接管裸设备,直接进行对象数据IO操作,同时元数据存放在RocksDB,大大缩短了整个对象存储的IO路径。BlueStore可以理解为一个支持ACID事物型的本地日志文件系统。
BlueStore是一个事务型的本地日志文件系统。因为面向下一代全闪存阵列的设计,所以BlueStore在保证数据可靠性和一致性的前提下,需要尽可能的减小日志系统中双写带来的影响。全闪存阵列的存储介质的主要开销不再是磁盘寻址时间,而是数据传输时间。因此当一次写入的数据量超过一定规模后,写入Journal盘(SSD)的延时和直接写入数据盘(SSD)的延迟不再有明显优势,所以Journal的存在性便大大减弱了。但是要保证OverWrite(覆盖写)的数据一致性,又不得不借助于Journal,所以针对Journal设计的考量便变得尤为重要了。
一个可行的方式是使用增量日志。针对大范围的覆盖写,只在其前后非磁盘块大小对齐的部分使用Journal,即RMW,其他部分直接重定向写COW即可。
RWM(Read-Modify-Write):指当覆盖写发生时,如果本次改写的内容不足一个BlockSize,那么需要先将对应的块读上来,然后再内存中将原内容和待修改内容合并Merge,最后将新的块写到原来的位置。但是RMW也带来了两个问题: 一是需要额外的读开销; 二是如果磁盘中途掉电,会有数据损坏的风险。为此我们需要引入Journal,先将待更新数据写入Journal,然后再更新数据,最后再删除Journal对应的空间。
COW(Copy-On-Write):指当覆盖写发生时,不是更新磁盘对应位置已有的内容,而是新分配一块空间,写入本次更新的内容,然后更新对应的地址指针,最后释放原有数据对应的磁盘空间。理论上COW可以解决RMW的两个问题,但是也带来了其他的问题: 一是COW机制破坏了数据在磁盘分布的物理连续性。经过多次COW后,读数据的顺序读将会便会随机读。 二是针对小于块大小的覆盖写采用COW会得不偿失。 是因为: 一是将新的内容写入新的块后,原有的块仍然保留部分有效内容,不能释放无效空间,而且再次读的时候需要将两个块读出来做Merge操作,才能返回最终需要的数据,将大大影响读性能。 二是存储系统一般元数据越多,功能越丰富,元数据越少,功能越简单。而且任何操作必然涉及元数据,所以元数据是系统中的热点数据。COW涉及空间重分配和地址重定向,将会引入更多的元数据,进而导致系统元数据无法全部缓存在内存里面,性能会大打折扣。
基于以上设计理念,BlueStore的写策略综合运用了COW和RMW策略。 非覆盖写直接分配空间写入即可; 块大小对齐的覆盖写采用COW策略; 小于块大小的覆盖写采用RMW策略。整体架构设计如下图:
Ceph新的存储引擎BlueStore已成为默认的存储引擎,抛弃了对传统文件系统的依赖,直接管理裸设备,通过Libaio的方式进行读写。抽象出了 BlockDevice基类,提供统一的操作接口,后端对应不同的设备类型的实现(Kernel、NVME、PMEM)。
IO架构图如下所示:
BlueStore直接管理裸设备,那么必然面临着如何高效分配磁盘中的块。BlueStore支持基于Extent和基于BitMap的两种磁盘分配策略,有 BitMap分配器(基于Bitmap)和 Stupid分配器(基于Extent),原则上都是尽量顺序分配而达到顺序写。
刚开始使用的是BitMap分配器,由于性能问题又切换到了Stupid分配器。之后Igor Fedotov大神重新设计和实现了 新版本BitMap分配器,性能也比Stupid要好,默认的磁盘分配器又改回了BitMap。
新版本BitMap分配器以Tree-Like的方式组织数据结构,整体分为L0、L1、L2三层。每一层都包含了完整的磁盘空间映射,只不过是slot以及children的粒度不同,这样可以加快查找,如下图所示:
新版本Bitmap分配器分配空间的大体策略如下:
新版本Bitmap分配器整体架构设计有以下几点优势:
BlueStore直接管理裸设备,需要自行管理空间的分配和释放。Stupid和Bitmap分配器的结果是保存在内存中的,分配结果的持久化是通过FreelistManager来做的。
FreelistManager最开始有extent和bitmap两种实现,现在默认为bitmap实现,extent的实现已经废弃。空闲空间持久化到磁盘也是通过RocksDB的Batch写入的。FreelistManager将block按一定数量组成段,每个段对应一个k/v键值对,key为第一个block在磁盘物理地址空间的offset,value为段内每个block的状态,即由0/1组成的位图,1为空闲,0为使用,这样可以通过与1进行异或运算,将分配和回收空间两种操作统一起来。
RocksDB不支持对裸设备的直接操作,文件的读写必须实现rocksdb::EnvWrapper接口,RocksDB默认实现有POSIX文件系统的读写接口。而POSIX文件系统作为通用的文件系统,其很多功能对于RocksDB来说并不是必须的, 同时RocksDB文件结构层次比较简单,不需要复杂的目录树,对文件系统的使用也比较简单,只使用追加写以及顺序读随机读。为了进一步提升RocksDB的性能,需要对文件系统的功能进行裁剪,而更彻底的办法就是考虑RocksDB的场景量身定制一套本地文件系统,BlueFS也就应运而生。相对于POSIX文件系统有以下几个优点:
接口功能
RocksDB是通过BlueRocksEnv来使用BlueFS的,BlueRocksEnv实现了文件读写和目录操作,其他的都继承自rocksdb::EnvWrapper。
磁盘布局
BlueFS的数据结构比较简单,主要包含三部分,superblock、journal、data。
元数据
BlueFS元数据:主要包含:superblock、dir_map、file_map、文件到物理地址的映射关系。
文件数据:每个文件的数据在物理空间上的地址由若干个extents表:一个extent包含bdev、offset和length三个元素,bdev为设备标识,因为BlueFS将存储空间设备划分为三层:慢速(Slow)空间、高速(DB)空间、超高速(WAL),bdev即标识此extent在哪块设备上,offset表示此extent的数据在设备上的物理偏移地址,length表示该块数据的长度。
structbluefs_extent_t{uint64_toffset=0;uint32_tlength=0;uint8_tbdev;}// 一个sstable就是一个fnodestructbluefs_fnode_t{uint64_tino;uint64_tsize;utime_tmtime;uint8_tprefer_bdev;mempool::bluefs::vectorextents;uint64_tallocated;}
按照9T盘、sstable 8MB,文件元数据80B来算,所需内存 9 * 1024 * 1024 / 8 * 80 / 1024 / 1024 = 90MB,说明把元数据全部缓存到内存并不会占用过多的内存。
加载流程
读写数据
读数据:先从dir_map和file_map找到文件的fnode(包含物理的extent),然后从对应设备的物理地址读取即可。
写数据:BlueFS只提供append操作,所有文件都是追加写入。RocksDB调用完append以后,数据并未真正落盘,而是先缓存在内存当中,只有调用sync接口时才会真正落盘。
第3步是写数据、第4步是写元数据,都涉及到sync落盘,整体一个文件的写入需要两次sync,已经算是很不错了。
BlueStore中的对象非常类似于文件系统中的文件,每个对象在BlueStore中拥有唯一的ID、大小、从0开始逻辑编址、支持扩展属性等,因此对象的组织形式,类似于文件也是基于Extent。
BlueStore的每个对象对应一个Onode结构体,每个Onode包含一张extent-map,extent-map包含多个extent(lextent即逻辑的extent),每个extent负责管理对象内的一个逻辑段数据并且关联一个Blob,Blob包含多个pextent(物理的extent,对应磁盘上的一段连续地址空间的数据),最终将对象的数据映射到磁盘上。具体可参考 BlueStore源码分析之对象IO和 BlueStore源码分析之事物状态机。
BlueStore中磁盘的最小分配单元是min_alloc_size,HDD默认64K,SSD默认16K,里面有2种磁盘分配的写类型(分配磁盘空间,数据还在内存):
真正写磁盘时,有两种不同的写类型:
1、simple-write:包含对齐覆盖写(COW)和非覆盖写,先把数据写入新的磁盘block,然后更新RocksDB里面的KV元数据,状态转换图如下:
这图话的比较好,拿过来直接用了,如有侵权,联系删除。2、deferred-write:为非对齐覆盖写,先把数据作为WAL写RocksDB即先写日志,然后会进行RMW操作写数据到磁盘,最后CleanupRocksDB中的deferred-write的数据。
这图话的比较好,拿过来直接用了,如有侵权,联系删除。3、simple-write + deferred-write:上层的一次IO很有可能同时涉及到simple-write和deferred-write,其状态机就是上面两个加起来,只不过少了deferred-write的写WAL一步,因为可以在simple-write写元数据时就一同把WAL写入RocksDB。
随着硬件的不断发展,IO的速度越来越快,PMEM和NVME也逐渐成为了存储系统的主流选择,相比之下CPU的速度没有那么快了,反而甚至成为了系统的瓶颈。如何高效合理的利用新型硬件是分布式存储不得不面临的一个重大问题。Ceph传统的线程模型是多线程+队列的模型,一个IO从发起到完成要经历重重队列和不同的线程池,锁竞争、上下文切换和Cache Miss比较严重,也导致IO延迟迟迟降不下来。通过Perf发现CPU主要都耗在了锁竞争和系统调用上,Ceph自身的序列化和反序列化也比较消耗CPU,所以需要一套新的编程框架来解决上述问题。Seastar是一套基于future-promsie现代化高效的share-nothing的网络编程框架,从18年开始,Ceph社区便基于Seastar来重构整个OSD,项目代号 Crimson,来更好的解决上述问题。
Crimson设计目标
线程模型
性能对比
测试RBD时,在达到同等iops和延迟时,crimson-osd的cpu比ceph-osd的cpu少了好几倍。
BlueStore适配
BlueStore目前是Ceph里性能比较高的单机存储引擎,从设计研发到稳定差不多持续了3年时间,足以说明研发一个单机存储引擎的时间成本是比较高的。由于BlueStore不符合Seastar的编程模型,所以需要对BlueStore适配,目前有两种方案:
但是由于RocksDB有自己的线程模型,外部不可控,所以无论怎么适配都不是最好的方案,理论上从0开始用基于Seastar的模型来写一个单机存储引擎是最完美的方案,于是便有了SeaStore,而BlueStore的适配也作为中间过渡方案,最多可用于HDD。
SeaStore
SeaStore是下一代的ObjectStore,适用于Crimson的后端存储,专门为了NVME设计,使用SPDK访问,同时由于Flash设备的特性,重写时必须先要进行擦除操作,也就是内部需要做GC,是不可控的,所以Ceph希望把Flash的GC提到SeaStore中来做: