Ceph:一个可扩展,高性能分布式文件系统
目录
Ceph: A Scalable,High-Performance Distributed File System
Ceph:一个可扩展,高性能分布式文件系统
摘要
我们开发了 Ceph,这是一个分布式文件系统,它提供了出色的性能、可靠性和可扩展性。 Ceph 通过将分配表替换为伪随机数据分布函数(CRUSH),最大化了数据和元数据管理之间的分离,该函数是为不可靠对象存储设备(OSD)的异构和动态集群而设计的。我们利用设备智能,将数据复制、故障检测和恢复分发到运行专用本地对象文件系统的半自治 OSD。动态分布式元数据集群提供了非常高效的元数据管理,并很好地适应了广泛的、通用的、科学计算文件系统工作负载。各种工作负载下的性能测试表明,Ceph 具有优异的 I/O 性能和可伸缩的元数据管理,每秒支持250000多个元数据操作。
1. 概述
长期以来,系统设计者一直在寻求提高文件系统的性能,事实证明,文件系统对一类极其广泛的应用程序的整体性能至关重要。尤其是科学界和高性能计算界推动了分布式存储系统的性能和可扩展性的发展,通常在几年内就可以预测到更通用的需求。以NFS[20]为例的传统解决方案提供了一个简单的模型,其中服务器导出一个文件系统层次结构,客户端可以将其映射到其本地名称空间。尽管客户端/服务器模型被广泛使用,但客户端/服务器模型中固有的集中式架构已被证明是可扩展性的一个重大障碍。
最近的分布式文件系统采用了基于对象存储的体系结构,其中传统的硬盘被对象存储设备(OSD)取代,后者将CPU、网络接口和本地缓存与底层磁盘或 RAID 相结合[4、7、8、32、35]。OSD 取代了传统的 block-level 接口,客户端可以将字节范围读写到更大(通常大小可变)的命名对象,将底层块分配决策分配给设备本身。客户端通常与元数据服务器(MDS)交互以执行元数据操作(open,rename),同时直接与 OSD 通信以执行文件 I/O(read,write),显著提高了整体可扩展性。
解释说明:每个 OSD 都是一个智能设备,具有自己的存储介质、处理器、内存以及网络系统等,负责管理本地的Object,是对象存储系统的关键。
采用此模型的系统还是会受到可扩展性限制的影响,因为元数据工作负载几乎没有分布。连续依赖传统的文件系统原则,如分配列表和索引节点表,此类信息授权给 OSD,进一步限制了可扩展性和性能,并增加了可靠性成本。
我们介绍了 Ceph ,一个分布式文件系统,它提供了卓越的性能和可靠性,同时提高了无与伦比的可扩展性。我们的体系结构基于这样一个假设,即 PB 级的系统本质上是动态的:大型系统不可避免地以增量方式构建,节点故障是常态而不是例外,并且工作负载的质量和特征随着时间不断变化。
Ceph 通过消除文件分配表和用生成函数替换它们来解耦(分离)数据和元数据操作。这使得 Ceph 能够利用 OSD 来分发数据访问、更新序列化、复制和可靠性、故障检测和恢复的复杂性。Ceph 使用了一个高度自适应的分布式元数据集群体系结构,它极大地提高了元数据访问的可扩展性,同时也提高了整个系统的可扩展性。我们讨论了架构设计中的目标和工作负载假设,分析了它们对系统可扩展性和性能的影响,并讲述了我们在实现功能系统原型方面的经验。
2. 系统概述
图1. 系统架构:客户端通过与OSD直接通信来执行文件 I/O 。每个进程既可以直接链接到客户机实例,也可以与装入的文件系统交互。
Ceph 文件系统有三个主要组件:客户端,每个实例通过类似 POSIX 的文件系统接口访问主机或进程;OSD 集群,集中存储所有数据和元数据;元数据服务器集群,它的职责是协调安全性、一致性和系统的连贯性(见图1)。我们说 Ceph 接口类似 POSIX ,因为我们发现扩展接口和有选择地放宽一致性语义是合理的设计,以便更好地符合应用程序的需要并提高系统性能。
体系结构的主要目标是可扩展性(达到数百倍甚至更高)、性能和可靠性。可扩展性从多个方面考虑,包括系统的总体存储容量和吞吐量,以及单个客户端、目录或文件的性能。我们的目标工作负载可能包括这样的极端情况:成千上万的主机同时读写同一个文件,或者在同一个目录中创建文件。这种场景在运行在超级计算集群上的科学应用程序中很常见,越来越多地表明了未来的通用工作负载。更重要的是,随着时间的推移,运行的应用程序和数据集的变化,数据和元数据访问会发生显著变化,我们认识到分布式文件系统工作负载本质上是动态的。Ceph 直接解决了可扩展性问题,同时通过三个基本的设计特性实现了高性能、高可靠性和高可用性:分离的数据和元数据、动态分布式元数据管理和可靠的自主分布式对象存储。
分离数据和元数据
Ceph 最大化了文件元数据管理与文件数据存储的分离。元数据操作(打开、重命名等)由元数据服务器集群集中管理,而客户端直接与 OSD 交互以执行文件 I/O (读和写)。基于对象的存储长期以来一直承诺通过将底层块分配决策委托给单个设备来提高文件系统的可扩展性。然而,与现有的基于对象的文件系统[4,7,8,32]相比,Ceph 完全消除了分配列表。相反,文件数据通过计算被分配到命名对象上,而一个名为 CRUSH [29]的特殊用途的数据分发函数将对象分配到存储设备上。这使得任何一方都可以计算(而不是查找)组成文件内容的对象的名称和位置,省去了维护和分发对象列表的需要,简化了系统的设计,减少了元数据集群的工作量。
动态分布式元数据管理
元数据工作负载相当于系统中典型的元数据工作负载的一半,因为系统的元数据文件管理效率占系统整体性能的一半。 Ceph 利用了一种新的基于动态子树划分的元数据集群体系结构[30],该体系结构在数十个甚至数百个 MDS 之间自适应、智能地分配管理文件系统目录层次结构的责任。一个(动态的)分层分区在每个 MDS 的工作负载中保留了局部性,促进了高效的更新和积极的预取,以提高常见工作负载的性能。值得注意的是,元数据服务器之间的工作负载分布完全基于当前的访问模式,这使得 Ceph 能够在任何工作负载下有效地利用可用的 MDS 资源,并在 MDS 数量上实现近乎线性的扩展。
可靠的自动分布的对象存储
由数千台设备组成的大型系统本质上是动态的:它们是以增量方式构建的,随着新存储的部署和旧设备的停用,设备故障频繁发生且在预料之中,以及大量数据的创建、移动和删除,它们会不断增长和收缩。所有这些因素都要求数据分布的发展,以便有效地利用可用资源并保持所需的数据复制级别。Ceph 将数据迁移、复制、故障检测和故障恢复的责任委托给存储数据的 OSD 集群,而在高层, OSD 向客户端和元数据服务器共同提供一个逻辑对象存储。这种方法允许 Ceph 更有效地利用每个 OSD 上的智能(CPU和内存),以线性扩展实现可靠、高可用的对象存储。
我们接下来描述 Ceph 客户端、元数据服务器集群和分布式对象存储的操作,以及它们如何受到我们体系结构的关键特性的影响。我们也将展示系统蓝图的状态。
3. 客户端操作
通过描述 Ceph 的客户端操作,我们介绍 Ceph 组件的大致操作以及它们与应用程序的交互。 Ceph 客户端在执行应用程序代码的每个主机上运行,并向应用程序公开文件系统接口。在 Ceph 原型中,客户端代码完全在用户空间中运行,可以通过直接链接到它来访问,也可以通过FUSE[25](一个用户空间文件系统接口)作为挂载的文件系统进行访问。每个客户端都维护自己的文件数据缓存,独立于内核页或缓冲区缓存,使其可以访问直接链接到客户端的应用程序。
3.1 文件 I/O 和功能
当进程打开一个文件时,客户端向 MDS 集群发送一个请求。 MDS 遍历文件系统层次结构,将文件名转换为文件 inode ,其中包含唯一的 inode 编号、文件所有者、模式、大小和其他每个文件的元数据。如果文件存在并且访问权限被授予,MDS 将返回 inode 编号、文件大小以及用于将文件数据映射到对象的信息。MDS 还可以向客户端发出一个指定允许哪些操作的功能(如果它还没有)。目前的功能包括控制客户端读、缓存读、写和缓冲区写入能力。未来,这些功能将包括安全**,允许客户端向 OSD 证明他们有权读写数据[13,19](原型目前信任所有客户端)。 MDS 在文件 I/O 中的后续参与仅限于管理保持文件一致性和实现适当语义的功能。
Ceph 概括了一系列分割策略,将文件数据映射到一系列对象上。为了避免对文件分配元数据的任何需要,对象名只需将文件 inode 编号和分割块的编号组合起来。然后使用 CRUSH (一个全局已知的映射函数)将对象副本分配给 OSD (见第5.1节)。例如,如果一个或多个客户端打开一个文件进行读访问,MDS 将授予它们读取和缓存文件内容的能力。有了 inode 编号、布局和文件大小,客户端可以命名和定位包含文件数据的所有对象,并直接从 OSD 集群中读取。任何不存在的对象或字节范围都被定义为文件 "孔" 或零。类似地,如果一个客户端打开一个文件进行写入,它就可以使用缓冲区进行写入,并且它在文件中任何偏移量处生成的任何数据都会简单地写入到相应 OSD 上的相应对象中。客户端放弃关闭文件时的功能,并为 MDS 提供新的文件大小(写入的最大偏移量),这将重新定义(可能)存在并包含文件数据的对象集。
3.2 客户端同步
POSIX 语义合理地要求读取反映以前写入的任何数据,写入是原子的(即,重叠、并发写入的结果将反映特定的发生顺序)。当一个文件被具有多个写入程序或读写器混合的多个客户端打开时,MDS 将撤消以前发布的任何读缓存和写缓冲功能,强制该文件的客户端 I/O 同步。也就是说,在 OSD 确认之前,每个应用程序的读或写操作都将被阻塞,从而有效地减轻了更新序列化和与存储每个对象的 OSD 同步的负担。当写入跨越对象边界时,客户端获取受影响对象的独占锁(由其各自的 OSD 授予),并立即提交写入和解锁操作以实现所需的序列化。对象锁类似地用于通过异步获取锁和刷新数据来屏蔽大量写操作的延迟。
毫不奇怪,同步 I/O 对于应用程序来说可能是一个性能杀手,尤其是那些执行小读写操作的应用程序,因为延迟了至少一次到 OSD 的往返。虽然读写共享在通用工作负载中相对少见[22],但在科学计算应用程序[27]中更为常见,因为在科学计算应用程序中,性能往往至关重要。因此,在应用程序不依赖一致性的情况下,通常希望以牺牲严格的、标准的一致性为代价来放宽一致性。尽管 Ceph 通过全局切换支持这种放宽策略,而且许多其他分布式文件系统都存在[20]这个问题,但这是一个不精确且不能令人满意的解决方案:要么性能受到影响,要么系统范围内的一致性丢失。
正是由于这个原因,高性能计算(HPC)社区[31]提出了一套 POSIX I/O 接口的高性能计算扩展,其中的一个子集由 Ceph 实现。重要的是,这些扩展包含一个 O_LAZY 标签,这个标签表示是否允许放宽共享文件的一致性要求。当 I/O 同步执行时,允许那些管理自身一致性的应用程序(例如,通过写入同一文件的不同部分,HPC 工作负载[27]中的一个通用模式)来缓冲写入或缓存读取。如果需要,应用程序可以显式地与另外两个调用同步: lazyio_propagate 将把给定的字节范围刷新到对象存储,同时 lazyio_synchronize 将确保先前的操作反映在任何后续读取中。因此,Ceph 同步模型通过同步 I/O 在客户端之间提供正确的读写和共享写语义,并扩展应用程序接口以放松对性能敏感的分布式应用程序的一致性,从而保持其简单性。
3.3 名空间操作
与文件系统命名空间的客户端交互由元数据服务器集群管理。MDS 同步应用读操作(例如 readdir 、stat)和更新(例如unlink、chmod),以确保序列化、一致性、正确性和安全性。为了简单起见,不向客户端发出元数据锁或租约。特别是对于HPC工作负载,回调提供了最小的好处,但其复杂性的潜在成本很高。
相反,Ceph 针对最常见的元数据访问场景进行了优化。readdir 后跟每个文件的 stat (例如ls -l)是一种非常常见的访问模式,也是大目录中的性能杀手。 Ceph 中的 readdir 只需要一个 MDS 请求,它将获取整个目录,包括 inode 内容。默认情况下,如果 readdir 后面紧跟着一个或多个统计信息,则返回短暂缓存的信息;否则将丢弃该信息。虽然这稍微放宽了连贯性,因为干预的 inode 修改可能会被忽视,但我们很乐意用这种方式来换取性能的大幅提升。这个行为是由 readdir plus[31]扩展显式捕获的,它返回带有目录项的 lstat 结果(正如 getdir 的某些特定于操作系统的实现所做的那样)。
Ceph 可以通过更长时间缓存元数据来进一步放宽一致性,这与NFS的早期版本非常相似,后者通常缓存30秒。然而,这种方法以一种通常对应用程序至关重要的方式破坏了一致性,例如那些使用 stat 来确定文件是否被错误地更新了,或者最终等待旧的缓存值超时的操作。
相反,我们选择再次提供正确的行为,并在对性能产生不利影响的情况下扩展接口。对当前由多个客户端打开进行写入的文件执行 stat 操作最清楚地说明了这一选择。为了返回正确的文件大小和修改时间,MDS 会取消任何写入功能,以便立即停止更新,并从所有写入程序中收集最新大小和 mtime 值。最大值随 stat 回复返回,并重新发布功能以允许进一步的进展。虽然停止多个写操作可能看起来很激烈,但有必要确保适当的串行化(对于单个写操作,可以从写入客户端检索到正确的值,而不会中断进程)。不需要一致性行为的应用程序(POSIX 接口与需求不符)可以使用 stat lite[31],后者使用位掩码指定哪些 inode 字段不需要一致。
4. 动态分布式元数据
元数据操作通常占到文件系统工作负载的一半[22],并且位于关键路径中,这使得 MDS 集群对整体性能至关重要。元数据管理在分布式文件系统中也提出了一个关键的扩展挑战:尽管随着存储设备的增加,容量和总 I/O 速率几乎可以任意扩展,但元数据操作涉及到更大程度的相互依赖性,这使得可伸缩的一致性和一致性管理变得更加困难。
Ceph 中的文件和目录元数据非常小,几乎完全由目录条目(文件名)和 inode (80字节)组成。与传统的文件系统不同,不需要文件分配元数据,对象名使用 inode 编号构造,并使用 CRUSH 分发到 OSD。这简化了元数据工作负载,并允许我们的 MDS 高效地管理非常大的工作文件集,而不依赖于文件大小。我们的设计进一步寻求通过使用两层存储策略来最小化与元数据相关的磁盘 I/O ,并通过动态子树分区来最大限度地提高局部性和缓存效率[30]。
4.1 元数据存储
尽管 MDS 集群的目标是满足来自其内存缓存的大多数请求,但为了安全起见,元数据更新必须提交到磁盘。一组大的、有边界的、延迟刷新的日志允许每个 MDS 以高效和分布式的方式将其更新的元数据流式传输到 OSD 集群。每一个 MDS 日志(每个数百兆字节)还吸收重复的元数据更新(大多数工作负载都很常见),因此当旧日志条目最终被刷新到长期存储中时,许多日志条目已经过时。虽然我们的原型还没有实现 MDS 恢复,但日志的设计是这样的:在 MDS 发生故障时,另一个节点可以快速地重新扫描故障节点内存缓存中的日志或关键内容(用于快速启动),并在这样做时恢复文件系统状态。
此策略提供了两个方面的最佳选择:以高效(顺序)方式将更新流式传输到磁盘,并大大减少了重新写入工作负载,从而使长期的磁盘存储布局能够为将来的读访问进行优化。特别是, inode 直接嵌入到目录中,允许 MDS 通过单个 OSD 读取请求预取整个目录,并利用大多数工作负载中存在的高度目录局部性[22]。每个目录的内容都使用与元数据日志和文件数据相同的分割和分配策略写入 OSD 集群。inode 编号是在一定范围内分配给元数据服务器的,并且在我们的原型中被认为是不可变的,尽管将来在删除文件时可以很容易地回收它们。一个辅助的锚定表[28]使具有多个硬链接的稀有索引节点能够按索引节点编号进行全局寻址,而不会妨碍具有巨大、稀疏填充和笨重 inode 表的单链接文件的绝大多数常见情况。
4.2 动态子树分区
我们的主要拷贝缓存策略是使一个权威的 MDS 负责管理缓存一致性和序列化任何给定元数据的更新。虽然大多数现有的分布式文件系统都采用某种形式的基于静态子树的分区来释放这种权限(通常迫使管理员将数据集划分为更小的静态"卷"),但一些最新的和实验性的文件系统已经使用哈希函数来分发目录和文件元数据[4],有效地牺牲了负载分配的局部性。这两种方法有严重的局限性:静态子树分区无法处理动态工作负载和数据集,而散列则破坏了元数据的局部性和高效元数据预取和存储的关键机会。
Ceph 的 MDS 集群基于一个动态子树分区策略[30],该策略在一组节点上自适应地分布缓存的元数据,如图2所示。每个 MDS 使用具有指数时间衰减的计数器来测量目录层次结构中元数据的访问热度。任何操作都会将受影响 inode 及其所有祖先上的计数器递增到根目录,为每个 MDS 提供一个描述最近负载分布的加权树。MDS 加载值(load values)会定期进行比较,并将目录层次结构中大小适合的子树被迁移,以保持工作负载能均匀分布(keep the work load evenly distributed)。共享的长期存储和精心构造的命名空间锁的组合允许这样的迁移通过将内存缓存的适当内容传输到新的授权来进行,而对一致性锁或客户端功能的影响最小。为了安全起见,导入的元数据被写入新 MDS 的日志中,而两端的附加日志条目确保了权限的转移不会受到中间故障的影响(类似于两阶段提交)。生成的基于子树的分区保持粗糙,以最小化前缀复制开销并保持局部性。
图2. Ceph 根据当前工作负载动态地将目录层次结构的子树映射到元数据服务器。只有当单个目录成为热点时,才会在多个节点之间散列。
当元数据跨多个 MDS 节点复制时, inode 内容被分为三组,每个组具有不同的一致性语义:security(owner,mode)、file(size、mtime)和immutable( inode number,ctime,layout)。虽然不可变(immutable)字段永远不变,但安全锁和文件锁由独立的有限状态机控制,每个状态机都有一组不同的状态和转换,旨在适应不同的访问和更新模式,同时最大限度地减少锁争用。例如,在路径遍历期间,安全检查需要所有者和模式,但很少更改,只需要很少的状态,而文件锁反映了更广泛的客户端访问模式,因为它控制 MDS 发出客户端功能的能力。
4.3 流量控制
在多个节点上划分目录层次结构可以平衡广泛的工作负载,但不能总是处理热点或突发访问(flash crowds),因为许多客户端访问同一目录或文件。Ceph 利用其元数据热度的信息(knowledge of metadata popularity),仅在需要时才为热点提供广泛的分布,并且在一般情况下不会导致相关的开销和目录位置的丢失。大量读取的目录(例如,许多打开的目录)的内容被选择性地跨多个节点复制以分配负载。特别大的目录或经历了繁重的写工作负载(例如,许多文件创建)在集群中按文件名散列它们的内容,以牺牲目录位置为代价实现均衡分布。这种自适应方法允许 Ceph 包含广泛的分区粒度,该策略在文件系统的特定环境和部分中分别捕捉粗细粒度分区是最有效。
每个 MDS 响应都向客户端提供相关 inode 及其祖先的授权和复制的更新信息,从而允许客户端了解与其交互的文件系统部分的元数据分区。未来的元数据操作将基于给定路径的最深的已知前缀针对授权(用于更新)或随机副本(用于读取)。通常情况下,客户会知道不受欢迎(未复制)元数据的位置,从而能够直接与相应的 MD 联系。然而,访问热点元数据的客户端被告知元数据位于不同或多个 MDS 节点上,从而有效地限制了相信任何特定元数据片段驻留在任何特定 MDS 上的客户端数量,从而在潜在热点和突发访问出现之前将其分散。
5. 分布式对象存储
从较高的层次来看,Ceph 客户端和元数据服务器将对象存储集群(可能有数万或几十万个 OSD )视为单个逻辑对象存储和名称空间。Ceph 可靠的自主分布式对象存储(Reliable Autonomic Distributed Object Store,RADOS)通过将对象复制、群集扩展、故障检测和恢复的管理以分布式的方式委托给 OSD 来实现容量和总体性能的线性扩展。
5.1 通过 CRUSH 算法进行数据映射
Ceph 必须在一个由数千个存储设备组成的不断发展的集群中分布数 PB 的数据,以便有效地利用设备存储和带宽资源。为了避免不平衡(例如,最近部署的设备大多空闲或空闲)或负载不对称(例如,新的、热的数据仅在新设备上),我们采用了随机分配新数据、将现有数据的随机子样本迁移到新设备、以及从移除的设备均匀地重新分配数据的策略。这种随机方法是健壮的,因为它在任何工作负载下都表现得同样好。
Ceph 首先使用一个简单的散列函数将对象映射到布局组(Placement Groups,PGs),并使用可调的位掩码来控制 PGs 的数量。我们选择一个值,该值给每个 OSD 大约 100 PGs,以平衡 OSD 利用率的差异与每个 OSD 维护的与复制相关的元数据的数量。然后使用 CRUSH (可伸缩哈希下的受控复制)[29]将放置组分配给 OSD ,这是一种伪随机数据分布函数,它有效地将每个 PG 映射到 OSD 的有序列表中,在 OSD 上存储对象副本。这与传统方法(包括其他基于对象的文件系统)的不同之处在于,数据放置不依赖于任何块或对象列表元数据。要定位任何对象,CRUSH 只需要放置组(Placement Group)和 OSD 集群映射(cluster map):组成存储集群的设备的紧凑的层次描述。这种方法有两个主要优点:第一,它是完全分布式的,这样任何一方(客户端、OSD 或 MDS)都可以独立计算任何对象的位置;第二,map 很少更新,实际上消除了与分发相关的元数据的任何交换。这样一来,CRUSH 同时解决了数据分发问题("我应该在哪里存放数据")和数据位置问题("我在哪里存了数据")。从设计上讲,对存储集群的微小更改对现有的 PG 映射几乎没有影响,从而最大限度地减少了由于设备故障或集群扩展而导致的数据迁移。
图3. 文件被分条到多个对象,分组到放置组(PGs),并通过 CRUSH (一种专门的副本放置功能)分发到 OSD。
群集映射层次结构的结构与群集的物理或逻辑组合和潜在故障源保持一致。例如,对于一个安装,可以形成一个四层的层次结构,其中包括装满 OSD 的机架式机柜、装满机架的机架式机柜和一排排机柜。每个 OSD 也有一个权重值来控制分配给它的相对数据量。 CRUSH 基于放置规则将 PG 映射到 OSD 上,这些规则定义了复制级别和对放置的任何限制。例如,一个可能会在三个 OSD 上复制每个 PG ,它们都位于同一行(以限制行间复制流量),但被分隔到不同的机柜中(以尽量减少对电源电路或边缘交换机故障的暴露)。集群映射还包括停机或不活动设备的列表和一个 epoch 编号,该数字随着时间的推移而增加。所有的 OSD 请求被标记为客户端的 map epoch,这样所有各方就可以就当前的数据分发达成一致。增量的 map 更新在协作的 OSD 之间共享,如果客户端的 map 过期了,则在 OSD 上进行响应。
5.2 备份
与 Lustre[4]这样的系统不同, Lustre[4]假设可以使用诸如 RAID 或SAN上的故障转移等机制来构造足够可靠的 OSD ,我们假设在 PB 级别或 EB 级别的情况下,系统故障将是常态,而不是例外,并且在任何时间点,几个 OSD 都可能无法操作。为了以可扩展的方式维护系统可用性并确保数据安全,RADOS使用主副本复制[2]的一个变体管理自己的数据复制,同时采取措施尽量减少对性能的影响。
数据按照放置组进行复制,每个放置组都映射到 n 个 OSD 的有序列表(n-way复制)。客户端将所有写操作发送到对象的 PG (主)中的第一个未发生故障的 OSD ,后者为该对象和 PG 分配一个新的版本号,并将写操作转发给任何其他副本 OSD 。在每个复制副本应用更新并响应主副本之后,主副本将在本地应用更新,并向客户端确认写入操作。读取是针对初级阶段的。这种方法避免了客户端在副本之间同步或序列化的任何复杂性,这些复杂性在存在其他写入程序或故障恢复时可能非常繁重。它还将复制所消耗的带宽从客户端转移到 OSD 集群的内部网络,我们期望在那里有更多的可用资源。由于任何后续的恢复(见第5.5节)都将可靠地恢复副本一致性,因此将忽略中间的副本 OSD 故障。
图4. RADOS 在对复制对象的所有的 OSD 上的缓冲区缓存应用写操作后用 ACK 响应。只有在安全地提交到磁盘之后,才会向客户端发送最终提交通知。
5.3 数据安全
在分布式存储系统中,数据写入共享存储的原因主要有两个。首先,客户端有兴趣让其他客户端看到他们的更新。这应该很快:写操作应该尽快可见,尤其是当多个写操作或混合读写操作强制客户端同步操作时。第二,客户端希望明确地知道它们所写的数据是安全地复制到磁盘上的,并且能够在断电或其他故障中幸存下来。RADOS 在确认更新时将同步与安全性分离,允许 Ceph 实现低延迟更新以实现高效的应用程序同步和定义良好的数据安全语义。
5.4 故障检测
及时的故障检测对于维护数据安全是至关重要的,但是当集群扩展到数千个设备时,可能会变得很困难。对于某些故障,例如磁盘错误或损坏的数据,OSD 可以自我报告(self-report)。然而,如果发生故障,使得 OSD 无法在网络上访问,则需要主动监控,RADOS 通过让每个 OSD 监视与其共享 PG 的对等方来进行分发。在大多数情况下,现有的复制流量充当活跃性的被动确认,没有额外的通信开销。如果 OSD 最近没有收到对等方的消息,则会发送显式Ping。
RADOS 考虑 OSD 活性的两个维度: OSD 是否可达,以及是否通过 CRUSH 为其分配数据。没有响应的 OSD 最初会被标记为下线,任何主要职责(更新序列化、复制)都会临时传递给每个放置组中的下一个 OSD 。如果 OSD 不能快速恢复,它将被标记出数据分发,并且另一个 OSD 将加入每个 PG 以重新复制其内容。具有挂起操作且 OSD 失败的客户端只需重新提交到新的主节点。
由于各种各样的网络异常可能会导致 OSD 连接的间歇性故障,所以一个小型的监控器集群集中收集故障报告并过滤掉暂时或系统性的问题(如网络分区)。监视器(仅部分实现)使用选举、活跃的 peer 监控、短期租约和两阶段提交来共同提供对集群映射的一致和可用的访问。当映射更新以反映任何故障或恢复时,受影响的 OSD 将得到增量的 map 更新,然后通过利用现有的 OSD 间通信传播到整个集群。分布式检测允许快速检测而不必过度增加监视器的负担,同时解决与集中仲裁不一致的问题。最重要的是,RADOS通过将 OSD 标记为down而不是out(例如,在所有 OSD 的一半断电后),避免了由于系统问题而引起的广泛的数据重复复制。
5.5 恢复和集群更新
OSD 群集映射将因 OSD 故障、恢复和显式群集更改(如部署新存储)而更改。Ceph 以相同的方式处理所有这些更改。为了便于快速恢复,OSD 为每个对象维护一个版本号,并为每个 PG 维护一个最近更改的日志(更新或删除对象的名称和版本)(类似于 Harp[14]中的复制日志)。
当一个活动的 OSD 收到一个更新的集群映射时,它会迭代所有本地存储的放置组,并计算压缩映射,以确定它所负责的是哪一个,无论是主还是副本。如果一个 PG 的成员身份发生了变化,或者 OSD 刚刚启动,OSD 必须与 PG 的其他 OSD 对等。对于复制的 PG , OSD 向主服务器提供其当前 PG 版本号。如果 OSD 是 PG 的主服务器,它将收集当前(和以前)副本的 PG 版本。如果主服务器缺少最新的 PG 状态,它将从 PG 中当前或以前的 OSD 检索最近 PG 更改的日志(或完整的内容摘要,如果需要),以确定正确的(最新的) PG 内容。主服务器然后向每个副本发送一个增量日志更新(或完整的内容摘要,如果需要),这样所有参与方都知道 PG 的内容应该是什么,即使它们在本地存储的对象集可能不匹配。只有在主服务器确定正确的 PG 状态并与任何副本共享后,才允许对 PG 中的对象进行 I/O 。 OSD 独立负责从 peer 对象检索丢失或过时的对象。如果 OSD 接收到对过时或丢失对象的请求,它会延迟处理并将该对象移到恢复队列的前面。
例如,假设 OSD1崩溃并被标记为down, OSD2接管了 PGA的主节点。如果 OSD1恢复,它将在引导时请求最新的映射,并且监视器会将其标记为启动。当 OSD2接收到生成的映射更新时,它将意识到它不再是 PGA的主映射,并将 PGA版本号发送给 OSD1。 OSD1将从 OSD2检索最近的 PGA日志项,告诉 OSD2它的内容是最新的,然后在后台恢复任何更新的对象时开始处理请求。
由于故障恢复完全由单个 OSD 驱动,因此受故障 OSD 影响的每个 PG 将与(很可能)不同的替换 OSD 并行恢复。这种方法基于快速恢复机制(FaRM)[37],减少了恢复时间并提高了总体数据安全性。
5.6 利用 EBOFS 的对象存储
尽管各种分布式文件系统使用本地文件系统像 ext3 来管理底层存储[4,12],但我们发现它们的接口和性能不太适合对象工作负载[27]。现有的内核接口限制了我们理解何时在磁盘上安全提交对象更新的能力。同步写入或日志记录提供了所需的安全性,但只会带来严重的延迟和性能损失。更重要的是,POSIX 接口无法支持原子数据和元数据(例如,属性)更新事务,这对于维护 RADOS 的一致性非常重要。
相反,每个 Ceph OSD 都使用 EBOFS 管理其本地对象存储,EBOFS 是一种基于扩展和 B树的对象文件系统(基于扩展的用户空间对象文件系统)。完全在用户空间中实现 EBOFS 并直接与原始块设备交互允许我们定义自己的底层(low-level)对象存储接口和更新语义,这将更新序列化(用于同步)与磁盘上提交(为了安全起见)分开。EBOFS 支持事务处理(例如,对多个对象进行写入和属性更新),更新函数在更新内存缓存时返回,同时提供异步提交通知。
用户空间方法除了提供更大的灵活性和更容易的实现之外,还避免了与 Linux vfs 和页面缓存(pagecache)的繁琐交互,这两个都是为不同的接口和工作负载而设计的。虽然大多数内核文件系统会在一段时间间隔后延迟将更新刷新到磁盘,但 EBOFS 会积极地计划磁盘写入,并在后续更新变得多余时选择取消挂起的 I/O 操作。这为我们的底层磁盘(low-level disk)调度程序提供了更长的 I/O 队列,并相应地提高了调度效率。用户空间调度器还使得最终确定工作负载的优先级(例如,客户端 I/O 与恢复)或提供服务质量保证变得更加容易[36]。
EBOFS 设计的核心是一个健壮、灵活和完全集成的B-tree服务,用于定位磁盘上的对象、管理块分配和索引集合(放置组)。块分配是根据区段开始和长度对而不是块列表来执行的,从而保持元数据的紧凑性。磁盘上的空闲块区按大小进行分块,并按位置排序,这使得 EBOFS 能够快速地找到磁盘上写位置或相关数据附近的可用空间,同时也限制了长期碎片化。除了每个对象块分配信息之外,所有元数据都保存在内存中,以提高性能和简化性(内存非常小,即使对于大容量内存也是如此)。最后,EBOFS 积极地执行写时拷贝:除了超级块更新,数据总是写到磁盘的未分配区域。
参考文献
可以根据翻译中的参考文献标号进行查阅,这里就不一一列举了。论文原文题目在开头,很容易就能下载到原文。本文只翻译了前 5 章,后续章节是性能测试,有兴趣的读者可以查看原文。