目录

1、简介

2、设计与实施

3、设计选择讨论

4、价值评估

5、相关工作

6、结论和期望工作


CFS: A Distributed File System for Large Scale Container Platforms

CFS:一个面向大规模容器平台的分布式文件系统

摘要

我们提出了一个面向大规模容器平台的分布式文件系统 CFS。CFS 支持顺序和随机文件访问,对大文件和小文件都进行了优化存储,并针对不同的写入场景采用不同的复制协议,以提高复制性能。它采用元数据子系统,根据内存使用情况在不同的存储节点上存储和分发文件的元数据。这种元数据放置策略避免了在容量扩展期间重新平衡数据的需求。CFS 还为 POSIX 兼容的 API 提供了宽松的语义和元数据原子性,以提高系统性能。我们与 Ceph 进行了全面的比较,Ceph 是在容器平台上广泛使用的分布式文件系统。实验结果表明,在测试 7 种常用的元数据操作时,CFS 的性能平均提高了 3 倍左右。此外,CFS 在具有多个客户端和进程的高度并发环境中表现出更好的随机读/写性能。

1、简介

在过去的几年里,容器微服务已经彻底改变了云环境和架构[1,3,17]。随着应用程序可以通过连续交付(through continuous delivery)更快地构建、部署和管理,越来越多的公司开始将遗留应用程序和核心业务功能转移到容器环境中。

运行在每组容器上的微服务通常独立于本地磁盘存储。虽然将计算与存储分离允许公司以更有效的方式扩展容器资源,但这也带来了对单独存储的需求。因为:(1)容器可能需要保存应用程序数据,即使在它们关闭之后。(2)同一文件可能需要由不同的容器同时访问。(3)存储资源可能需要由不同的服务和应用程序共享。如果没有持久化数据的能力,容器在许多工作负载中的使用可能有限,尤其是在有状态的应用程序中。

一种选择是通过容器存储接口(Container Storage Interface,CSI)将现有的分布式文件系统带到云本地环境中,该接口已被 Kubernetes[5] 和 Mesos[13] 等各种容器协调器支持,或者通过一些存储协调器(如Rook2)支持。在寻找这样一个分布式文件系统时,拥有运行在 JD 容器平台上的应用程序和服务的工程团队提供了很多有价值的反馈。然而,在性能和可扩展性方面,这些反馈也让我们很难直接采用任何现有的开源解决方案。

例如,为了降低存储成本,通常需要从 同一个共享存储基础架构 中为不同的应用程序和服务提供服务。因此,组合工作负载中的文件大小可以从几千字节到几百GB不等,并且可以按顺序或随机方式访问这些文件。然而,许多分布式文件系统都针对大文件(如 HDFS[22] )或小文件(如 Haystack[2] )进行了优化,但很少有系统同时针对大文件和小文件优化了存储[6,12,20,26]。此外,这些文件系统通常采用一刀切的复制协议,这可能无法为不同的写入场景提供优化的复制性能。

此外,大量 Client 可能同时对文件进行大量访问。大多数文件操作(如创建、追加或删除文件)都需要更新文件元数据。因此,由于硬件限制,存储所有文件元数据的单个节点很容易成为性能或存储瓶颈[22,23]。通过使用一个单独的集群来存储元数据,可以解决这个问题,但是以这种方式的大多数现有工作[4]都需要在容量扩展期间重新平衡存储节点,这可能会严重降低读/写性能。

最后,尽管拥有与 POSIX 兼容的文件系统接口可以大大简化上层应用程序的开发,但是 POSIX I/O 标准中定义的强一致性语义也会极大地影响性能。大多数与 POSIX 兼容的文件系统通过提供宽松的 POSIX 语义来缓解这个问题,但是同一文件的 inode(索引节点)和 dentry(目录项)之间的原子性需求仍然会限制它们在元数据操作上的性能

针对这些问题,本文提出了一种面向大规模容器平台的分布式文件系统 Chubao 文件系统(CFS)。CFS 是用 Go 编写的,代码在 https://github.com/ChubaoFS/cfs。一些关键功能包括:

- 通用高性能存储引擎(General-Purpose and High Performance Storage Engine)。CFS 提供了一个通用存储引擎,可以在不同的文件访问模式下以优化的性能高效地存储大文件和小文件。它利用 Linux[21] 中的 punch hole 接口异步地释放掉被删除的小文件所占用的磁盘空间,这大大简化了处理小文件删除的工程工作。

- 场景感知复制(Scenario-Aware Replication)。与现有的任何开放源代码解决方案在任何时候只允许一个复制协议[22,26,27]不同,CFS 采用了两种基于不同写场景的强一致性复制协议(即追加和覆盖)来提高复制性能。

- 基于利用率的元数据分配(Utilization-Based Metadata Placement)。CFS 使用一个单独的集群来根据内存使用情况在不同的存储节点上存储和分发文件元数据。这种基于利用率的布局的一个优点是,它不需要在容量扩展期间进行任何元数据重新平衡。尽管 MooseFS[23] 中也使用了类似的思想来选择块服务器,但据所知,CFS 是第一个将此技术应用于元数据分配的开源解决方案。

- 宽松的 POSIX 语义和元数据原子性(Relaxed POSIX Semantics and Metadata Atomicity)。在与 POSIX 兼容的分布式文件系统中,在多个 Client 节点上服务多个进程的行为应该与在具有直接连接存储的单个节点上服务多个进程的本地文件系统的行为相同。CFS 提供了与 POSIX 兼容的 API。然而,POSIX 一致性语义,以及同一文件的 inode 和 dentry 之间的原子性要求,已经被仔细放宽,以便更好地与应用程序的需求保持一致,并提高系统性能。

2、设计与实施

图 1 所示,CFS 由 一个元数据(metadata)子系统一个数据子系统 一个资源管理器 组成,并且可以被不同的 Client 作为一组托管在容器上的应用程序进程来访问。

元数据子系统存储文件元数据,由一组 meta 节点组成。每个元节点由一组元分区组成。数据子系统存储文件内容,由一组数据节点组成。每个数据节点由一组数据分区组成。我们将在下面几节中详细介绍这两个子系统。

volume 是 CFS 中的一个逻辑概念,由一组元分区和数据分区组成每个分区只能分配给一个 volume。从 Client 的角度来看,volume 可以被视为包含容器可访问的数据的文件系统实例。一个 volume 可以装载到多个容器上,这样文件就可以在不同的 Client 之间同时共享。volume 需要在任何文件操作开始之前创建。

资源管理器 通过处理不同类型的任务( 例如创建和删除分区、创建新 volume 以及添加/删除节点 )来管理文件系统。它还跟踪状态,比如内存和磁盘利用率,以及集群中的元数据节点和数据节点的活跃度。资源管理器有多个副本,其中的强一致性由一致算法( 如Raft[16] )维护,并持久化到键值存储( 如rocksdb )进行备份和恢复。

2.1 元数据存储

元数据子系统可以看作是文件元数据在内存中的分布式数据存储。

2.1.1 内部结构

元数据子系统由一组 meta 节点组成,每个 meta 节点可以有数百个元分区。每个元分区在内存中存储同一个 volume 的文件的 inode 和 dentry,并使用两个名为 inodeTree 和 dentryTree 的 B 树来快速查找。inodeTree 按 inode id 索引,dentryTree 按父 inode id 和 dentry 名称编制索引。

下面的代码片段显示了 CFS 中的元分区inodedentry 的定义。

type metaPartition struct {config          *MetaPartitionConfig;size            uint64;dentryTree      *BTree               // btree for dentries;inodeTree       *BTree.              // btree for inodes;raftPartition   raftstore.Partition;freeList        *freeList            // free inode list;vol             *Vol;...                                  // other fields
}
type inode struct {inode       uint64  // inode idtype        uint32  // inode typelinkTarget  []byte  // symLink target namenLink       uint32  // number of linksflag        uint32...                 // other fields
}
type dentry struct {parentId  uint64   // parent inode idname      string   // name of the dentryinode     uint64   // current inode idtype      uint32   // dentry type
}

2.1.2 基于 Raft 的复制

文件写入期间的复制是按照元分区执行的。副本之间的强一致性由 Raft 共识协议[16]的修订版 MultiRaft 来保证,与原始版本相比,该协议的优点是减少了网络上的心跳流量

2.1.3 故障恢复

在内存中被缓存的元分区通过快照和日志保存到本地磁盘上,用于备份和恢复。一些技术,如日志压缩被用来减少日志文件的大小和缩短恢复时间。

值得注意的是,在元数据操作期间发生的故障可能会导致一个孤立的 inode,而该 inode 没有与 dentry 关联。这个 inode 占用的内存和磁盘空间很难释放。为了将这种情况发生的可能性降到最低,客户端总是在失败后发出重试,直到请求成功或达到最大重试限制。

2.2 数据存储

数据子系统针对大文件和小文件的存储进行了优化,这些文件可以按顺序或随机方式访问。

2.2.1 内部结构

数据子系统由一组数据节点组成,每个数据节点都有一组数据分区。每个数据分区会存储一组分区的元数据,例如分区 id 和副本地址。它还有一个称为 extent store 的存储引擎(参见图2),它由一组称为 extense 的存储单元构成。每个区段的 CRC 缓存在内存中,以加快数据完整性的检查。下面的代码片段显示了 CFS 中数据分区的结构。

type dataPartition struct {clusterID       string;volumeID        string;partitionID     uint64;partitionStatus int;partitionSize   int;replicas        []string;             // replica addressesdisk            *Disk;isLeader        bool;isRaftLeader    bool;path            string;extentStore     *storage.ExtentStore;raftPartition   raftstore.Partition;...                                    // other fields
}

大文件和小文件的存储方式不同,用阈值 t(默认为128kb)来判断是否应该将文件视为 "小文件",即大小小于或等于阈值的文件将被视为 "小文件"。t 的值在启动时是可配置的,并且在数据传输过程中通常与数据包大小一致,以避免分组组装或拆分。

2.2.2 大文件存储

对于大文件,内容存储为一个或多个块的序列,这些块可以分布在不同数据节点上的不同数据分区上。将新文件写入数据块存储区时,数据总是以新数据块的零偏移量写入,这样就不需要在数据块内进行偏移量。文件的最后一个盘区不需要通过填充来填充其大小限制,并且从不存储来自其他文件的数据。

2.2.3 小文件存储

将多个小文件的内容聚合并存储在单个块中,块中每个文件内容的物理偏移量记录在相应的块的元数据中。CFS 依赖 fallocate() 来异步释放被删除文件所占用的磁盘空间。这种设计的优点是不需要实现垃圾收集机制,因此避免在块[2]中使用从逻辑偏移量到物理偏移量的映射。注意,这与删除大文件不同,在删除大文件时,可以直接从磁盘中删除文件的区段。

2.2.4 场景感知复制

虽然可以简单地使用基于 Raft 的复制来保证强一致性,但 CFS 在数据子系统中采用了两种复制协议,以在性能和代码重用性上取得更好的折衷。

在文件写入期间,复制是按分区执行的。根据文件写入模式,CFS 采用不同的强一致性复制策略。具体来说,对于顺序写入(即append),我们使用主备份复制[25,27],对于覆盖,我们使用基于MultiRaft复制协议,类似于元数据子系统中使用的协议。

一个原因是主备份复制不适合覆盖,因为可能需要损害复制性能。具体地说,在覆盖期间,将至少创建一个新的扩展数据块来存储新的文件内容,并且一些原始扩展数据块将逻辑地拆分为多个碎片,这些碎片通常像链接列表一样链接在一起。在此链接列表中,指向原始碎片的指针将替换为与新创建的盘区关联的指针。随着越来越多的文件内容被覆盖,最终数据分区上会有太多需要碎片整理的碎片,这可能会严重影响复制性能。

另一个原因是,众所周知,基于 MultiRaft 的复制存在写放大问题,因为它引入了写入日志文件的额外 IO,这可能会直接影响读写性能。但是,由于我们的应用程序和微服务的覆盖操作的需求比顺序写入操作的需求少的多,所以这个性能问题是可以容忍的。

2.2.5 故障恢复

由于存在两种不同的复制协议,当发现副本上的故障时,我们首先通过检查和调整所有数据块来启动主备份复制恢复过程。一旦处理完成,我们就可以在基于 MultiRaft 的复制中启动恢复过程。

应该注意的是,在顺序写入期间,只要不将过时数据返回给 Client ,就允许在分区上使用(存在)过时的数据。这是因为,在本例中,偏移量处的数据提交也表示偏移量之前的所有数据的提交。因此,我们可以使用偏移量来指示所有副本已提交的数据部分,Leader 总是将所有副本提交的最大偏移量返回给客户端,然后客户端在相应的元节点上更新这个偏移量。当 Client 请求读取时,元节点只提供副本的地址以及所有副本已提交的数据的偏移量,而不管是否存在尚未完全提交的过时数据。

因此,不需要恢复副本上不一致的数据部分。如果 Client 发送了一个 k MB 文件的写入请求,并且所有副本只提交了第一个p MB,则该 Client 将对剩余的 k−p MB 数据重新发送一个写请求到不同数据分区/节点中的数据块。这种设计极大地简化了当文件按顺序写入 CFS 时保持强复制一致性的实现。

2.3 资源管理器

资源管理器通过处理不同类型的任务来管理文件系统。

2.3.1 基于利用率的设置

一个重要的任务是在不同的节点之间分发文件元数据和内容。在 CFS 中,我们采用了基于利用率的布局,其中文件元数据和内容根据内存/磁盘使用情况分布在存储节点上。这大大简化了分布式文件系统中的资源分配问题。

据我们所知,CFS 是第一个将这种思想用于元数据分配的开源解决方案。一些常用的元数据分配策略,如哈希和子树分区[4]通常需要在添加服务器时移动不成比例的元数据。这种数据迁移在容器环境中可能是一个令人头痛的问题,因为容量扩展通常需要在短时间内完成。其他方法,如 lazyhybrid[4] 和 dynamicsubtree partition[28] 延迟移动数据或使用代理来隐藏数据迁移延迟。但这些解决方案也为未来的开发和维护带来了额外的工程工作量。与这些方法不同,基于利用率的布局尽管简单,但在容量扩展期间添加新的存储节点时,不需要对数据进行任何重新平衡。此外,由于分布的均匀性,可以减少多个 Client 同时访问同一存储节点上的数据的机会,这有可能提高文件系统的性能稳定性。具体而言,我们基于利用率的安置工作如下:

首先,在创建 volume 之后, Client 向资源管理器请求一定数量的可用元数据和数据分区。这些分区通常位于内存/磁盘利用率最低的节点上。但是,需要注意的是,在编写文件时, Client 只需选择从资源管理器分配的元数据和数据分区中随机抽取。客户端不采用类似的基于利用率的方法的原因是为了避免与资源管理器的通信,以获取每个分配节点的最新利用率信息。

其次,当资源管理器发现一个 volume 中的所有分区即将满时,它会自动向该 volume 添加一组新分区。这些分区通常位于内存/磁盘利用率最低的节点上。请注意,当分区已满或达到阈值(即,元分区上的文件数或数据分区上的盘区数)时,此分区上不能存储任何新数据,尽管它仍然可以修改或删除。

2.3.2 元分区拆分

在分割元分区时,资源管理器还需要处理一个特殊的需求。特别是,如果一个元分区即将达到存储 inode 和 dentry 数量的上限,则需要执行拆分任务,以确保存储在新创建分区中的 inode id 与存储在原始分区中的 inode id 是唯一的。

我们解决方案的伪代码在 算法1 中给出,资源管理器首先在上界端预先切断元分区的 inode 范围,一个大于目前使用的最大 inode id 的值(表示为 maxinodeid),然后向 meta 节点发送一个分割请求:(1)将 inode id 范围从 1 到 end 更新为原始元分区,和(2)创建一个新的元分区,索引节点的范围从 end+1 到  ∞ 。因此,这两个元分区的 inode 范围分别为 [1,end] 和 [end+1,∞] 。如果需要创建另一个文件,那么它的 inode id 将在原始元分区中选择为 maxInode ID+1,或者在新创建的元分区中选择为 end+1。通过资源管理器和元节点之间的周期性通信,可以得到每个元分区的最大值。

2.3.3 异常处理

当对元数据/数据分区的请求超时时(例如,由于网络中断),其余副本被标记为只读。当元数据/数据分区不再可用时(例如,由于硬件故障),该分区上的所有数据最终将被手动迁移到新分区。此不可用性由节点报告的多个故障来标识。

2.4 Client

Client 已与 FUSE 集成,以在用户空间中提供文件系统接口。Client 进程完全在自己缓存的用户空间中运行。

为了减少与资源管理器的通信,Client 缓存分配给装入 volume 的可用元数据和数据分区的地址,这些地址可以在启动时获得,并定期将这些可用分区与资源管理器同步。

为了减少与元节点的通信,Client 在创建新文件时还缓存返回的 inode 和 dentry,并在文件成功写入数据节点后缓存数据分区 id、盘区 id 和偏移量。当打开文件进行读/写操作时,客户端将强制缓存元数据与元节点同步。

为了减少与数据节点的通信,Client 缓存最近识别的 Leader。我们的观察结果是,在读取文件时,客户端可能不知道哪个数据节点是当前的 Leader 节点,因为在故障恢复后, Leader 可能会发生变化。因此,客户端可能会尝试逐个向每个副本发送读请求,直到找到一个Leader。但是,由于 Leader 不经常更改,通过缓存最后一个标识的 Leader,在大多数情况下,客户端可以最小化重试次数。

2.5 优化

部署在 JD 的 CFS 集群主要有两种优化方式,解释如下:

2.5.1 尽量减少心跳。因为我们的生产环境可能会有大量的分区分布在不同的元数据和数据节点上,即使使用基于 MultiRaft 的协议,每个节点仍然可以从同一 Raft 组中的其他节点获得大量心跳,从而导致严重的通信开销。为了缓解这种情况,我们在节点上增加了一层称为 Raft set 的抽象层,以进一步减少 Raft 组之间要交换的心跳次数。具体来说,我们把所有的将节点分为若干个 Raft 组,每个 Raft 组维护自己的 Raft 组。在创建新分区时,我们更喜欢从同一个 Raft 集中选择副本。这样,每个节点只需与同一个 Raft 集中的节点交换心跳。

2.5.2 非持久性连接。可能有成千上万的 Client 访问同一个 CFS 集群,如果它们的所有连接都保持活动状态,这可能会导致资源管理器过载。为了防止这种情况发生,我们在每个 Client 和资源管理器之间使用非持久性连接。

2.6 元数据操作

在大多数现代 POSIX 兼容的分布式文件系统中[26],文件的 inode 和 dentry 通常驻留在同一个存储节点上,以保持目录的局部性。但是,由于基于利用率的元数据放置,在 CFS 中,同一文件的 inode 和 dentry 可能分布在不同的元数据节点上。因此,操作一个文件的 inode 和 dentry 通常需要作为分布式事务运行,这带来了可能会显著影响系统性能的开销。

我们的权衡是,只要 dentry 总是与至少一个 inode 相关联,就可以放宽这种原子性要求。CFS 中的所有元数据操作都基于此设计原则。缺点是有可能创建孤立的 inodes,并且很难从内存中释放出来。为了缓解这个问题,CFS 中的每个元数据操作工作流都经过了精心设计,以尽量减少出现孤立的 inode 的机会。实际上,元节点在内存中很少有很多的孤立 inode 。但是如果发生这种情况,管理员可以使用 fsck 等工具修复文件。

图3 演示了三个常见元数据操作的工作流,可以解释如下:

2.6.1 创建

创建文件时,客户端首先请求可用的元节点创建 inode。元节点为新创建的 inode 获取迄今为止在这个分区中尚未使用的最小 inode id,并相应地更新其最大的 inode id。只有成功创建 inode 后, Client 才能请求创建相应的 dentry。如果发生故障,客户端将发送一个 unlink 请求,并将新创建的 inode 放入本地孤立的 inode 列表中,当 meta 节点收到来自客户端的删除请求时,这些 inode 将被删除。注意 inode 和同一个文件的 dentry 不需要存储在同一个元节点上。

2.6.2 连接

当链接一个文件时, Client 首先要求 inode 的 meta 节点将 nlink 的值(关联的链接数)增加1,然后请求目标父 inode 的 meta 节点在同一个元分区上创建 dentry。如果创建 dentry 时发生故障,nlink 的值将减少1。

2.6.3 取消链接

当取消文件链接时,客户端首先请求相应的元节点删除 dentry。只有当这个操作成功时,客户端才会向 meta 节点发送一个 unlink 请求,以将目标 inode 中 nlink 的值减少一个。当达到某个阈值(文件为0,目录为2)时,客户端将此 inode 放入本地孤立 inode 列表中,当 meta 节点收到来自客户端的逐出请求时,该列表将被删除。请注意,如果降低 nlink 的值失败,客户端将执行多次重试。如果所有重试失败,这个 inode 最终将成为孤立的 inode,管理员可能需要手动解决该问题。

2.7 文件操作

CFS 放宽了 POSIX 一致性语义,也就是说,它没有提供强大的一致性保证,而是只确保文件/目录操作的顺序一致性,没有任何租赁机制来防止多个客户端写入同一个文件/目录。如果需要,它依赖于上层应用程序来维护更严格的一致性级别。

2.7.1 顺序写入

图 4 所示,为了按顺序写入文件,Client 首先从 缓存 中随机选择可用的数据分区,然后连续地将若干固定大小的数据包(例如,128 KB)发送给 Leader,每个数据包包括副本的地址目标盘区 id盘区中的偏移量文件内容。副本的地址由资源管理器以数组的形式提供,并缓存在客户端。此数组中项目的索引指示复制的顺序,即索引 0 处的地址为 Leader 的副本。因此, Client 总是可以在不引入额外通信开销的情况下向 Leader 发送写请求,并且如前一节所述,将按照以下顺序执行主备份复制。一旦 Client 接收到来自 Leader 的提交,它将立即更新本地缓存,并定期或在收到来自上层应用程序的系统调用 fsync() 时与 meta node 同步。

2.7.2 随机写入

CFS 中的随机写入已就绪。为了随机写入一个文件,客户端首先使用原始数据和新数据的偏移量来计算要附加的数据部分和要覆盖的部分数据,然后分别处理它们。在前一种情况下, Client 按照前面所述顺序写入文件。在后一种情况下,如 图 5 所示,文件在数据分区上的偏移量没有改变。

2.7.3 删除

删除操作是异步的。要删除文件, Client 向相应的元节点发送删除请求。meta 节点一旦接收到这个请求,就会更新目标 inode 中 nlink 的值。如果这个值达到某个阈值(文件为0,目录为2),目标 inode 将被标记为已删除(请参阅第 2.1 节中给出的 inode 结构)。稍后,将有一个单独的进程来清除这个 inode 并与数据节点通信以删除文件内容。

2.7.4 读

读取只能在 Raft 上进行(请注意,主备分组和 Raft 组可能不同)。要读取文件, Client 向相应的数据节点发送读取请求。这个请求是由 Client 缓存中的数据构造的,如数据分区 id、盘区 id、盘区偏移量等。

3、设计选择讨论

在本节中,我们将重点介绍构建 CFS 时所做的一些设计选择。

3.1 集中与分散

集中和分散是分布式系统的两种设计范式[7]。尽管前一个[10,22]相对容易实现,但单主机可能成为可伸缩性的瓶颈。相比之下,后一种方法通常更可靠,但实现起来也更复杂。

在设计 CFS 时,我们选择集中而不是分散,主要是因为它的简单性。然而,存储所有文件元数据的单个资源管理器限制了元数据操作的可伸缩性,元数据操作可能占典型文件系统工作负载的一半[18]。因此,我们使用一个单独的集群来存储元数据,这大大提高了整个文件系统的可伸缩性。从这个角度来看,CFS 的设计可以最大限度地减少资源管理器的参与,从而减少成为瓶颈的机会。诚然,即使有了这些努力,资源管理器的可伸缩性仍然会受到内存和磁盘空间的限制。但根据我们的经验,这从来不是一个问题。

3.2 单独的 Meta 节点与数据节点上的元数据

在一些分布式文件系统中,文件元数据和内容存储在同一台机器上[15,26](不管数据还是元数据,最后在底层 filestore 层,都以对象文件的形式存储)。在一些分布式文件系统中,元数据由专门的元数据服务器单独管理[11,22]。

在 CFS 中,我们选择使用一个单独的 meta 节点,并在内存中维护所有的文件元数据,以便快速访问。因此,我们可以为 meta 节点选择内存密集型机器,为数据节点选择磁盘密集型机器以提高成本效益。这种设计的另一个优点是部署的灵活性。如果一台机器同时有很大的内存和磁盘空间,我们总是可以将 meta 节点和数据节点物理地部署在一起。

3.3 一致性模型和保证

在 CFS 中,存储层和文件系统层有不同的一致性模型。

存储引擎通过主备份或基于 Raft 的复制协议来保证副本之间的强一致性。这个设计决定是基于这样的观察:前者不适合覆盖,因为复制性能需要受到损害;后者由于引入了写入日志文件的额外 IO,因此存在写放大问题。

尽管文件系统本身提供了与 POSIX 兼容的 API,但它有选择性地放宽了 POSIX 一致性语义,以便更好地与应用程序的需求保持一致,并提高系统性能。例如,POSIX 的语义定义了写操作必须是强一致的,也就是说,在系统可以保证任何其他读操作都能看到刚刚写入的数据。虽然这可以很容易地在本地完成,但是由于与锁争用/同步相关操作的配合导致性能下降,在分布式文件系统上确保如此强的一致性是非常困难的。CFS 放宽了 POSIX 的一致性,当不同的 Client 修改文件中不重叠的部分时,CFS 提供了一致性,但是如果两个 Client 试图修改文件的同一部分,它不提供任何一致性保证。这种设计是基于这样一个事实:在一个容器化的环境中,有许多情况下,POSIX 语义的刚性并不是必须的,即应用程序很少依赖文件系统来提供完全强致一性,并且两个独立的作业很少写入多用户系统上的公共共享文件。

4价值评估

Ceph 是一个分布式文件系统,在容器平台上得到了广泛的应用。在这一部分,我们进行了一组实验,从各个方面比较了 CFS 和 Ceph。

4.1 实验安装

我们实验所用机器的规格见 表1。对于 CFS,我们将 meta 节点和数据节点部署在同一个集群的 10 台机器上,以及一个具有 3 个副本的单个资源管理器。每台机器有 10 个元分区和 1500 个数据分区。对于 Ceph,我们有类似的设置,其中对象存储设备(OSD)和元数据服务器(MDS)部署在 10 台机器的同一个集群上。每台机器有 16 个 OSD 过程和 1 个 MSD 过程。Ceph 版本是 12.2.11,存储引擎配置为具有 TCP/IP 网络堆栈的 bluestore。除非另有说明,CFS 和 Ceph 在下面的实验中都使用默认配置。

4.2 元数据操作

在评估元数据子系统的性能和可伸缩性时,我们从 mdtest 中重点对 7 个常用的元数据操作进行了测试。说明见 表2。请注意,TreeCreation 和 TreeRemoval 测试主要集中在树结构中作为非叶节点的操作目录。

图6 显示了具有不同进程数的单 Client 环境中这些测试的 IOPS,图7 描绘了多 Client 环境中同一组测试的 IOPS,其中每个 Client 运行 64 个进程。请注意,Y 轴使用对数刻度,以便更好地说明不同的测试结果。

可以看出,当只有一个 Client 有一个进程时,Ceph 在 7 个测试中有 5 个优于 CFS(除了 DirStat 和 TreeRemoval 测试),但随着客户端和进程数量的增加,CFS 开始迎头赶上。当它到达 8 个 Client (每个 Client 运行 64 个进程)时,CFS 在 7 个测试中有  6  个优于 Ceph (除了 TreeCreation 测试)。表3 给出了使用 8 个客户端进行测试的详细 IOPS,它显示平均性能提高了大约  3  倍。从这个结果我们可以看出,随着 Client 和进程数量的增加,CFS 中基于利用率的元数据分配带来的性能优势可能会超过 Ceph 中基于目录位置的元数据分配的优势。

从 DirStat、TreeCreation 和 TreeRemoval 的结果中有一些观察结果,如下所述:

在 DirStat 测试中,CFS 在两种情况下(即单 Client 和多 Client )都比 Ceph 表现出更好的性能。这主要是因为它们以不同的方式处理 readdir 请求。在 Ceph 中,每个 readdir 请求后面都有一组 inodeGet 请求,用于从不同的 md 获取当前目录中的所有 inode。MDINO 通常在将来的访问中请求 MDINO。然而,在 CFS 中,为了减少通信开销,这些 inodeGet 请求被 batchInodeGet 请求所代替,并且结果被缓存在客户端,以便在不需要与同一组元节点进一步通信的情况下快速响应连续的请求。在多 Client 情况下(参见图7),性能的突然下降可能是由 CFS 中的 Client 缓存未命中引起的。

在 TreeCreation 测试中,Ceph 在单 Client 情况下的性能优于 CFS。但随着越来越多的客户参与进来,他们之间的性能差距越来越大。这可能是由于这样一个事实:当只有少数 Client 时,目录局部性提供的好处(如在同一 MDS 上重用缓存的 dentry 和 inode)使 Ceph 具有更好的性能。然而,随着越来越多的 Client 参与进来,某些 MDS 的压力越来越大,这就要求 Ceph 将同一目录下的文件元数据动态地放入不同的 MDS 中,并将相应的请求重定向到代理 MDSs[28],这会增加额外的开销,并缩小 Ceph 与 CFS 之间的差距。

在 TreeRemoval 试验中,CFS 在这两种情况下的性能都优于 Ceph。与 DirStat 测试类似,CFS 处理 readdir 请求的方式可能是导致这些结果的原因之一。另外,由于同一目录下的文件元数据通常存储在一个 MDS 上,当客户端数量较少时,删除文件元数据的请求可能需要在 Ceph 中排队;随着客户端的增多,Ceph 中目录局部性所带来的潜在好处可能会降低,因为文件元数据可能已经分布在不同的 MDS 中,这是因为在前面的测试中触发的重新平衡。

4.3 大文件

对于大型文件操作,我们首先查看单个 Client 环境中进程数不同的结果,其中每个进程操作一个单独的 40GB 文件。我们使用 fio (带直接IO模式)来生成各种类型的工作负载。在 CFS 和 Ceph 的两种设置中, Client 和服务器部署在不同的机器上。此外,为了获得最佳性能,Ceph 中需要调整两个参数,即 osd_op_num_shards 和 osd_op_num_threads_per_shard,它们控制队列的数量和处理队列的线程数。我们将它们分别设置为 6 和 4。进一步增加这些值中的任何一个都会由于高CPU压力而导致写入性能下降。

图 8 所示,不同进程下的性能非常相似,除了随机读写测试,当进程数大于 16 时,CFS 具有更高的 IOPS。

这可能是由于以下原因。首先,Ceph 的每个 MDS 只在其内存中缓存文件元数据的一部分。在随机读取的情况下,缓存未命中率会随着进程数的增加而急剧增加,从而导致频繁的磁盘 IO。相比之下,CFS 的每个 meta 节点都在内存中缓存所有的文件元数据,以避免昂贵的磁盘 IO 次数。其次,CFS 中的覆盖已经就绪,不需要更新文件元数据。相反,Ceph 中的覆盖通常需要遍历多个队列,只有在数据和元数据被持久化和同步之后,提交消息才能返回给客户端。

接下来,我们研究 CFS 和 Ceph 如何在多 Client 环境中使用相同的测试集执行。在这个实验中,每个客户端在随机读/写测试中有 64 个进程,在顺序读/写测试中有 16 个进程,其中每个进程操作一个单独的 40GB 文件。Ceph 中的每个客户端操作不同的文件目录,每个目录都绑定到特定的 MDS 上,以最大限度地提高并发性和提高性能稳定性。如 图9 所示,CFS 在随机读/写测试中比 Ceph 具有显著的性能优势,尽管它们在顺序读/写测试中的性能非常相似。这些结果与之前的单客户实验结果一致,并可以用类似的方法解释。

总而言之,在高并发环境中,CFS 在我们对大文件的随机读/写测试中的性能优于 Ceph。

4.4 小文件

在本节中,我们将研究在 CFS 和 Ceph 中操作大小从 1kb 到 128kb 的小文件的性能。与元数据测试类似,结果来自 mdtest。这个实验模拟了在操作产品图像时的用例,这些图像通常在创建后永远不会被修改。在我们的 CFS 配置中,128kb 是设置的阈值,用于确定是否应该在单个范围内聚合文件,即,我们是否应该将其视为 "小文件"。在 Ceph 中,每个客户端操作不同的文件目录,每个目录都绑定到特定的 MDS 上,以最大限度地提高并发性和提高性能稳定性。从 图10 中可以看出,CFS 在读写测试方面都优于 Ceph。这是因为:

(1) CFS 将所有的文件元数据存储在内存中,以避免文件读取过程中的磁盘IO。

(2) 在小文件写入的情况下,CFS 客户端不需要向资源管理器请求新的扩展数据块,而是直接将写请求发送到数据节点,这进一步降低了网络开销。

5、相关工作

GFS [10] 和它的开源实现 HDFS[22] 是为按顺序访问存储大文件而设计的。它们都采用主从架构[24],其中单个主机存储所有文件元数据。与 GFS 和 HDFS 不同,CFS 使用一个独立的元数据子系统来为元数据存储提供一个可扩展的解决方案,这样资源管理器就不太可能成为瓶颈。

Haystack[2] 采用了日志结构文件系统[19]来处理在大型社交网络中分享照片所带来的请求。关键是在访问元数据时避免磁盘操作。CFS 采用了类似的思想,将文件元数据放入主内存中。但是,与 Haystack 不同的是,文件内容的实际物理偏移量而不是逻辑索引存储在内存中,删除文件是通过底层文件系统提供的 punch hole 接口来实现的,而不是依赖垃圾收集器定期进行合并和压缩,以提高磁盘的效率利用。此外,Haystack 在删除文件时不能保证副本之间的强一致性,它需要定期执行合并和压缩以提高磁盘利用率,这可能是一个性能杀手。一些著作[9,29]提出了通过将相关文件和元数据智能地组合在一起来更有效地管理小文件和元数据。CFS 采用不同的设计原则来分离文件元数据和内容的存储。这样,我们就可以更灵活、更具成本效益地部署元数据和数据节点。

Windows Azure Storage(WAS)[6]是一个云存储系统,它为客户端提供了强一致性和多租户。与 CFS 不同,它构建了一个额外的分区层来处理随机写入,然后将数据流传输到较低级别。WS-EFS[20]是一种云存储服务,提供可伸缩和弹性的文件存储。我们无法评估 Azure 和 WS-EFS 与 CFS 的性能,因为它们不是开源的。

PolarFS[7] 是为阿里巴巴的数据库服务设计的分布式文件系统,它利用轻量级网络堆栈和 I/O 堆栈,利用 RDMA、NVMe 和 SPDK 等新兴技术。OctopusFS[14] 是一个基于 HDFS 的分布式文件系统,具有自动化的数据驱动策略,用于跨集群的存储层(如内存、ssd、hdd 和远程存储)管理数据的放置和检索。这些与我们的工作是有交集的,因为 CFS 也可以采用类似的技术来充分利用新兴的硬件和存储层次结构。

有一些分布式文件系统已经与 Kubernetes 集成[12,23,26]。Ceph[26]是一个 PB 规模的对象/文件存储,通过采用伪随机数据分布函数(CRUSH)将数据和元数据管理之间的分离最大化,该函数是为不可靠对象存储设备(Object storage devices, OSDs)的异构和动态集群而设计的。本文对 Ceph 进行了全面的比较。GlusterFS[12]是一个可扩展的分布式文件系统,它将来自多个服务器的磁盘存储资源聚合到一个全局命名空间中。MooseFS[23]是一个容错、高可用、兼容 POSIX 且可扩展的分布式文件系统。但是,与 HDFS 类似,它使用一个主节点来管理文件元数据。

MapR-fs 是一个与 POSIX 兼容的分布式文件系统,它提供可靠、高性能、可扩展和完整的读/写数据存储。与 Ceph 类似,它以分布式方式存储元数据,同时存储数据本身。

6、结论和期望工作

本文介绍了为京东电子商务服务的分布式文件系统 CFS。CFS 有一些独特的特性,它有别于其他开源解决方案。例如,它有一个通用存储引擎,该引擎具有场景感知复制功能,以优化性能适应不同的文件访问模式。此外,它使用一个单独的集群以分布式方式存储文件元数据,并采用简单而高效的元数据放置策略。此外,它还为符合 POSIX 的 API 提供了宽松的 POSIX 语义和元数据原子性,以获得更好的系统性能。

我们已经通过遵循 POSIX 标准实现了大部分操作,并且正在积极处理其余的操作,如 xattr、fcntl、ioctl、mknod 和 readdirplus。未来,我们计划利用 Linux 页面缓存加速文件操作,改善文件锁定机制和缓存一致性,并支持 RDMA 等新兴硬件标准。我们还计划在内核空间开发自己的兼容 POSIX 的文件系统接口,以完全消除 FUSE 带来的开销。

原论文中的声明:

Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. Copyrights for components of this work owned by others than ACM must be honored. Abstracting with credit is permitted. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. Request permissions from permissions@acm.org.

ChubaoFS:一个面向大规模容器平台的分布式文件系统相关推荐

  1. 一篇文章搞定大规模容器平台生产落地十大实践

    Kubernetes已经成为企业容器平台的标配,在大部分企业,小规模容器平台已经试用了一段时间,然而当容器平台规模大了之后,尤其是用于生产,可能会遇到各种各样的问题,这里我们总结十大问题. 第零节,K ...

  2. 论文笔记(五)面向大规模智能计量的分布式差分隐私

    Distributional Differential Privacy for Large-Scale Smart Metering 面向大规模智能计量的分布式差分隐私 摘要 在智能电网中,通过应用基 ...

  3. PolarFS :一个用于共享存储云数据库的超低延迟和容错分布式文件系统

    目录 1. 简介 2. 背景 3. 架构 4. I/O 执行模型 5. 一致性模型 6. FS中层的实现 7. 设计选择和经验教训 8. 价值评估 9. 相关工作 10. 结论 PolarFS : A ...

  4. 《Hadoop权威指南》第三章 Hadoop分布式文件系统

    <Hadoop权威指南>第三章 Hadoop分布式文件系统 目录 前言 HDFS的设计 HDFS的概念 命令行接口 Hadoop文件系统 Java接口 数据流 通过distcp并行复制 注 ...

  5. 浅谈Google分布式文件系统(GFS)

    前提 GFS是一个面向大规模的数据密集型应用的.可扩展的分布式文件系统.即便是将GFS运行在十分廉价的设备上,它也能提供十分强大的容错能力,为大量的客户端提供高性能服务的服务.其设计是为了解决谷歌内部 ...

  6. 分布式文件系统(GFS和HDFS)概述

    目录 背景意义 分布式存储相关概念 分布式存储系统的分类 复制副本 CAP理论 一致性 GFS架构 租约(lease)和变更顺序 容错机制 前言 因为我研一下学期有一门分布式的课,老师要求我们选择一个 ...

  7. 分布式基础学习(1)--分布式文件系统

    分布式基础学习 所谓分布式,在这里,很狭义的指代以Google的三驾马车,GFS.Map/Reduce.BigTable为框架核心的分布式存储和计算系统.通常如我一样初学的人,会以Google这几份经 ...

  8. 分布式基础学习【一】 —— 分布式文件系统

    分布式基础学习 所谓分布式,在这里,很狭义的指代以Google的三驾马车,GFS.Map/Reduce.BigTable为框架核心的分布式存储和计算系统.通常如我一样初学的人,会以Google这几份经 ...

  9. 新人学习笔记-分布式基础学习-分布式文件系统

    所谓分布式,在这里,很狭义的指代以Google的三驾马车,GFS.Map/Reduce.BigTable为框架核心的分布式存储和计算系统.通常如我一样初学的人,会以Google这几份经典的论文作为开端 ...

最新文章

  1. 容器装不下内容时,显示滚动条
  2. 初始Spring boot和一个入门SpringBoot工程
  3. leetcode 606. Construct String from Binary Tree | 606. 根据二叉树创建字符串
  4. oracle xe gentoo,Oracle在gentoo下安装
  5. 2022年中国在线视频行业研究报告
  6. python模拟百度搜索点击链接_python采集百度搜索结果带有特定URL的链接代码实例...
  7. 生产环境实施 VMware 虚拟化基础架构,千万不要犯 4 个错误
  8. Web漏洞扫描工具:AWVS下载
  9. 【C语言】【笔记】ASCII码值表;常用转义字符表
  10. 联想 计算机无线网络设置方法,联想笔记本无线网络开关,详细教您联想笔记本无线网络开关...
  11. windows创建bat文件进行截图
  12. 麒麟案例 | 低竞争,高利润!凭借这个品,外贸老兵在跨境蓝海市场一路畅游
  13. 关于王小云破解MD5
  14. [高项]工作绩效数据 vs工作绩效信息 vs工作绩效报告
  15. Python读取MEIC文件(.nc格式及.asc格式)
  16. CSGO新手教学-CSGO游戏模式介绍 CSGO 攻略
  17. TSC打印机打印条形码和二维码,java实现方式
  18. IRIS平台部署手册及基本操作
  19. VIPS 98经济型漏电继电器
  20. ENVI实验教程(5) 实验五、遥感图像增强

热门文章

  1. Delphi常用关键字用法详解
  2. c++ 继承机制易犯的错误
  3. IOCP中在WSASend以及WSARecv的时候出现WSA_IO_PENDING情况的说明
  4. 高薪Java工程师必看的书籍
  5. 一个95分位延迟要求5ms的场景,如何做性能优化
  6. LiveVideoStack主编观察04 /
  7. 【LiveVideoStack线上分享】— 海外复杂网络环境下的视频播放体验优化
  8. 云游戏之真心话:一切才刚刚开始 | 对话南京大学副教授马展
  9. Xilinx软硬IP双管齐下解决音视频处理痛点
  10. 最短路径问题 --- Dijkstra算法详解