Ceph-OSD数据读写流程分析
Ceph-OSD数据读写流程分析
一、版本
Ceph版本号nautilus v14.2.9
二、读写框架
客户端通过librbd,librados库访问RBD块设备,这个块设备会被切块,每块大小默认4MB,编号以后,通过2次映射,第一次映射通过伪随机hash算法,输入(pool id, object id),计算出映射到的pg id,这一步保证对象均匀分布在所有PG之间,第二次映射实现PG到OSD的映射,仍然采用伪随机hash算法保证PG均匀分布到OSD之间,当然前提是还要满足CRUSH规则,输入(crush rule id, pg id)输出PG应该存放到哪些OSD上,这一步是客户端通过CRUSH算法查找得到,因为PG与OSD之间的关系是在创建pool的时候已经确定好了,至于客户端的cluster map,是从MON获取到的。客户端找到每个object应该存放的主OSD之后,直接与它通信,如果是读数据,直接发送消息就可以拿到数据了,如果是写,先将数据发给主OSD,由主OSD再将数据发给其它从OSD,主从OSD都写完数据后返回给客户端。
2.1读流程
读写走的是不同流程,对于读操作:
- 客户端直接计算出存储数据所属主osd,直接给主osd发送消息。
- 主osd收到消息后,可以调用Filestore/Bluestore直接读取处在磁盘的主pg里面的内容然后返回给客户端。具体调用函数在ReplicatedBackend:: do_repop中实现
2.2写流程
客户端可以使用kernel rbd和librbd两种方式读写,应用写入rbd块设备的过程:
- 应用调用 librbd 接口或者对linux 内核虚拟块设备写入二进制块。下面以 librbd 为例。
- librbd 对二进制块进行分块,默认块大小为 4M,每一块都有名字,成为一个对象
- librbd 调用 librados 将对象写入 Ceph 集群
- librados 向主 OSD 写入分好块的二进制数据块 (先建立TCP/IP连接,然后发送消息给 OSD,OSD 接收后写入其磁盘)
- 主 OSD 负责同时向一个或者多个次 OSD 写入副本。
- 当主次OSD都写入完成后,主 OSD 向客户端返回写入成功。
- 当OSD的数据向磁盘写入成功后,Ceph通过事件通知客户端数据写入磁盘成功(commit),此时,客户端可以将写缓存中的数据彻底清除掉了。
- 默认地,Ceph 客户端会缓存写入的数据直到收到集群的commit通知。如果此阶段内(在写方法返回到收到commit通知之间)OSD 出故障导致数据写入文件系统失败,Ceph 将会允许客户端重做尚未提交的操作(replay)。因此,PG 有个状态叫 replay:“The placement group is waiting for clients to replay operations after an OSD crashed.”
该过程具有强一致性的特点:
- Ceph 的读写操作采用 Primary-Replica 模型,Client 只向 Object 所对应 OSD set 的 Primary 发起读写请求,这保证了数据的强一致性。
- 由于每个 Object 都只有一个 Primary OSD,因此对 Object 的更新都是顺序的,不存在同步问题。
- 当 Primary 收到 Object 的写请求时,它负责把数据发送给其他 Replicas,只要这个数据被保存在所有的OSD上时,Primary 才应答Object的写请求,这保证了副本的一致性。这也带来一些副作用。相比那些只实现了最终一致性的存储系统比如 Swift,Ceph 只有三份拷贝都写入完成后才算写入完成,这在出现磁盘损坏时会出现写延迟增加。
- 在 OSD 上,在收到数据存放指令后,它会产生2~3个磁盘seek操作:
- 把写操作记录到 OSD 的 Journal 文件上(Journal是为了保证写操作的原子性,Bluestore使用rocksdb提供事务,不再额外使用journal)。
- 把写操作更新到 Object 对应的文件上。
- 把写操作记录到 PG Log 文件上
几个比例关系:
文件:对象 = 1:n(由客户端实时计算)
Object:PG = n:1(由客户端使用哈希算法计算)
PG:OSD = m:n(由 MON 根据 CRUSH 算法计算)
CRUSH算法是相当复杂,快速看看的话可以参考官方文章,或者直接读代码和作者的论文。几个简单的结论或原则:
- 一个 RBD image(比如虚机的一个镜像文件)会分成几个 data objects 保存在 Ceph 对象存储中。
- 一个 Ceph 集群含有多个 pool (使用 ceph osd pool create 命令创建pool)
- 一个 Pool 包含若干个 PG (在创建 pool 时必须指定 pg_num,在需要的时候对已有的pool的 pg_num 也可以进行修改)
- 一个 PG 可以包含多个对象
- 一个 object 只在一个 PG 中
- 一个 PG 映射到一组 OSD,其中第一个 OSD 是主(primary),其余的是从(secondary)
- 许多 PG 可以映射到某个 OSD,通常一个OSD上会有50到100个PG。
三、IO路径源码分析
3.1 rbd客户端处理流程
- 在rbd中一个操作对象为一个image,这个请求经过处理拆分成object对象的请求,拆分后交给Objector进行处理,找到目标osd的集合及主osd。
- 将请求封装成MOSDOp消息,交由Messenger处理, Messager会尝试查找 / 创建一个OSDSession,并且为这个OSDSession创建一个数据通道pipe。消息从Messager收到后会保存到pipe的outq队列中。
- pipe 与目标osd建立Socket通信通道,并有专门的写线程writer来负责socket通信。writer线程同时监视这个outq队列,当队列中存在消息等待发送时,会就将消息写入socket,发送给目标OSD。
- OSD将数据消息处理完成之后,进行回调,反馈执行结果
主要流程见下图:
原图链接:https://www.processon.com/view/link/5c36a87ae4b0db2e592e8c1d
3.2 OSD处理流程
- OSD接收到message信息以后,解析并将消息转换成OpRequest,加入dispatch队列。同时从message中得到pool及PG的信息,将message交由PG进行相关处理。此时从OSD处理的message转化为了PG处理的op,添加到osd->op_wq队列中。PG中创建一个OpContext结构,接管message中的所有ops的操作。
- 解析出OpRequest中的所有op与data,用Transaction结构进行管理,即将ops与Transaction绑定,将操作和数据打包成事务,并发送给其他副本,同时管理自己和其他副本的数据处理统计,创建repop 所有的applied与commit的管理。
- 提交事务后,交由filestore/bluestore进行处理。
- 如果数据处理完成了,使用eval_repop()进行收尾的工作,将结果回调给客户端。
3.2.1OSD读流程
3.2.1OSD写流程
写数据I/O路径
写数据耗时统计
四、blueStore
核心模块
BlockDevice:物理块设备,使用Libaio操作裸设备,AsyncIO。
RocksDB:存储WAL、对象元数据、对象扩展属性Omap、磁盘分配器元数据。
BlueRocksEnv:抛弃了传统文件系统,封装RocksDB文件操作的接口。
BlueFS:小型的Append文件系统,实现了RocksDB::Env接口,给RocksDB用。
Allocator:磁盘分配器,负责高效的分配磁盘空间。
BlueStore便是一个事务型的本地日志文件系统。因为面向下一代全闪存阵列的设计,所以BlueStore在保证数据可靠性和一致性的前提下,需要尽可能的减小日志系统中双写带来的影响。
全闪存阵列的存储介质的主要开销不再是磁盘寻址时间,而是数据传输时间。因此当一次写入的数据量超过一定规模后,写入Journal盘(SSD)的延时和直接写入数据盘(SSD)的延迟不再有明显优势,
所以Journal的存在性便大大减弱了。但是要保证OverWrite(覆盖写)的数据一致性,又不得不借助于Journal,所以针对Journal设计的考量便变得尤为重要了。
一个可行的方式是使用增量日志。针对大范围的覆盖写,只在其前后非磁盘块大小对齐的部分使用Journal,即RMW,其他部分直接重定向写COW即可。
BlockSize:磁盘IO操作的最小单元(原子操作)。HDD为512B,SSD为4K。即读写的数据就算少于BlockSize,磁盘IO的大小也是BlockSize,是原子操作,要么写入成功,要么写入失败,即使掉电不会存在部分写入的情况。
RWM(Read-Modify-Write):指当覆盖写发生时,如果本次改写的内容不足一个BlockSize,那么需要先将对应的块读上来,然后再内存中将原内容和待修改内容合并Merge,最后将新的块写到原来的位置。但是RMW也带来了两个问题:一是需要额外的读开销;二是RMW不是原子操作,如果磁盘中途掉电,会有数据损坏的风险。为此我们需要引入Journal,先将待更新数据写入Journal,然后再更新数据,最后再删除Journal对应的空间。
COW(Copy-On-Write):指当覆盖写发生时,不是更新磁盘对应位置已有的内容,而是新分配一块空间,写入本次更新的内容,然后更新对应的地址指针,最后释放原有数据对应的磁盘空间。理论上COW可以解决RMW的两个问题,但是也带来了其他的问题:一是COW机制破坏了数据在磁盘分布的物理连续性。经过多次COW后,读数据的顺序读将会便会随机读。二是针对小于块大小的覆盖写采用COW会得不偿失。是因为:一是将新的内容写入新的块后,原有的块仍然保留部分有效内容,不能释放无效空间,而且再次读的时候需要将两个块读出来做Merge操作,才能返回最终需要的数据,将大大影响读性能。二是存储系统一般元数据越多,功能越丰富,元数据越少,功能越简单。而且任何操作必然涉及元数据,所以元数据是系统中的热点数据。COW涉及空间重分配和地址重定向,将会引入更多的元数据,进而导致系统元数据无法全部缓存在内存里面,性能会大打折扣
整体架构基于以上设计理念,BlueStore的写策略综合运用了COW和RMW策略。非覆盖写直接分配空间写入即可;块大小对齐的覆盖写采用COW策略;小于块大小的覆盖写采用RMW策略。
4.1BlueFS
RocksDB是基于本地文件系统的,但是文件系统的许多功能对于RocksDB不是必须的,所以为了提升RocksDB的性能,需要对本地文件系统进行裁剪。最直接的办法便是为RocksDB量身定制一套本地文件系统,BlueFS便应运而生。
BlueFS是个简易的用户态日志型文件系统,恰到好处的实现了RocksDB::Env所有接口。根据设计理念这一章节,我们知道引入Journal是为了进行写加速,WAL对于提升RocksDB的性能至关重要,所以BlueFS在设计上支持把.log和.sst分开存储,.log使用速度更快的存储介质(NVME等)
在引入BlueFS后,BlueStore将所有存储空间从逻辑上分了3个层次:
- 慢速空间(Block):存储对象数据,可以使用HDD,由BlueStore管理。
- 高速空间(DB):存储RocksDB的sst文件,可以使用SSD,由BlueFS管理。
- 超高速空间(WAL):存储RocksDB的log文件,可以使用NVME,由BlueFS管理。
BlueFS的结构如下图所示,
主要有三部分数据,superblock、journal、以及data。superblock主要存放BlueFS的全局信息以及日志的信息,其位置固定在BlueFS的头部;
journal中存放日志记录,一般会预分配一块连续区域,写满以后从剩余空间再进行分配;
data为实际的文件数据存放区域,每次写入时从剩余空间分配一块区域。
- superblock
superblock记录文件系统的全局信息,也是文件系统加载的入口,其位置固定存放于BlueFS的第二个block(第一个block保留不是很清楚作用),superblock由以下内容组成:
- uuid 表示BlueFS的全局唯一编号,区别于其他BlueFS;
- osd_uuid拥有此BlueFS的OSD的全局唯一编号,识别该BlueFS所属的OSD;
- version BlueFS的版本号,当且仅当日志进行压缩的时候递增,可通过版本号判断BlueFS进行日志压缩的次数;
- block_size BlueFS中的块大小,即每次读写的最小单位
log_fnode BlueFS中日志的fnode结构,fnode类似于Linux文件系统中inode的结构,表示一个文件的元数据信息以及数据存放位置。在fnode中,记录有文件的ino编号、文件的大小、文件修改的时间、文件的extent集合(一个extent表示文件的逻辑地址到底层物理空间的映射,这里extent包含物理偏移、区块长度以及设备标识)以及文件已分配空间的大小;写数据时,BlueFS会预分配一段空间,通过文件当前大小和已分配空间大小,可以判断新写入的数据是否需要分配新的存储空间。
superblock在BlueFS格式化的时候生成,仅当文件系统格式化时或日志压缩完成时持久化到磁盘。BlueFS在初始化时,从superblock中获取日志的fnode,找到日志的位置,然后逐条将日志中的记录回放到内存中,来还原整个BlueFS的元数据
-
journal
BlueFS的元数据不像传统文件系统一样,用特定的数据结构和布局存放,而是通过将所有的操作记录到journal中,然后在加载的时候逐条回放journal中的记录,从而将元数据加载到内存。
journal实际上是一种特殊的文件,其fnode记录在superblock中,而其他文件的fnode作为日志内容记录在journal文件中。
journal是由一条条的事务组成,如下图所示,每条事务包含uuid、seq、op_bl三部分,其作用分别为:
- uuid表示事务归属的bluefs对应的uuid
- seq事务的全局唯一序列号
- op_bl编码后的事务条目,可包含多条记录,每条记录由相关的操作码和操作所涉及的相关数据组成。
- crc在将事务写到日志文件之前,会对op_bl计算crc校验值,该值用于校验该事务的正确性,例如在写日志的中途出现异常掉电的情况,那么下载加载BlueFS时,回放到该条事务时会校验crc,如果上次日志未写完crc校验会失败,从而提示错误
- Metadata
BlueFS的metadata全部加载在内存当中,主要包含superblock、目录和文件的集合、文件跟目录的映射关系以及文件到物理地址的映射关系;当下次文件系统加载时,将日志中的记录逐条回放到内存,从而还原出metadata。
其中superblock的结构可见上面superblock小节,文件和目录的内存逻辑结构如下图所示,BlueFS的内存中维护一个dir_map,记录BlueFS中的所有目录的目录名到实际目录结构的映射,目录为扁平结构,不存在隶属关系,例如目录/a/b与目录/a在同一级,/a/b即为目录名;每个目录结构下包含一个file_map,记录目录下的所有文件的文件名到实际文件结构的映射,每个文件结构中包含该文件的fnode结构,fnode结构可参考superblock小节中的log_fnode结构。
BlueFS在加载时,会根据日志中记录的内容,在metadata的dir_map和file_map中添加、删除或修改相应的条目;在BlueFS的使用过程中,也会不断变更此映射结构;BlueFS在定位一个具体的文件时会在内存中经过两次查找:第一次通过dir_map找到文件所在的最底层文件夹,第二次通过该文件夹下的file_map找到对应的文件。
4.2 Block Device
- 以BlockDevice为基类,抽象出了不同类型的Device,包括KernelDevice、NVMEDevice和PMEMDevice等。
- Device的使用者通过设备提供的create和open接口初始化设备。
- Device提供同步和异步的读写接口,以及flush等操作保证数据落盘。
- 异步写操作通过aio_write和aio_submit接口完成,Device的aio相关的线程会在io执行完成后,执行回调函数通知调用者
4.3 数据压缩
4.3.1Blue Store压缩策略
数据压缩针对原始数据执行转换,以期得到长度更短的输出,目的是节省存储空间。与数据校验不同,数据压缩的转换过程必须是可逆的,即采用输出作为输入,反过来可以还原得到原始数据,后面过程称为解压。
常见的压缩算法大多是基于模式匹配,因此一般而言,其过程迭代次数决定了压缩收益,这意味着对于大多数压缩算法而言,更高的压缩比几乎总是对应更多的性能损耗(因为需要更多的迭代次数,即更长的压缩时间)。因此实际选用压缩算法时,同样需要考虑在这两者(压缩收益与执行效率)之间进行权衡。
采用数据压缩算法需要固化两个关键信息:一是选用的压缩算法;二是压缩后的数据长度。BlueStore使用压缩头保存这两个信息
成员 |
含义 |
type |
压缩算法类型 |
length |
数据压缩后的长度 |
4.3.2Blue Store压缩用法
用法:
ceph osd pool set <pool-name> compression_algorithm <algorithm> 压缩算法
ceph osd pool set <pool-name> compression_mode <mode> 压缩模式
ceph osd pool set <pool-name> compression_required_ratio <ratio> 压缩比率
ceph osd pool set <pool-name> compression_min_blob_size <size> 最小blob大小
ceph osd pool set <pool-name> compression_max_blob_size <size>最大blob大小
五、其他资料
Ceph之RADOS设计原理与实现
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步