对象存储
严格意义上讲,Ceph只提供对象存储接口,所谓的块存储接口和文件系统存储接口都算是对象存储接口应用程序。不同于传统文件系统提供的open/read/write/close/lseek,对象存储只提供put/get/delete,对象存储的逻辑单元就是对象而不是我们通常概念中的文件。
如下图所示,对于Ceph来说,RADOS GW是一个基于librados库构建的对象存储接口,为应用程序提供Ceph存储集群的RESTful网关,这样Ceph就作为Amazon S3和OpenStack Swift的后端对象存储,应用程序可以直接通过librados的C语言或C++语言API实现对象操作了。
对象存储和我们接触的硬盘和文件系统等存储形态不同,它有两个显著特征,如下所示。
对象存储采用Key/Value(K/V)方式的RESTful数据读/写接口,并且常以网络服务的形式提供数据的访问。
· 扁平的数据组织结构。对比文件系统,对象存储采用扁平的数据组织结构,往往是两层或三层。例如AWS S3和华为的UDS,每个用户可以把他的存储空间划分为“容器”,然后往每个容器里放对象,对象不能直接放到用户的根存储空间里,必须放到某个容器下面,而且不能嵌套,也就是说,容器下面不能再放一层容器,只能放对象。OpenStack Swift也类似。
RADOS
如下图所示,RADOS集群主要由两种节点组成:为数众多的OSD,负责完成数据存储和维护;若干个Monitor,负责完成系统状态检测和维护。OSD和Monitor之间互相传递节点的状态信息,共同得出系统的总体运行状态,并保存在一个全局的数据结构中,即所谓的集群运行图(Cluster Map)里。集群运行图与RADOS提供的特定算法相配合,便实现了Ceph的许多优秀特性。
在使用RADOS系统时,大量的客户端程序向Monitor索取最新的集群运行图,然后直接在本地进行计算,得出对象的存储位置后,便直接与对应的OSD进行通信,完成数据的各种操作。一个Monitor集群确保了某个Monitor失效时的高可用性。
Ceph客户端、Monitor和OSD可以直接交互,这意味着OSD可以利用本地节点的CPU和内存执行那些传统集群架构中有可能拖垮中央服务器的任务,充分发挥节点上的计算能力。
OSD
OSD用于实现数据的存储与维护。根据定义,OSD可以被抽象为系统和守护进程(OSD Daemon)两个部分。
OSD的系统部分本质上就是一台安装了操作系统和文件系统的计算机,其硬件部分至少包括一个单核的处理器、一定数量的内存、一块硬盘及一张网卡。实际应用中通常将多个OSD集中部署在一台更大规模的服务器上。在选择系统配置时,应当能够保证每个OSD占用一定的计算能力、一定数量的内存和一块硬盘(在通常情况下一个OSD对应一块硬盘)。同时,应当保证该服务器具备足够的网络带宽。
在上述系统平台上,每个OSD拥有一个自己的OSD Daemon。这个Daemon负责完成OSD的所有逻辑功能,包括与Monitor和其他OSD(事实上是其他OSD的Daemon)通信,以维护及更新系统状态,与其他OSD共同完成数据的存储和维护操作,与客户端通信完成各种数据对象操作,等等。
RADOS集群从Ceph客户端接收数据(无论是来自Ceph块设备、Ceph对象存储、Ceph文件系统,还是基于librados的自定义实现),然后存储为对象。下图所示,每个对象是文件系统中的一个文件,它们存储在OSD的存储设备上,由OSD Daemon处理存储设备上的读/写操作。
OSD在扁平的命名空间内把所有数据存储为对象(也就是没有目录层次)。对象包含一个标识符、二进制数和由名/值对组成的元数据,元数据语义完全取决于Ceph客户端。比如,Ceph FS用元数据存储文件属性,包括文件所有者、创建日期、最后修改日期等。
OSD的状态:
OSD的状态直接影响数据的重新分配,所以监测OSD的状态是Monitor的主要工作之一。
OSD状态用两个维度表示:up或down(OSD Daemon与Monitor连接是否正常);in或out(OSD是否含有PG)。因此,对于任意一个OSD,共有4种可能的状态。
· up & out:OSD Daemon与Monitor通信正常,但是没有PG分配到该OSD上。这种状态一般是OSD Daemon刚刚启动时。
· up & in:OSD Daemon工作的正常状态,有PG分配到OSD上。
· down & in:OSD Daemon不能与Monitor或其他OSD进行正常通信,这可能是因为网络中断或Daemon进程意外退出。
· down & out:OSD无法恢复,Monitor决定将OSD上的PG进行重新分配。之所以会出现该状态,是考虑OSD可能会在短时间内恢复,尽量减少数据的再分配。2.OSD状态检测
Ceph是基于通用计算机硬件构建的分布式系统,发生故障的概率要远高于专用硬件的分布式系统。如何及时检测节点故障和网络故障是检验Ceph高可用性的重要一环。由于心跳(Heartbeat)机制简单有效,所以Ceph采用这种方式,但是会增加监测维度。
OSD之间的心跳包。如果集群中的所有OSD都互相发送心跳包,则会对集群性能产生影响,所以Ceph选择Peer OSD发送心跳包。Peer OSD是指该OSD上所有PG的副本所在的OSD。同时由于Ceph提供公众网络(Public Network)(OSD与客户端通信)和集群网络(Cluster Network)(OSD之间的通信),所以Peer OSD之间的心跳包也分为前端(公众网络)和后端(集群网络),这样可最大限度地监测OSD及公众网络和集群网络的状态,及时上报Monitor。同时考虑到网络的抖动问题,可以设置Monitor在决定OSD下线之前需要收到多少次的报告。
· OSD与Monitor之间的心跳包。这个心跳包可以看作是Peer OSD之间心跳包的补充。如果OSD不能与其他OSD交换心跳包,那么就必须与Monitor按照一定频率进行通信,比如OSD状态是up & out时就需要这种心跳包。
数据寻址
如前所述,一个大规模分布式存储系统,必须要能够解决两个最基本的问题,即“我应该把数据写到什么地方”与“我之前把数据写到什么地方了”,因此会涉及数据如何寻址的问题。Ceph寻址流程如下图所示。
File:此处的File就是用户需要存储或访问的文件。对于一个基于Ceph开发的对象存储应用而言,这个File也就对应于应用中的“对象”,也就是用户直接操作的“对象”。
· Object:此处的Object是RADOS所看到的“对象”。Object与File的区别是,Object的最大尺寸由RADOS限定(通常为2MB或4MB),以便实现底层存储的组织管理。因此,当上层应用向RADOS存入尺寸很大的File时,需要将File切分成统一大小的一系列Object(最后一个的大小可以不同)进行存储。
· PG(Placement Group):顾名思义,PG的用途是对Object的存储进行组织和位置映射的。具体而言,一个PG负责组织若干个Object(可以为数千个甚至更多),但一个Object只能被映射到一个PG中,即PG和Object之间是“一对多”的映射关系。同时,一个PG会被映射到n个OSD上,而每个OSD上都会承载大量的PG,即PG和OSD之间是“多对多”的映射关系。在实践当中,n至少为2,如果用于生产环境,则至少为3。一个OSD上的PG可达到数百个。事实上,PG数量的设置关系到数据分布的均匀性问题。
· OSD:OSD的数量事实上也关系到系统的数据分布均匀性,因此不应该太少。在实践当中,至少也应该是数百个的量级才有助于Ceph系统发挥其应有的优势。
1)File→Object映射
这次映射的目的是,将用户要操作的File映射为RADOS能够处理的Object,其十分简单,本质上就是按照Object的最大尺寸对File进行切分,相当于磁盘阵列中的条带化过程。这种切分的好处有两个:一是让大小不限的File变成具有一致的最大尺寸、可以被RADOS高效管理的Object;二是让对单一File实施的串行处理变为对多个Object实施的并行化处理。
每一个切分后产生的Object将获得唯一的oid,即Object ID。其产生方式也是线性映射,极其简单。图5中,ino是待操作File的元数据,可以简单理解为该File的唯一ID。ono则是由该File切分产生的某个Object的序号。而oid就是将这个序号简单连缀在该File ID之后得到的。举例而言,如果1个ID为filename的File被切分成了3个Object,则其Object序号依次为0、1和2,而最终得到的oid就依次为filename0、filename1和filename2。
这里隐含的问题是,ino的唯一性必须得到保证,否则后续的映射将无法正确进行。
2)Object → PG映射
在File被映射为1个或多个Object之后,就需要将每个Object独立地映射到1个PG中去。这个映射过程也很简单,如下图所示,其计算公式如下:
由此可见,其计算由两步组成。首先,使用Ceph系统指定的一个静态哈希算法计算oid的哈希值,将oid映射为一个近似均匀分布的伪随机值。然后,将这个伪随机值和mask按位相与,得到最终的PG序号(pgid)。根据RADOS的设计,给定PG的总数为m(m应该为2的整数幂),则mask的值为m-1。因此,哈希值计算和按位与操作的整体结果事实上是从所有m个PG中近似均匀地随机选择1个。基于这一机制,当有大量Object和大量PG时,RADOS能够保证Object和PG之间的近似均匀映射。又因为Object是由File切分而来的,大部分Object的尺寸相同,因此,这一映射最终保证了各个PG中存储的Object的总数据量近似均匀。
3)PG → OSD映射
第3次映射就是将作为Object的逻辑组织单元的PG映射到数据的实际存储单元OSD上。RADOS采用一个名为CRUSH的算法,将pgid代入其中,然后得到一组共n个OSD。这n个OSD共同负责存储和维护一个PG中的所有Object。前面提到过,n的数值可以根据实际应用中对于可靠性的需求而配置,在生产环境下通常为3。具体到每个OSD,则由其上运行的OSD Daemon负责执行映射到本地的Object在本地文件系统中的存储、访问、元数据维护等操作。
和“Object → PG”映射中采用的哈希算法不同,CRUSH算法的结果不是绝对不变的,而会受到其他因素的影响。其影响因素主要有两个。
一是当前系统状态,也就是在前面有所提及的集群运行图。当系统中的OSD状态、数量发生变化时,集群运行图也可能发生变化,而这种变化将会影响到PG与OSD之间的映射关系。
二是存储策略配置。这里的策略主要与安全相关。利用策略配置,系统管理员可以指定承载同一个PG的3个OSD分别位于数据中心的不同服务器或机架上,从而进一步改善存储的可靠性。
因此,只有在系统状态和存储策略都不发生变化的时候,PG和OSD之间的映射关系才是固定不变的。在实际使用中,策略一经配置通常不会改变。而系统状态的改变或是因为设备损坏,或是因为存储集群规模扩大。好在Ceph本身提供了对这种变化的自动化支持,因而,即便PG与OSD之间的映射关系发生了变化,也并不会对应用产生影响。事实上,Ceph正是利用了CRUSH算法的动态特性,可以将一个PG根据需要动态迁移到不同的OSD组合上,从而自动化地实现高可靠性、数据分布再平衡等特性。
之所以在此次映射中使用CRUSH算法,而不使用其他哈希算法,一方面原因是CRUSH算法具有上述可配置特性,可以根据管理员的配置参数决定OSD的物理位置映射策略;另一方面原因是CRUSH算法具有特殊的“稳定性”,也即,当系统中加入新的OSD,导致系统规模增大时,大部分PG与OSD之间的映射关系不会发生改变,只有少部分PG的映射关系会发生变化并引发数据迁移。这种可配置性和稳定性都不是普通哈希算法所能提供的。因此,CRUSH算法的设计也是Ceph的核心内容之一。
至此为止,Ceph通过3次映射,完成了从File到Object、Object到PG、PG再到OSD的整个映射过程。从整个过程可以看到,这里没有任何的全局性查表操作需求。至于唯一的全局性数据结构:集群运行图。它的维护和操作都是轻量级的,不会对系统的可扩展性、性能等因素造成影响。
接下来的一个问题是:为什么需要引入PG并在Object与OSD之间增加一层映射呢?
可以想象一下,如果没有PG这一层映射,又会怎么样呢?在这种情况下,一定需要采用某种算法,将Object直接映射到一组OSD上。如果这种算法是某种固定映射的哈希算法,则意味着一个Object将被固定映射在一组OSD上,当其中一个或多个OSD损坏时,Object无法被自动迁移至其他OSD上(因为映射函数不允许),当系统为了扩容新增了OSD时,Object也无法被再平衡到新的OSD上(同样因为映射函数不允许)。这些限制都违背了Ceph系统高可靠性、高自动化的设计初衷。如果采用一个动态算法(如仍然采用CRUSH算法)来完成这一映射,似乎是可以避免由静态映射而导致的问题的。但是,其结果将是各个OSD所处理的本地元数据量暴增,由此带来的计算复杂度和维护工作量也是难以承受的。
例如,在Ceph的现有机制中,一个OSD平时需要和与其共同承载同一个PG的其他OSD交换信息,以确定各自是否工作正常,是否需要进行维护操作。由于一个OSD上大约承载数百个PG,每个PG内通常有3个OSD,因此,在一段时间内,一个OSD大约需要进行数百次至数千次OSD信息交换。
然而,如果没有PG的存在,则一个OSD需要和与其共同承载同一个Object的其他OSD交换信息。由于每个OSD上承载的Object可能高达数百万个,因此,同样长度的一段时间内,一个OSD大约需要进行的OSD间信息交换将暴涨至数百万次乃至数千万次。而这种状态维护成本显然过高。
综上所述,引入PG的好处至少有两方面:一方面实现了Object和OSD之间的动态映射,从而为Ceph的可靠性、自动化等特性的实现留下了空间;另一方面也有效简化了数据的存储组织,大大降低了系统的维护与管理成本。
存储池
存储池是一个逻辑概念,是对存储对象的逻辑分区。Ceph安装后,会有一个默认的存储池,用户也可以自己创建新的存储池。如下图所示,一个存储池包含若干个PG及其所存储的若干个对象。
Ceph客户端从监视器获取一张集群运行图,并把对象写入存储池。存储池的大小或副本数、CRUSH存储规则和归置组数量决定Ceph如何放置数据。我们可以使用以下命令来创建存储池:
从代码中可以看出,存储池支持的内容如下。
· 设置数据存储的方法属于多副本模式还是纠删码模式。如果是多副本模式,则可以设置副本的数量;如果是纠删码模式,则可以设置数据块和非数据块的数量(纠删码存储池把各对象存储为K+M个数据块,其中有K个数据块和M个编码块)。默认为多副本模式(即存储每个对象的若干个副本),如果副本数为3,则每个PG映射到3个OSD节点上。换句话说,对于每个映射到该PG的对象,其数据存储在对应的3个OSD节点上。
· 设置PG的数目。合理设置PG的数目,可以使资源得到较优的均衡。
· 设置PGP的数目。在通常情况下,与PG数目一致。当需要增加PG数目时,用户数据不会发生迁移,只有进一步增加PGP数目时,用户数据才会开始迁移。
· 针对不同的存储池设置不同的CRUSH存储规则。比如可以创建规则,指定在选择OSD时,选择拥有固态硬盘的OSD节点。
另外,通过存储池,还可以进行如下操作。
· 提供针对存储池的功能,如存储池快照等。
· 设置对象的所有者或访问权限。
我们看到这里在PG的基础上又出现了PGP的概念,至于PG与PGP之间的区别,可以先看Learning Ceph和Ceph Cookbook两本书的作者Karan Singh的一段解释:
总结来说:
- PG是指定存储池存储对象的目录有多少个,PGP是存储池PG的OSD分布组合个数
- PG的增加会引起PG内的数据进行分裂,分裂到相同的OSD上新生成的PG当中
- PGP的增加会引起部分PG的分布进行变化,但是不会引起PG内对象的变动
Monitor
Ceph客户端读或写数据前必须先连接到某个Ceph监视器上,获得最新的集群运行图副本。一个Ceph存储集群只需要单个监视器就能运行,但它就成了单一故障点(即如果此监视器宕机,Ceph客户端就不能读或写数据了)。为增强其可靠性和容错能力,Ceph支持监视器集群。在一个监视器集群内,延时及其他错误会导致一到多个监视器滞后于集群的当前状态。因此,Ceph的各监视器例程必须与集群的当前状态达成一致。
由若干个Monitor组成的监视器集群共同负责整个Ceph集群中所有OSD状态的发现与记录,并且形成集群运行图的主副本,包括集群成员、状态、变更,以及Ceph存储集群的整体健康状况。随后,这份集群运行图被扩散至全体OSD及客户端,OSD使用集群运行图进行数据的维护,而客户端使用集群运行图进行数据的寻址。
在集群中,各个Monitor的功能总体上是一样的,其之间的关系可以被简单理解为主从备份关系。Monitor并不主动轮询各个OSD的当前状态。正相反,OSD需要向Monitor上报状态信息。常见的上报有两种情况:一是新的OSD被加入集群,二是某个OSD发现自身或其他OSD发生异常。在收到这些上报信息后,Monitor将更新集群运行图的信息并加以扩散。
集群运行图实际上是多个Map的统称,包括Monitor Map、OSDMap、PG Map、CRUSH Map及MDS Map等,各运行图维护着各自运行状态的变更。其中CRUSH Map用于定义如何选择OSD,内容包含了存储设备列表、故障域树状结构(设备的分组信息,如设备、主机、机架、行、房间等)和存储数据时如何利用此树状结构的规则。如下图所示,根节点是default,包含3个主机,每个主机包含3个OSD服务。
相应的CRUSH Map代码片段如下:
"buckets": [{"id": -1,"name": "host0","type_id": 1,"type_name": "host","weight": 65536,"alg": "straw2","hash": "rjenkins1","items": [{"id": 0,"weight": 65536,"pos": 0}]},{"id": -2,"name": "host1","type_id": 1,"type_name": "host","weight": 65536,"alg": "straw2","hash": "rjenkins1","items": [{"id": 1,"weight": 65536,"pos": 0}]},{"id": -3,"name": "rack0","type_id": 2,"type_name": "rack","weight": 196608,"alg": "straw2","hash": "rjenkins1","items": [{"id": -1,"weight": 65536,"pos": 0},{"id": -2,"weight": 65536,"pos": 1},{"id": -5,"weight": 65536,"pos": 2}]},{"id": -4,"name": "root","type_id": 3,"type_name": "root","weight": 262144,"alg": "straw2","hash": "rjenkins1","items": [{"id": -3,"weight": 262144,"pos": 0}]},{"id": -5,"name": "host2","type_id": 1,"type_name": "host","weight": 65536,"alg": "straw2","hash": "rjenkins1","items": [{"id": 2,"weight": 65536,"pos": 0}]}]
在上图所示的树状结构中,所有非叶子节点称为桶(Bucket),所有Bucket的ID号都是负数,和OSD的ID进行区分。选择OSD时,需要先指定一个Bucket,然后选择它的一个子Bucket,这样一级一级递归,直到到达设备(叶子)节点。目前有5种算法来实现子节点的选择,包括Uniform、List、Tree、Straw、Straw2,如表所示。这些算法的选择影响了两个方面的复杂度:在一个Bucket中,找到对应的节点的复杂度及当一个Bucket中的OSD节点丢失或增加时,数据移动的复杂度。
其中,Uniform与item具有相同的权重,而且Bucket很少出现添加或删除item的情况,它的查找速度是最快的。Straw/Straw2不像List和Tree一样都需要遍历,而是让Bucket包含的所有item公平竞争。这种算法就像抽签一样,所有的item都有机会被抽中(只有最长的签才能被抽中,每个签的长度与权重有关)。
除了存储设备的列表及树状结构,CRUSH Map还包含了存储规则,用来指定在每个存储池中选择特定OSD的Bucket范围,还可以指定备份的分布规则。CRUSH Map有一个默认存储规则,如果用户创建存储池时没有指定CRUSH规则,则使用该默认规则。但是用户可以自定义规则,指定给特定存储池。
下面代码表示默认的CRUSH规则,重点在steps部分。这里指定从default这个Bucket开始,选择3个(创建存储池时指定的副本数)主机,在这3个主机中再选择OSD。每个对象的3份数据将位于3个不同的主机上。
"rules": [{"rule_id": 3,"rule_name": "data","ruleset": 3,"type": 1,"min_size": 2,"max_size": 2,"steps": [{"op": "take","item": -4,"item_name": "root"},{"op": "chooseleaf_firstn","num": 0,"type": "rack"},{"op": "emit"}]}]
1.Monitor与客户端的通信
客户端包括RBD客户端、RADOS客户端、Ceph FS客户端/MDS。根据通信内容分为获取OSDMap和命令行操作。
1)命令行操作
命令行操作主要包括集群操作命令(OSD、Monitor、MDS的添加和删除,存储池的创建和删除等)、集群信息查询命令(集群状态、空间利用率、IOps和带宽等)。这些命令都是由Monitor直接执行或通过Monitor转发到OSD上执行的。
2)获取OSDMap
我们知道客户端与RADOS的读/写不需要Monitor的干预,客户端通过哈希算法得到Object所在的PG信息,然后查询OSDMap就可以得到PG的分布信息,就可以与Primary OSD进行通信了。那么客户端与Monitor仅仅是当需要获取最新OSDMap时才会进行通信。
· 客户端初始化时。
· 某些特殊情况,会主动获取新的OSDMap:OSDMap设置了 CEPH_OSDMAP_PAUSEWR/PAUSERD(Cluster暂停所有读/写),每一次的读/写都需要获取OSDMap;OSDMap设置了Cluster空间已满或存储池空间已满,每一次写都需要获取OSDMap;找不到相应的存储池或通过哈希算法得到PG,但是在OSDMap中查不到相关PG分布式信息(说明PG删除或PG创建)。
2.Monitor与OSD的通信
相比Monitor与客户端的通信,Monitor与OSD的通信会复杂得多,内容如下。
· Monitor需要知道OSD的状态,并根据状态生成新的OSDMap。所以OSD需要将OSD的Down状态向Monitor报告。
· OSD和Monitor之间存在心跳机制,通过这种方式来判断OSD的状态。
· OSD定时将PG信息发送给Monitor。PG信息包括PG的状态(Active、degraded等)、Object信息(数目、大小、复制信息、Scrub/Repair信息、IOps和带宽等)。Monitor通过汇总这些信息就可以知道整个系统的空间使用率、各个存储池的空间大小、集群的IOps和带宽等实时信息。
· OSD的操作命令是客户端通过Monitor传递给OSD的。比如osd scrub/deep scrub、pg scrub/deep scrub等。
· OSD初始化或Client/Primary OSD所包含的OSDMap的版本高于当前的OSDMap。
数据操作流程
Ceph的读/写操作采用Primary-Replica模型,客户端只向Object所对应OSD set的Primary发起读/写请求,这保证了数据的强一致性。当Primary收到Object的写请求时,它负责把数据发送给其他副本,只有这个数据被保存在所有的OSD上时,Primary才应答Object的写请求,这保证了副本的一致性。
这里以Object写入为例,假定一个PG被映射到3个OSD上。Object写入流程如下图所示。
当某个客户端需要向Ceph集群写入一个File时,首先需要在本地完成前面所述的寻址流程,将File变为一个Object,然后找出存储该Object的一组共3个OSD。这3个OSD具有各自不同的序号,序号最靠前的那个OSD就是这一组中的Primary OSD,而后两个则依次是Secondary OSD和Tertiary OSD。
找出3个OSD后,客户端将直接和Primary OSD进行通信,发起写入操作(步骤1)。Primary OSD收到请求后,分别向Secondary OSD和Tertiary OSD发起写入操作(步骤2和步骤3)。当Secondary OSD和Tertiary OSD各自完成写入操作后,将分别向Primary OSD发送确认信息(步骤4和步骤5)。当Primary OSD确认其他两个OSD的写入完成后,则自己也完成数据写入,并向客户端确认Object写入操作完成(步骤6)。
之所以采用这样的写入流程,本质上是为了保证写入过程中的可靠性,尽可能避免出现数据丢失的情况。同时,由于客户端只需要向Primary OSD发送数据,因此,在互联网使用场景下的外网带宽和整体访问延迟又得到了一定程度的优化。
当然,这种可靠性机制必然导致较长的延迟,特别是,如果等到所有的OSD都将数据写入磁盘后再向客户端发送确认信号,则整体延迟可能难以忍受。因此,Ceph可以分两次向客户端进行确认。当各个OSD都将数据写入内存缓冲区后,就先向客户端发送一次确认,此时客户端即可以向下执行。待各个OSD都将数据写入磁盘后,会向客户端发送一个最终确认信号,此时客户端可以根据需要删除本地数据。
分析上述流程可以看出,在正常情况下,客户端可以独立完成OSD寻址操作,而不必依赖于其他系统模块。因此,大量的客户端可以同时和大量的OSD进行并行操作。同时,如果一个File被切分成多个Object,这多个Object也可被并行发送至多个OSD上。
从OSD的角度来看,由于同一个OSD在不同的PG中的角色不同,因此,其工作压力也可以被尽可能均匀地分担,从而避免单个OSD变成性能瓶颈。
如果需要读取数据,客户端只需完成同样的寻址过程,并直接和Primary OSD联系。在目前的Ceph设计中,被读取的数据默认由Primary OSD提供,但也可以设置允许从其他OSD中获取,以分散读取压力从而提高性能。
Cache Tiering
分布式的集群一般都采用廉价的PC与传统的机械硬盘进行搭建,所以在磁盘的访问速度上有一定的限制,没有理想的IOps数据。当去优化一个系统的I/O性能时,最先想到的就是添加快速的存储设备作为缓存,热数据在缓存被访问到,缩短数据的访问延时。Ceph也从Firefly 0.80版本开始引入这种存储分层技术,即Cache Tiering。
Cache Tiering的理论基础,就是存储的数据是有热点的,数据并不是均匀访问的。也就是80%的应用只访问20%的数据,那么这20%的数据就称为热点数据,如果把这些热点数据保存到固态硬盘等性能比较高的存储设备上,那么就可以减少响应的时间。
所以Cache Tiering的做法就是,用固态硬盘等相对快速、昂贵的存储设备组成一个存储池作为缓存层存储热数据,然后用相对慢速、廉价的设备作为存储后端存储冷数据(Storage层或Base层)。缓存层使用多副本模式,Storage层可以使用多副本或纠删码模式。
在Cache Tiering中有一个分层代理,当保存在缓存层的数据变冷或不再活跃时,该代理把这些数据刷到Storage层,然后把它们从缓存层中移除,这种操作称为刷新(Flush)或逐出(Evict)。
如下图所示,Ceph的对象管理器(Objecter,位于osdc即OSD客户端模块)决定往哪里存储对象,分层代理决定何时把缓存内的对象“刷回”Storage层,所以缓存层和Storage层对Ceph客户端来说是完全透明的。需要注意的是,Cache Tiering是基于存储池的,在缓存层和Storage层之间的数据移动是两个存储池之间的数据移动。
目前Cache Tiering主要支持如下几种模式。
· 写回模式:对于写操作,当请求到达缓存层完成写操作后,直接应答客户端,之后由缓存层的代理线程负责将数据写入Storage层。对于读操作则看是否命中缓存层,如果命中直接在缓存层读,没有命中可以重定向到Storage层访问,如果Object近期有访问过,说明比较热,可以提升到缓存层中。
· forward模式:所有的请求都重定向到Storage层访问。
· readonly模式:写请求直接重定向到Storage层访问,读请求命中缓存层则直接处理,没有命中缓存层需要从Storage层提升到缓存层中进而完成请求,下次再读取直接命中缓存。
· readforward模式:读请求都重定向到Storage层中,写请求采用写回模式。
· readproxy模式:读请求发送给缓存层,缓存层去Storage层中读取,获得Object后,缓存层自己并不保存,而是直接发送给客户端,写请求采用写回模式。
· proxy模式:对于读/写请求都是采用代理的方式,不是转发而是代表客户端去进行操作,缓存层自己并不保存。
这里提及的重定向、提升与代理等几种操作的具体含义如下。
· 重定向:客户端向缓存层发送请求,缓存层应答客户端发来的请求,并告诉客户端应该去请求Storage层,客户端收到应答后,再次发送请求给Storage层请求数据,并由Storage层告诉客户端请求的完成情况。
· 代理:客户端向缓存层发送读请求,如果未命中,则缓存层自己会发送请求给Storage层,然后由缓存层将获取的数据发送给客户端,完成读请求。在这个过程中,虽然缓存层读取到了该Object,但不会将其保存在缓存层中,下次仍然需要重新向Storage层请求。· 提升:客户端向缓存层发送请求,如果缓存层未命中,则会选择将该Object从Storage层中提升到缓存层中,然后在缓存层进行读/写操作,操作完成后应答客户端请求完成。在这个过程中,和代理操作的区别是,在缓存层会缓存该Object,下次直接在缓存中进行处理。
块存储
如前所述,Ceph可以用一套存储系统同时提供对象存储、块存储和文件系统存储3种功能。Ceph存储集群RADOS自身是一个对象存储系统,基础库librados提供一系列的API允许用户操作对象和OSD、MON等进行通信。基于RADOS与librados库,Ceph通过RBD提供了一个标准的块设备接口,提供基于块设备的访问模式。
Ceph中的块设备称为Image,是精简配置的,即按需分配,大小可调且将数据条带化存储到集群内的多个OSD中。
条带化是指把连续的信息分片存储于多个设备中。当多个进程同时访问一个磁盘时,可能会出现磁盘冲突的问题。大多数磁盘系统都对访问次数(每秒的I/O操作)和数据传输率(每秒传输的数据量,TPS)有限制,当达到这些限制时,后面需要访问磁盘的进程就需要等待,这时就是所谓的磁盘冲突。避免磁盘冲突是优化I/O性能的一个重要目标,而优化I/O性能最有效的手段是将I/O请求最大限度地进行平衡。
条带化就是一种能够自动将I/O负载均衡到多个物理磁盘上的技术。通过将一块连续的数据分成多个相同大小的部分,并把它们分别存储到不同的磁盘上,条带化技术能使多个进程同时访问数据的不同部分而不会造成磁盘冲突,而且能够获得最大限度上的I/O并行能力。
条带化能够将多个磁盘驱动器合并为一个卷,这个卷所能提供的速度比单个盘所能提供的速度要快很多。Ceph的块设备就对应于LVM的逻辑卷,块设备被创建时,可以指定如下参数实现条带化。
· stripe-unit:条带的大小。
· stripe-count:在多少数量的对象之间进行条带化。
如下图所示,当stripe-count为3时,表示块设备上地址[0, object-size*stripe_count-1]到对象位置的映射。每个对象被分成stripe_size大小的条带,按stripe_count分成一组,块设备在上面依次分布。块设备上[0, stripe_size-1]对应Object1上的[0,stripe_size-1],块设备上[stripe_size, 2*stripe_size-1]对应Object2上的[0, stripe_size-1],以此类推。
当处理大尺寸图像、大Swift对象(如视频)的时候,我们能看到条带化到一个对象集合(Object Set)中的多个对象能带来显著的读/写性能提升。当客户端把条带单元并行地写入相应对象时,就会有明显的写性能提升,因为对象映射到了不同的PG,并进一步映射到不同OSD,可以并行地以最大速度写入。如下图所示,使用Ceph的块设备有两种路径。
· 通过Kernel Module:即创建了RBD设备后,把它映射到内核中,成为一个虚拟的块设备,这时这个块设备同其他通用块设备一样,设备文件一般为/dev/rbd0,后续直接使用这个块设备文件就可以了,可以把/dev/rbd0格式化后挂载到某个目录,也可以直接作为裸设备进行使用。
· 通过librbd:即创建了RBD设备后,使用librbd、librados库访问和管理块设备。这种方式直接调用librbd提供的接口,实现对RBD设备的访问和管理,不会在客户端产生块设备文件。
其中第二种方式主要是为虚拟机提供块存储设备。在虚拟机场景中,一般会用QEMU/KVM中的RBD驱动部署Ceph块设备,宿主机通过librbd向客户机提供块存储服务。QEMU可以直接通过librbd,像访问虚拟块设备一样访问Ceph块设备。
Ceph FS
Ceph FS是一个可移植操作系统接口兼容的分布式存储系统,与通常的网络文件系统一样,要访问Ceph FS,需要有对应的客户端。Ceph FS支持两种客户端:Ceph FS FUSE和Ceph FS Kernel。也就是说有两种使用Ceph FS的方式:一是通过Kernle Module,Linux内核里包含了Ceph FS的实现代码;二是通过FUSE(用户空间文件系统)的方式。通过调用libcephfs库来实现Ceph FS的加载,而libcephfs库又调用librados库与RADOS进行通信。
之所以会通过FUSE的方式实现Ceph FS的加载,主要是考虑Kernel客户端的功能、稳定性、性能都与内核绑定,在不能升级内核的情况下,很多功能可能就不能使用。而FUSE基本就不存在这个限制。Ceph FS架构如下图所示。
上层是支持客户端的Ceph FS Kernel Object、Ceph FS FUSE、Ceph FS Library等,底层还是基础的OSD和Monitor,此外添加了元数据服务器(MDS)。Ceph FS要求Ceph存储集群内至少有一个元数据服务器,负责提供文件系统元数据(目录、文件所有者、访问模式等)的存储与操作。MDS只为Ceph FS服务,如果不需要使用Ceph FS,则不需要配置MDS。
Ceph FS从数据中分离出元数据,并存储于MDS,文件数据存储于集群中的一个或多个对象。MDS(称为ceph-mds的守护进程)存在的原因是,简单的文件系统操作,比如ls、cd这些操作会不必要地扰动OSD,所以把元数据从数据里分离出来意味着Ceph文件系统既能提供高性能服务,又能减轻存储集群负载。