分布式存储ceph:可靠性设计

Ceph使用CRUSH算法实现了去中心化,Ceph客户端和OSD守护进程都使用CRUSH算法来有效地计算出对象存储的位置,而不是基于一个中心化的查找表格,而且随着OSD守护进程拓扑的变化,比如有新节点加入或旧节点退出,CRUSH算法会重新在OSD上分布数据,不会由于单个OSD的故障而影响整个集群的存储。

Ceph还利用Monitor集群来保证集群结构的高可靠性,Ceph客户端在读/写数据之前,必须与Monitor通信来获得最新的集群运行图的副本。原则上来说,一个Ceph的存储集群可以只使用一个Monitor,然而这就相当于引入了单点故障,如果这个单一的Monitor宕机,则Ceph客户端将无法正常读/写数据。因此为了保证Ceph的高可靠性和容错性,Ceph支持Monitor集群来消除该单点故障,当一个Monitor失效时,由其他Monitor来接管。

Ceph作为一个分布式存储系统,基于分布式算法和集群的概念,从架构上保证了去中心化并消除单点故障。

除了在架构上保证消除集群中的单点故障,Ceph还要保证其集群中存储数据的高可靠性。保证数据的高可靠性主要有两种方式:数据冗余备份和纠删码机制。在Ceph中数据的冗余备份又分为两种:一种是RBD的冗余备份,另一种是OSD的备份机制。RBD的冗余备份包括如下内容。

· RBD mirror,即在两个独立的Ceph集群间建立实时的数据镜像备份。

· RBD Snapshot,即RBD的快照,通过设置快照将增量数据定期备份到灾备中心,在灾难发生时,可以将数据文件回滚到某个快照状态。

RBD数据的镜像和快照能成功恢复的前提是后端,即OSD端存储系统是正常的,而一旦存储系统被破坏了,镜像会丢失,快照也会失效,因此OSD端也有数据冗余的办法:副本和纠删码。OSD端以备份副本和纠删码的方式来保证数据冗余,一旦发生数据损坏可以从剩下的数据中恢复完全的数据。

OSD多副本

传统存储的可靠性都是依靠多副本实现的,Ceph也不例外。用户给定一份数据,Ceph在后台自动存储多个副本(一般使用3个副本),从而保证在硬盘损坏、服务器故障、机柜停电等情况下,数据不会丢失,甚至数据仍能保持在线。Ceph需要做的是及时进行故障恢复,将丢失的数据副本补全,以维持数据的高可靠性。

用户可以配置每个存储池的多副本策略,即每个存储池可以有各自的副本数量。而且Ceph提供的是强一致性副本策略,只有当数据的多个副本都写入之后,OSD才告知给客户端写入完成。但强一致性副本策略也导致了延时的增加,因此副本数量不宜过多,基本上也不会异地多中心存储副本。副本定义的数量也就决定了只要超过副本数量的服务器宕机,整个存储系统就面临部分数据不可用的情况。

Ceph OSD守护进程也像Ceph客户端一样使用CRUSH算法,OSD使用CRUSH算法计算对象副本所存储的OSD并进行重新平衡。在一个典型的写情境下,Ceph客户端使用CRUSH算法计算对象应该存储在哪里,映射对象到一个存储池和PG中,然后查找CRUSH Map来确定该PG的主OSD。

客户端把对象写入相应的主OSD的PG后,该主OSD通过自身带有的CRUSH Map来确定副本所在的第二和第三OSD,并且将对象写入相应的第二和第三OSD的PG中,一旦确定对象及其副本已经成功存储之后就发送响应给客户端。

基于Ceph这种本身所具有的多副本能力,Ceph OSD进程代替Ceph客户端承担了提高数据高可靠性和高安全性的责任。

 

OSD纠删码

Ceph的多副本存储可以保证数据的高可靠性,但是针对大容量存储场景会耗费大量的存储空间,从而增加存储成本。针对这个问题,纠删码是通用的解决方案,尤其是一次写多次读的场景,比如镜像文件、多媒体影像等。纠删码是一种编码技术,用1.5副本就可以实现丢失任意两块数据都可以恢复出原始数据的效果,但纠删码的缺点是修复数据的代价太大。

副本策略和编码策略是保证数据冗余度的两个重要方法,纠删码属于编码策略的一种。虽然编码策略比副本策略具有更高的计算开销而且修复需要一定的时间,但其能极大减少存储开销的优势还是为自己赢得了巨大的空间。实际中,副本策略和编码策略也往往共存于一个存储系统中,比如在分布式存储系统中热数据往往通过副本策略保存,冷数据则通过编码策略保存,以节省存储空间。比如在上述Cache Tiering的实现里,Storage层可以使用纠删码提高存储容量,而缓存层使用多副本解决纠删码引起的速度降低问题。

比如在以磁盘为单位的存储设备的存储系统中,假设磁盘总数为n,编码策略通过编码k个数据盘,得到m个校验盘(n=k+m),保证丢失若干个磁盘(不超过m个)可以恢复出丢失磁盘数据。磁盘可以推广为数据块或任意存储节点。

Ceph纠删码常用的编码是RS (k, m),k块数据块,编码为m块校验块,可以容忍任意m块丢失。所以,为了保证数据一致性,纠删码的写操作需要至少完成k块才算写成功。Ceph为保证这样的一致性,引入了回滚机制,任意操作都是可回滚的,保证在出错时,能够恢复为上一个完整的版本。

Ceph默认的纠删码库是Jerasure,Jerasure库是第三方提供的中间件。在Ceph环境安装时,已经默认安装了Jerasure库。

纠删码提供了和多副本近似的可靠性,同时减少了额外所需的冗余设备,从而提高了存储设备的利用率。但纠删码带来了计算量和网络负载等额外的负担,数据的重建非常耗费CPU资源,重建一个数据块需要通过网络读取多倍的数据并进行传输,网络负载也有数倍甚至数十倍的增加。因此纠删码适用的场景主要是镜像等冷数据的存储,而热数据可以通过缓存层使用快速设备去存储。

整体来看,若采用纠删码技术,则需要在容错能力、存储资源利用率和数据重建所需的代价之间做一个平衡。

 

RBD mirror

Ceph在保障数据安全这方面做了非常多的工作。例如,在保存比特数据和在网络传输时采用了CRC进行数据的完整性检查;在数据落盘时使用日志来应对系统意外崩溃或硬件发生故障时可能出现的数据损坏;在RADOS对象层使用了CRUSH算法让数据能够在集群内进行自动复制;或者使用纠删码来实现对RADOS对象数据的冗余保存;在PG层使用了PG日志来应对副本之间的数据同步和恢复问题。同时,Ceph将数据副本分布到不同的硬件,从而有效地避免了由于硬件故障而导致数据丢失的问题,从而实现数据的高可用性。

但是,这些都不能应对整个集群失效的情形,Ceph需要有自己的灾备方案来实现不同集群间的数据备份。因此,Ceph从Jewel版本开始引入了RBD mirror功能,为集群间的异地容灾和高可用性提供了较为可靠的解决方案。

由于集群间的距离更远,网络延迟大大增加,若仍使用集群内部的强一致性同步模型,主集群就需要等待所有备份完成更新才能继续执行写操作,这将会影响主集群的写性能。所以,集群间的镜像基于日志实现了最终一致性模型,它提供了基于时间点的故障恢复机制,这也大大降低了集群间延迟对主集群读/写性能的影响。在主集群内部,对于所有开启了RBD日志的存储池或Image,它们的写操作会首先以日志形式保存在RADOS对象中,再根据CRUSH算法将数据真正保存到本地集群中。由于写日志的创建是严格有序的,远端集群可以读取主集群的写日志,再按顺序将修改重放到备份集群中。这样,一旦主集群发生失效,备份集群能有一份完整有效的最近数据,尽可能降低数据丢失的风险。而重放过的日志是可以被安全删除的,这样又能不断释放空间来保存更新的日志。

除了集群的日志机制,Ceph还需要启动rbd-mirror服务来管理、同步镜像数据和连接镜像集群。它负责从远端集群拉取日志更新,并应用到当前集群中。由于RADOS实现了对象的watch/notify操作,rbd-mirror可基于此获得远端镜像的状态变化和镜像日志更新的通知。在为RBD Image启动镜像功能时,主集群会在本地为该Image创建一个内部快照,并开始记录日志。当远端rbd-mirror服务监听到Image状态的变化时,它就会建立对应的Image镜像,开始读取并重放日志。

如下图所示,当客户端执行I/O write操作时,首先写入RBD Image的journal,一旦写入journal完成就会向客户端发送ACK确认,然后OSD开始执行底层RBD Image的写入,同时rbd-mirror根据journal内容进行回放操作,同步到远端的Ceph集群中。

目前RBD mirror已经实现了多种常用的功能,例如可以选择对Image或存储池进行镜像,并支持两个集群间的相互备份,同时提供为已有的Image或存储池开启或关闭镜像功能。

RBD mirror备份模式中最成熟的是active/standby,即主集群负责读/写任务,从集群只负责镜像同步和读操作。用户可以通过命令降级(Demote)主集群,并提升从集群来切换同伴集群内的主从角色。在发生主集群失效时,用户也能操作从集群承担读/写任务,并回滚失效的主集群来重新达到一致的状态。

最新的mimic版本还支持active/active模式(或叫作two-way mirroring),即不限制镜像集群间的主从角色,它通过现有的exclusive-lock机制来保证同一时刻只有获得锁的Image能够产生写操作,从而阻止同伴集群间写操作的竞争情况。

为了降低误操作带来的风险,rbd-mirror还提供延时镜像更新和延时删除功能,给误修改和误删除带来一定的反应时间。

RBD mirror目前仍然存在一定的局限性,例如镜像带来的性能开销仍然比较高,不支持多于两个集群的镜像功能,以及集群间镜像数据传输的带宽限制等,这将在未来得到解决。同时,社区也在持续倾听用户的使用反馈,从而不断提升镜像功能在不同场景中的易用性。

 

RBD Snapshot

全球网络存储工业协会(Storage Networking Industry Association,SNIA)对快照的定义是:关于指定数据集合的一个完全可用拷贝,该拷贝包括相应数据在某个时间点(拷贝开始的时间点)的映像。快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。

简单来说,快照的功能一般就是基于时间点做一个标记,然后在某些需要的时候,将状态恢复到标记的那个点。

1.快照模式

Ceph有两种快照模式,内容如下。

· Pool Snapshot:存储池级别的快照,是给整个存储池做一个快照,该存储池中所有的对象都统一处理。存储池是Ceph中逻辑上的隔离单位,每个存储池都可以有自己不同的数据处理方式。

· Self Managed Snapshot:由用户管理的快照,即存储池中受影响的对象是受用户控制的。这里的用户是指librbd之类的应用。利用rbd Snapshot命令进行的操作实质上就是这种模式。

两种快照模式不能同时存在。一旦一个存储池创建了RBD Image,就认为该存储池处于Self Managed Snapshot模式,不能再对存储池整体创建快照;反之对一个存储池整体创建了快照,就不能再在其中创建RBD Image了。

2.RBD快照的导出和恢复

存储池级别的快照与RBD的快照实现原理基本相同,这里只以RBD快照为例。

在Ceph中,由Image提供块存储功能,Image对应着卷的概念,一个Image就是一个卷。快照是对应某个Image在某一指定时刻的只读副本,用户可以创建Image的一系列快照,来记录Image的数据变化或状态变化。

创建好快照之后,不管我们对Image做什么改变,从快照中读到的数据都是创建快照时Image上的数据。如果Image在之后因被改变而导致数据损坏,则可以用快照去恢复Image,这就是所谓的回滚。

3.写时复制

可以做这样一个实验,创建一个Image,写入4MB的数据,然后创建一个快照,可以看到该存储池中并没有创建新的对象,也就是说这时候Ceph并没有给该快照分配存储空间来创建对象。再向该Image中写入4MB的数据覆盖刚才写入的数据,此时才发现数据目录中多了一个4MB的文件。

由此可见,Ceph使用了写时复制方式实现快照:先复制出原数据对象中的数据生成快照对象,然后对原数据对象进行写入。快照操作的粒度并不是整个Image,而是数据对象。

这种方式又称为第一次写时复制(Copy On First Write,COFW),即在数据第一次写入某个存储位置时,先将原有的内容复制到另一个位置(为快照保留的存储空间,可以称为快照空间),再将数据写入存储设备中。而下次针对同一位置的写操作将不再触发写时复制。

从第一次写时复制的执行过程看,这种实现方式在第一次写入某个存储位置时需要完成一个读操作(读原位置的数据)、两个写操作(写原位置与写快照空间),对于写入频繁的场景,这种方式将非常消耗I/O资源。因此这种实现比较适合I/O以读操作为主的场景。此外,如果只需要针对某个有限范围内的数据进行写操作,那么这种写时复制的快照实现也是比较理想的选择,因为数据更改都局限在一个范围内,对同一份数据的多次写操作只会出现一次写时复制操作。

4.克隆

上述快照是只读备份,多个快照或Image本身指向同一份数据。Ceph也允许用户创建写时复制快照的数据备份,也就是克隆,这些克隆是可写的。

创建克隆就是将Image的某一个快照复制变成一个Image。比如imageA有一个快照Snapshot-1,克隆就是根据imageA的Snapshot-1克隆得到imageB,imageB此时的状态与Snapshot-1完全一致,区别是imageB此时可写,并且拥有Image的相应能力。

从用户角度来看,一个克隆和其他RBD Image没有区别,同样可以对它做快照、读/写、改变大小等操作,唯一的要求是,因为克隆是基于快照来创建的,所以在创建之前需要对该快照做好备份,以免快照被删除。克隆创建过程如下图所示。

克隆只能基于快照创建,而且也是使用写时复制的方式实现的,通常将需要被克隆的快照称为Parent,每个被克隆出来的子Image都保留对Parent的引用,用以读取原始数据。

从克隆读数据,本质上是从克隆出来的RBD Image中读数据,对于不是它自己的数据对象,Ceph会从它的Parent上读,如果Parent上也没有,就继续找Parent的Parent,直到获取到需要的数据对象。

对于向克隆中的对象写数据,Ceph会首先检查该克隆上的数据对象是否存在。如果不存在,则从Parent上复制该数据对象,再执行数据写入操作。

随着快照和克隆的创建,克隆对Parent的引用链可能会变得很长。Ceph提供了flattern方法将克隆与Parent共享的数据对象复制到克隆,并删除父子关系,之后,克隆与原来的Parent之间也不再有关系,真正成为一个独立的Image。不过这是一个非常耗时的操作。

 

Ceph数据恢复

Ceph的高可靠性还体现在当集群系统出现故障时,能够及时地处理故障,以及能够对受影响的数据进行有效的恢复。

Ceph的故障处理有以下3个步骤。

· 感知集群故障:Ceph通过集群系统的心跳机制可以感知整个集群中各个节点的状态,判断哪些节点失效,这些失效的节点又影响了哪些PG的数据副本。

· 确定受故障影响的数据:Ceph会根据新的集群节点状态计算和判断副本缺失的数据。

· 数据恢复:无论是故障节点重启还是故障节点永久失效,都要恢复受影响的数据副本。1.感知集群状态

Ceph集群可以分为MON集群和OSD集群两大部分。其中MON集群由奇数个Monitor节点组成,这些Monitor节点通过前面提到的Paxos算法组成了一个决策者集群,对关键的集群事件,比如“OSD节点离开”和“OSD节点加入”,做出决策和广播。

OSD集群节点的状态信息存放在OSDMap中,由MON集群统一管理,OSD节点定期向MON和对等OSD发送心跳信息,声明自己处于在线状态,如下图所示。MON接收到来自OSD的心跳消息后确认OSD在线;同时其他对等OSD也会根据心跳状态向MON报告该OSD是否发生故障。MON根据心跳是否正常和其他故障报告判定OSD是否在线,同时更新OSDMap并向各个节点报告最新集群状态。比如某台服务器宕机,其上的OSD节点和MON集群的心跳超时,或者这些OSD的对等OSD发送的信息失败次数超过阈值后,这些OSD将被MON集群判定为离线。

MON集群判定某个OSD节点离线后,会将最新的OSDMap通过消息机制随机分发给每一个OSD,客户端和对等OSD处理I/O请求的时候发现自身的OSDMap版本过低,会向MON请求最新的OSDMap。经过一段时间的传播,最终整个集群的OSD都会接收到OSDMap的更新。

确定需要恢复的数据

如前所述,Ceph OSD的任何操作都会写一条记录。对数据对象的维护根据PG划分,每个OSD管理一定数量的PG,客户端对数据对象的I/O请求,会由CRUSH算法均匀分布在各个PG中,PG中维护了一份pglog,用来记录对该PG中数据对象的操作,这些记录最终会被持久化记录到后端存储中。

pglog每条记录包含了操作的对象信息和PG的版本号,pglog主要是用来记录进行了什么操作的,比如修改、删除等,每次对数据对象的操作都会使版本号递增。Ceph使用版本控制的方式来标记一个PG内的每一次更新,每个版本包括一个epoch,version:其中epoch是OSDMap的版本号,每当有OSD状态发生变化,如进行增加、删除等操作时,epoch就会递增;version是PG内每次更新操作的版本号,此版本号是递增的,由PG内的主OSD进行分配。

在默认情况下,pglog保存3000条记录,会定期对超限的pglog进行清理操作。每个副本上都维护了pglog,pglog里最重要的两个指针就是last_complete和last_update,在正常情况下,每个副本上这两个指针都指向同一个位置,但当出现机器重启、网络中断等故障时,故障副本的这两个指针就会有所区别,以便于来记录副本间的差异。在正常集群状态情况下,同一个PG的不同副本中的pglog是一致的,但是,一旦发生故障,不同副本间的pglog很可能会处于不一致的状态。

Ceph在进行故障恢复的时候要进行peering,peering是以PG为单位进行的,在peering的过程中,PG会根据pglog检查多个副本的一致性,即对比各个副本上的pglog,并尝试计算PG的不同副本的数据缺失数目,根据各个副本上pglog的差异来构造缺失的记录数目,最后得到一份完整的对象缺失列表,在恢复阶段就可以根据缺失列表来进行恢复了。

在进行peering的过程中,I/O请求会挂起,当peering完成进入恢复阶段时,I/O操作可以继续进行,不过当I/O请求命中了缺失列表的时候,对应的对象会优先进行恢复,再进行I/O的处理。

由于pglog记录的数目有限制,当对比各个副本上的pglog时,如果发现副本落后太多,就无法再根据pglog来进行恢复了。当遇到这种情况时,就只能通过backfill操作复制整个PG的数据来进行恢复了。

 

数据恢复

当peering完成后,PG进入激活状态,可以开始接收数据I/O的请求,并根据peering的信息决定是否进行恢复或backfill操作。主PG将根据对象的缺失列表进行具体对象的数据复制,对于Replica PG缺失的数据主PG会通过Push操作推送,而对于主PG缺失的数据会通过Pull操作从副本获取。对于无法依靠pglog进行恢复的,PG将进行backfill操作,进行数据的全量复制。在所有副本的数据都完全同步后,PG被标记为Clean状态,数据恢复完成。

 

Ceph一致性

多副本提高了数据的可靠性与可用性,但是同时带来了分布式系统的最大问题之一:数据一致性。Ceph使用pglog来保证多副本之间的数据一致性。想要保证出现故障时能根据pglog的记录进行数据恢复,就必须保证pglog记录的顺序性。由于Ceph客户端的I/O操作都是发给主OSD的,所以保证同一PG内的读/写顺序性(pglog的顺序性),也就保证了多副本之间的一致性。

另一个需要考虑的一致性问题是对同一对象文件的并发读/写操作,需要注意顺序性及对并发访问的控制,以免造成数据和逻辑错乱。

1)不同对象的并发控制

如果不同的对象基于CRUSH算法进入不同的PG中,则可以直接进行并发访问;如果不同的对象进入同一个PG中,则需要进行顺序控制,即先到达的I/O请求需要先处理,并先写pglog。在OSD PG层处理时,处理线程中就会给PG加锁,一直到queue_transactions把事务传给ObjectStore层,才会释放PG的锁。因此当同一PG内的上一个请求还在PG层处理时,下一个请求会被阻塞在获取PG锁的等待中。可以看出,对同一个PG里的不同对象,是通过PG锁来进行并发控制的。并且对获取PG锁的阻塞等待,会导致I/O访问的延时增加。

2)同一个对象的并发顺序控制

同一个对象的并发顺序控制比较复杂,涉及两种情形:一种是单个客户端的情形,客户端对同一个对象的I/O处理是串行的,前一次读/写操作完成后才能进行后一次读/写操作;另一种是多个客户端的情形,多个客户端对同一对象的并发访问需要集群文件系统的支持,这里不再进行讨论。

这里主要以单个客户端访问Ceph RBD块设备的场景为例进行阐述。例如,同一个客户端对同一对象先后发送了两次异步写请求。

· 网络顺序的保证。

Ceph客户端和OSD端的连接使用的是TCP,TCP使用序号来保证消息的顺序性。发送方发送数据时,TCP给每个数据包分配一个序列号,接收方收到数据后会给发送方发送ACK进行确认。如果数据丢包,在一定的时间内没有收到确认,则重传该数据包。接收方利用序列号对接收的数据进行确认,检测对方发送的数据是否有丢失或乱序等,接收方一旦收到已经被顺序化的数据,它就将这些数据按正确的顺序重组成数据流并传递到应用层进行处理。因此当客户端对同一对象(同一TCP连接)发送两个I/O请求时,在Ceph OSD层收到消息时,已经是TCP层保证顺序的数据包了。

· 消息层的顺序保证。

Ceph消息层也即Ceph的应用协议层,在处理网络连接时会检查每个消息头部的一个序列号header.seq,以Async Messenger为例,这个序列号对于接收端来说就是in_seq,接收端会把该序列号存入该连接的in_seq,对于发送端来说就是out_seq,发送端会把该连接的out_seq填入消息头部的序列号位置。

in_seq是收到消息的序列号,当收到消息时,会检查该消息所带的header.seq是否等于当前保存的in_seq+1,如果小于或等于in_seq表明收到旧的消息,如果大于in_seq+1表明消息丢失,如果既不是旧消息也没有丢失,则更新当前保存的in_seq为消息中的header.seq。Ceph的应答消息会带上收到消息的in_seq,因此发送端收到应答消息后,就会得知in_seq序列号的消息已经成功处理完成。out_seq是发送端生成的序列号,在发送消息时,message的头部header.seq会带上该序列号。因此Ceph消息层,通过消息序列号的检查,即可知道是否有消息丢失或重复,如果没有收到接收端的ACK,消息层会重发丢失的消息。这就在TCP层上更进一步保证了消息的顺序性。

当网络异常导致TCP连接断开后,发送端会调用AsyncConnection::fault(),关闭socket,调用requeue_sent()把没有收到ACK的消息从sent队列尾部弹出再重新从头部压入out_seq队列,而且out_seq会减去sent队列的大小。这样再重新建立连接时,重发的消息所带的header.seq还是跟之前的一样。

同样,当网络异常导致TCP连接断开后,接收端tcp_read失败后,也会调用AsyncConnection::fault()来关闭socket。因此对于连接异常断开后再重新建立连接的情况,in_seq也不会接着之前的序列号,仍然是取决于发送端生成的out_seq(header.seq),以此保证消息的顺序性。

· PG层的顺序保证。

OSD端PG层对消息队列里请求的处理是划分为多个shard的,而且每个shard可以配置多个线程,PG按照取模的方式映射到不同的shard里,每个shard里的多个线程可能处理的是同一PG内的对象,因此,OSD在处理PG时,从消息队列里取出的时候就对PG加了写锁,而且在请求下发到存储后端才会释放锁,因此处理同一PG内下一个对象的线程会阻塞在pglock获取上。所以如果从消息队列里过来的消息有序,那么在OSD端PG这一层处理时也是有序的。

· 读/写锁。

对某一个对象进行写操作时,会在对象文件上加写锁,格式为ondisk_write_lock(),对某一个对象进行读操作时,会在对象文件上加读锁,格式为ondisk_read_lock()。读锁和写锁是互斥的,当一个对象正在进行写操作时,尝试ondisk_read_lock()会一直阻塞等待;同样,当一个对象正在进行读操作时,尝试ondisk_write_lock()也会一直阻塞等待。直到读/写操作完成,才会释放各自的读/写锁。因此对于同一个对象不会发生读/写冲突的问题。

这里的两个锁操作限制的是同一个对象上的读和写的并发,对于读和读、写和写的并发并没有进行限制。Ceph中写请求和读请求所走的路径不同,读请求由PG层直接调用Store层读接口,ondisk_read_lock()的加锁和解锁都由PG层处理,并不会把请求传给Store层处理。因此pglock即可保证对同一对象两个读请求的顺序性。而写请求会把请求传递给Store层进行写磁盘和日志的处理,ondisk_write_lock()在PG层加锁,在Store层写完后释放,由于PG层对PG加锁pglock,而且是请求都下发到Store层才释放pglock,因此同一个对象的两个写请求,是不会并发进入PG层进行处理的,必定是按照顺序前一个写请求经过PG层的处理后,到达Store层进行处理(由Store层线程来进行处理),然后后一个写请求才会进入PG层进行处理后下发到Store层。所以,同一个对象上的写请求,一定是按照顺序在PG层进行处理的。虽然pglock保证了对同一对象两个写请求在PG层的顺序性,但还需要其他Store层机制保证真正写磁盘和日志时的顺序性。对于同一PG内不同对象的写顺序也需要这样的保证机制。即当PG层以一定的顺序把同一PG内的请求传递给Store层时,Store层也必须保证同一PG内请求的顺序性。

· ObjectStore层的顺序保证。

以filestore journal writeahead为例,当写请求到达ObjectStore层后(入口queue_transactions)会取得请求所在PG的OpSequencer,如果之前没有生成(第一次访问该PG)则生成该OpSequencer(每个PG有一个osr,类型为ObjectStore::Sequencer,osr→p表示指向OpSequencer)。

每个封装的写请求事务op都有一个递增的seq序列号,获得该序列号需要进行加锁操作,ObjectStore层按照顺序获得序列号并把请求先放到completions,同时放到writeq队尾,然后通知单线程(write_thread)去处理。在单线程中使用aio将writeq事务异步写到journal里,并将I/O信息放到aio_queue,然后触发write_finish_cond通知write_finish_thread进行处理。因为aio并不保证I/O操作执行的顺序性,因此采用op的seq序列号来保证完成后处理的顺序性。在write_finish_thread里检查已经完成的I/O的seq序列号,如果某个op之前的op还未完成,那么这个op会等到它之前的op都完成后才一起按照op的seq序列号顺序放到journal的finisher队列里。因此单线程保证了写journal的顺序性,op的seq序列号又保证了op放入finisher队列的顺序性。

在journal的finisher线程处理函数中,会将op按顺序放到OpSequencer的队列里,FileStore::OpWQ线程池中的线程用apply_lock进行OpSequencer队列加锁操作后,会从队列中获取op进行处理(此时并没有出队列),然后写数据到文件系统的操作完成后,才会出队列,并释放apply_lock。即通过OpSequencer队列锁apply_lock来控制同一个PG内写I/O到文件系统的并发,但是对于不同PG的写I/O是可以在OpWQ的线程池里进行并发处理的。

因此,FileStore里先通过单线程来控制持久化写到journal的顺序性,通过op的seq保证放入finish队列(OpSequencer队列)的顺序性,再通过OpSequencer队列加锁来保证同一PG内写数据到文件系统的顺序性,并且整个处理过程中都是通过FIFO来确保出入队列的顺序性的。由此可见,同一个对象的两次写请求按照顺序进入FileStore里进行处理,也是按照先后顺序处理完成的。

· 副本顺序的保证。

当primary将请求发送给副本OSD的时候,同一PG内对对象的访问也是有序的,副本端收到同一PG内对象访问的顺序与primary的顺序是一致的,副本端PG层和ObjectStore层的处理与primary一样,这样就保证了副本端与primary端对同一PG内的对象访问写pglog的顺序一致,写磁盘的顺序也一致。

 

Ceph Scrub机制

Ceph通过多副本策略来保证数据的高可靠性,而且副本间需要保证数据的一致性。但是当PG内有OSD的状态发生异常时,比如某个服务器宕机或OSD进程异常,则PG内一组OSD数据出现不一致的情况是有预期的;或者PG内OSD状态正常,但出现某些硬件故障,如磁盘出现坏道,这并不能被多副本根据pglog的数据恢复机制检测出来。因此,Ceph需要提供一种机制去检查各个副本之间的数据是否一致,如果发现不一致就必须恢复,这种机制就是Scrub。

Scrub分为两类:普通Scrub和Deep Scrub。普通Scrub只比较元数据信息,而Deep Scrub会真正地读取文件内容进行比较,但这样会造成很大的负担,会产生很多磁盘读取的I/O请求,而一旦数据不一致,在数据恢复时又会产生大量的磁盘写请求,因此OSD会一直处于较忙的状态。更严重的是,在Scrub过程中,会获取pglock,这样客户端的I/O请求将会被挂起,从而将造成严重的读/写请求延迟。

如前所述,Ceph是以PG为粒度来管理数据一致性的,Scrub也是以PG为粒度进行触发的,并且是由PG内主OSD启动的。Scrub是一个周期性事件,OSD进程会定期调度Scrub,如果满足条件就会将PG送入scrub_wq队列,等待disk_tp线程执行。在Ceph OSD进程中,由一个周期性的timer线程来检查是否需要对PG进行Scrub,该timer线程以一定频率调度Scrub流程(需要根据如下因素:PG是否primary,Scrub配置的时间段、系统当前负载等),主OSD预留slot,并且发送消息让从OSD也预留slot,主/从OSD均预留slot成功,将PG放入队列scrub_wq。当然也可以通过命令行“ceph pg scrub pgid”来触发Scrub。一个PG内包含成百上千个对象文件,Scrub流程需要读取每个对象在一组OSD上所有副本的校验信息来进行比较,这期间等待校验的对象文件是不能被修改的,因此为了避免一次产生大量的信息,Scrub流程会把一个PG内的所有对象文件划分成块,即一次只比较一个块的对象,Scrub用对象文件名的哈希值部分作为块划分的依据,每次启动Scrub流程会找到符合本次哈希值的对象进行校验,这称为chunky scrub。

为了避免Scrub占据pglock的时间过长,在正常情况下(没有数据恢复及集群和PG状态变化时),Scrub也需要多次调度(状态机)才能完成对一个块对象集合的Scrub操作。当Scrub流程找到本次待校验对象集合后,主OSD会向各副本OSD发起请求来锁定在其他副本OSD上的这部分对象集合。因为主OSD与副本OSD的每个对象可能会存在版本上的差异,Scrub发起者主OSD会附带一个待校验对象集合的版本信息发送给其他副本OSD,直到副本节点与主节点的版本同步后才进行比较。

在待校验对象集合在主/副OSD上的版本同步后,发起者主OSD会要求所有节点都开始计算这个对象集合中对象文件的校验信息并进行反馈。校验信息包含对象文件的元信息,如文件大小、扩展属性等,汇总为ScrubMap。发起者主OSD在发出请求后会进入WAIT_REPLICAS状态,由于副本OSD尚未返回,此次的Scrub操作会暂时执行完毕,Scrub状态记录在scrubber中。当主OSD在收到各副本的ScrubMap反馈后,上次挂起的Scrub操作会重新被放入scrub_wq队列,等待线程池重新从队列获取并执行,此时scrubber的状态是WAIT_REPLICAS,会继续比较各个ScrubMap,如果有不一致的对象,则收集不一致的对象并发送给MON,用户可以通过MON了解对象不一致的情况,并手动执行“ceph pg repair [pg_id]”命令来启动修复进程。当ScrubMap比较完成后,会对是否还有对象需要比较进行判断,如果还有新的对象需要做Scrub,会将PG重新加入scrub_wq队列,这样避免了Scrub执行时间太长,导致PG的其他I/O挂死(Hang)的情况。在进入Scrub时,配置还会随机睡眠一下,这也是为了避免长时间占用pglock。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值