目录

1. 简介

2. 背景

3. 架构

4. I/O 执行模型

5. 一致性模型

6. FS中层的实现

7. 设计选择和经验教训

8. 价值评估

9. 相关工作

10. 结论


PolarFS : An Ultra-low Latency and Failure Resilient Distributed File System for Shared Storage Cloud Database

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

摘要

PolarFS 是一个具有超低延迟和高可用性的分布式文件系统,专为 POLARDB 数据库服务而设计,目前已在阿里云上提供。 PolarFS 在用户空间中利用了轻量级网络堆栈和 I/O 堆栈,充分利用了 RDMA、NVMe 和 SPDK 等新兴技术。通过这种方式,PolarFS 的端到端延迟大大降低,实验表明 PolarFS 的写入延迟与 SSD 上本地文件系统的写入延迟非常接近。为了在保证 PolarFS 的副本一致性的同时最大限度地提高 I/O 吞吐量,我们开发了 ParallelRaft 协议,它利用数据库的无序 I/O 完成容能力,打破了 Raft 的严格串行化。ParallelRaft 继承了 Raft 的可理解性和易于实现,同时为 PolarFS 提供了更好的 I/O 可伸缩性。我们还描述了 PolarFS 的共享存储体系结构,它为 POLARDB 提供了强大的支持。

1. 简介

近年来,存储与计算的分离已成为云计算产业的发展趋势。这种设计使得体系结构更加灵活,有助于开发共享存储能力:(1) 计算和存储节点可以使用不同类型的服务器硬件,并且可以单独定制。例如,计算节点不再需要考虑内存大小与磁盘容量的比率,这是高度依赖于应用场景的,并且很难预测。(2) 群集中存储节点上的磁盘可以形成单个存储池,这样可以降低碎片化、节点间磁盘使用不平衡和空间浪费的风险。存储集群的容量和吞吐量可以很容易地透明地扩展。(3) 由于数据都存储在存储集群上,所以计算节点上没有本地持久状态,这使得执行数据库迁移更容易、更快。分布式存储系统的高可靠性和其他特性也可以提高数据的可靠性。

云数据库服务也受益于此架构。首先,数据库可以基于虚拟化技术(如 Xen[4]、KVM[19] 或 Docker[26])构建一个更安全、更易于扩展的环境。其次,在后端存储集群的支持下,可以增强数据库的一些关键特性,如多个只读实例和检查点,后端存储集群提供快速 I/O 、数据共享和快照。

然而,数据存储技术仍在快速变化,因此当前的云平台很难充分利用 RDMA 和 NVMe SSD 等新兴硬件标准。例如,一些广泛使用的开源分布式文件系统,如 HDFS[5] 和 Ceph[35],被发现比本地磁盘有更高的延迟。当使用最新的 PCIe SSD 时,性能差距甚至可能达到数量级。直接在这些存储系统上运行的关系数据库(如MySQL)的性能明显低于具有相同CPU和内存配置的本地 PCIe-SSD。

为了解决这个问题,AWS、Google 云平台和阿里云等云计算厂商提供了实例存储( instance store )。实例存储使用本地 SSD 和高 I/O 虚拟机实例来满足客户对高性能数据库的需求。不幸的是,在实例存储上运行云数据库有几个缺点。首先,实例存储容量有限,不适合大型数据库服务。其次,它无法在磁盘驱动器故障中生存。为了保证数据的可靠性,数据库必须自己管理数据复制。第三,实例存储使用通用文件系统,如 ext4 或 XFS。当使用低 I/O 延迟硬件(如RDMA或PCIe SSD)时,内核空间和用户空间之间的消息传递成本会影响 I/O 吞吐量。更糟糕的是,实例存储不能支持共享一切数据库集群架构,这是高级云数据库服务的一个关键特性。

在本文中,我们描述了我们的分布式文件系统 PolarFS 的设计和实现,它通过以下机制提供超低延迟、高吞吐量和高可用性。首先,PolarFS 会完全利用 RDMA  NVMe  SSD  等新兴硬件的优势,在用户空间实现了一个轻量级的网络堆栈和 I/O 堆栈,以避免陷入内核和处理内核锁。第二,PolarFS 提供了一个类似 POSIX 的文件系统 API,目的是编译到数据库进程中,替换操作系统提供的文件系统接口,使整个 I/O 路径都能保存在用户空间中。第三,PolarFS 数据层面的 I/O 模型也被设计用来消除锁和避免关键数据路径上的上下文切换:所有不必要的内存副本也被消除,同时 DMA 被大量用于在主内存和 RDMA NIC/NVMe 磁盘之间传输数据。有了这些特性,PolarFS 的端到端延迟大大降低,与 SSD 上的本地文件系统非常接近。

部署在云生产环境中的分布式文件系统通常有数千台计算机。在这样的规模内,由硬件或软件缺陷引起的故障是常见的。因此,需要一个一致性协议来确保所有已提交的修改不会丢失,并且副本始终可以达到位相同一致的级别

Paxos 家族[23,22,7,18,38]的协议是公认的能用于解决问题的共识机制。Raft[28],是 Paxos 的一个变种,更容易理解和实现。许多分布式系统都是基于 Raft 开发的。然而,一旦 Raft 应用于 PolarFS ,我们发现 Raft 严重阻碍了 PolarFS 在使用超低延迟硬件时的 I/O 可扩展性(例如延迟在几十微秒量级的 NVMe SSD  RDMA)。因此,我们开发了基于 Raft 的增强一致性协议 ParallelRaft,它允许无序的日志确认、提交和应用,同时允许 PolarFS 遵循传统的 I/O 语义。通过该协议, PolarFS 的并行 I/O 并发性得到了显著提高。

最后,在 PolarFS 的基础上,我们实现了 POLARDB ,这是一个由 AliSQL(MySQL/InnoDB的分支)[2]修改而来的关系型数据库系统,最近在阿里云计算平台上作为一个数据库服务提供。POLARDB 遵循共享存储体系结构,支持多个只读实例。如 图1 所示,POLARDB 的数据库节点分为两种类型:主节点 只读(RO)节点。主节点可以同时处理读和写查询,而 RO 节点只提供读查询。主节点 RO 节点共享 PolarFS 中同一数据库目录下的 redo 日志文件和数据文件。

PolarFS 支持 POLARDB,具有以下特点:(1) PolarFS 可以将文件元数据的修改(如文件截断或扩展、文件创建或删除)从主节点同步到 RO 节点,使 RO 节点可以看到所有的更改。(2) PolarFS 确保对文件元数据的并发修改被序列化,以便文件系统本身在所有数据库节点上保持一致。(3) 在一个网络分区的情况下,可能有两个或多个节点作为主节点(因为分区所以可能有多个主节点)的情况下同时在 PolarFS 中写入共享文件,PolarFS 可以确保只有真正的主节点被成功服务,防止数据损坏

本文的贡献包括:

• 我们描述了如何利用新兴的硬件和软件优化来构建 PolarFS,这是一种具有超低延迟的最先进分布式文件系统。(第3、4和7节)

• 我们提出 ParallelRaft,一个新的协议,以达成共识。ParallelRaft 是为大规模、容错和分布式文件系统而设计的。在 Raft 的基础上对其进行了修改,以适应存储语义。与 Raft 相比,ParallelRaft 为 PolarFS 中的高并发 I/O 提供了更好的支持。(第5节)

• 我们介绍了 PolarFS 的关键特性,这些特性为 POLARDB 的共享存储架构提供了强有力的支持。(第6节)

论文的其他部分结构如下。第 2 节介绍了 PolarFS 使用的新兴硬件的背景信息。第 8 节介绍并讨论了我们的实验评估。第 9 节回顾相关工作,第 10 节总结全文。

2. 背景

本节简要介绍 NVMe SSD 、RDMA 及其相应的编程模型。

NVMe SSD。SSD 正在从 SAS、SATA 等传统协议发展到具有高带宽和低速率 I/O 互连的 NVMe。NVMe SSD 可以以低于 100µs的延迟提供每秒 500K 次 I/O 操作(IOPS),最新的 3D XPoint SSD 甚至将 I/O 延迟降低到 10µs 左右,同时提供比 NAND SSD 更好的 QoS。随着 SSD 越来越快,遗留内核 I/O 堆栈的开销成为瓶颈[37,36,31]。正如之前的研究[6]所揭示的,仅完成一个 4KB 的 I/O 请求,就需要执行大约 20000 条指令。最近,Intel发布了存储性能开发工具包(SPDK,Storage Performance Development Kit)[12],这是一套工具和库,用于构建基于 NVMe 设备的高性能、可扩展的用户模式存储应用程序。它通过将所有必需的驱动程序移入用户空间并以轮询模式操作而不是依赖中断来获得高性能,这避免了内核上下文切换并消除了中断处理开销。

RDMA 公司。RDMA 技术为数据中心内部服务器之间提供了一种低延迟的网络通信机制。例如,在连接到同一交换机的两个节点之间传输 4KB 的数据包大约需要 7µs,这比传统的 TCP/IP 网络栈要快得多。大量文献[9,14,15,17,24,25,30]表明,RDMA 可以提高系统性能。应用程序通过 Verbs API 访问队列对(QP,Queue Pair)与 RDMA NIC 交互。QP 由 发送队列接收队列 组成,并且每个 QP 与另一个完成队列(CQ,Completion Queue)相关联。CQ 通常用作完成 事件/信号 的轮询目标。

Send/Recv 操作通常称为双边操作,因为每个 Send 操作都需要远程进程调用匹配 Recv操作,而读/写操作称为单边操作,因为远程内存由 NIC 操作,而不涉及任何远程 CPU。

PolarFS 使用 Send/Recv 和 Read/Write 操作的混合体。小的有效载荷通过 Send/Recv 操作直接传递。对于大数据块或一批数据,节点使用 Send/Recv 操作协商远程节点上的目标内存地址,然后通过读/写操作完成实际的数据传输。PolarFS 通过在用户空间中轮询 CQ 而不是依赖中断来消除上下文切换。

3. 架构

PolarFS 由两个主要层组成。下层是存储管理,上层是文件系统元数据管理,提供文件系统API。存储层负责存储节点的所有磁盘资源,并为每个数据库实例提供一个数据库 volume。文件系统层支持 volume 中的文件管理,负责文件系统元数据并发访问的互斥和同步。对于数据库实例,PolarFS 将文件系统元数据存储在其 volume 中。

这里我们展示 PolarFS 集群中的主要组件,如 图2 所示。libpfs 是一个用户空间文件系统实现库,具有一组类似 POSIX 的文件系统 API,链接到 POLARDB 进程中;PolarSwitch 驻留在计算节点上,用于将 I/O 请求从应用程序重定向到 ChunkServer;ChunkServer 部署在存储节点上以服务于 I/O 请求;PolarCtrl 是控制面板,它包括一组在微服务中实现的主节点,以及部署在所有计算和存储节点上的代理。PolarCtrl 使用 MySQL 实例作为元数据存储库。

3.1 文件系统层

文件系统层提供了一个共享和并行的文件系统,设计为能够由多个数据库节点同时访问。例如,在 POLARDB 的场景中,当主数据库节点执行 create table DDL 语句时,将在 PolarFS 中创建一个新文件,使选择在 RO 节点上执行的语句以访问该文件。因此,需要跨节点同步对文件系统元数据的修改以保持一致性,同时序列化并发修改以避免元数据被破坏。

3.1.1 libpfs

libpfs 是一个完全在用户空间中运行的轻量级文件系统实现。如 图3 所示,libpfs 提供了一个类似于 POSIX 的文件系统的 API。移植一个数据库在 PolarFS 上运行非常容易。

当数据库节点启动时,pfs_mount 会连接到其 volume 并初始化文件系统状态。volname 是分配给 POLARDB 实例的 volume 的全局标识符,hostid 是数据库节点的索引,在磁盘 Paxos 投票算法中用作标识符(见6.2节)。在装载过程中,libpfs 从 volume 中加载文件系统元数据,并在内存中构造数据结构,如目录树、文件映射表和 block 映射表(见第6.1节)。pfs_umount 在数据库销毁期间分离 volume 并释放资源。在 volume 空间增长后,应该调用 pfs_mount_growfs 来识别新分配的 block 并将它们标记为可用。Rest 函数是相当于 POSIX API 中对应的文件和目录操作。有关文件系统元数据管理的详细信息,请参见第 6 节。

3.2 储存层

存储层为文件系统层提供管理和访问 volume 的接口。为每个数据库实例分配一个 volume,并由一组 chunk 组成。一个 volume 的容量从 10GB 到 100TB 不等,可以满足绝大多数数据库的需求,并且可以根据需要在 volume 中添加 chunk 来扩展容量。一个 volume 可以像传统的存储设备一样以 512B 的方式随机访问(读、写)。对单个 I/O 请求中携带的相同 chunk 的修改是原子性的。

Chunk。一个 volume 被分成 chunks,这些 chunk 分布在块服务器之间。chunk 是数据分布的最小单位。一个 chunk 不会跨越 ChunkServer 上的多个磁盘,默认情况下,它的副本复制到三个不同的 ChunkServer (总是位于不同的机架中)。当存在热点时,可以在 ChunkServer 之间迁移 chunk 。

在 PolarFS 中,chunk 的大小被设置为10GB,这明显大于其他系统中的单位大小,例如 GFS 使用的 64MB 块大小[11]。这种选择在数量级上减少了元数据数据库中维护的元数据量,并且简化了元数据管理。例如,100TB 的 volume 只包含 10000 个 chunk。在元数据数据库中存储 10000 条记录的成本相对较小。此外,所有这些元数据都可以缓存在 PolarSwitch 的主内存中,从而避免了关键 I/O 路径上额外的元数据访问开销。

这种设计的缺点是一个 chunk 上的热点不能进一步分离。但是由于 chunk 与服务器的比率很高(现在大约是1000:1),通常有大量的数据库实例(数千个或更多),以及服务器间的块迁移能力,PolarFS 可以在整个系统级别实现负载平衡。

Block。chunk 在 ChunkServer 中进一步划分为 block,每个 block 被设置为 64KB。block 按需分配并映射到 chunk,以实现精简资源调配。一个 10GB 的数据 chunk 包含 163840 个数据 block。Chunk 的 LBA (逻辑块地址,线性地址范围从 0 到 10GB )到 blocks 的映射表存储在 ChunkServer 中,同时存储每个磁盘上空闲 blocks 的位图。单个 Chunk 的映射表占用 640KB 内存,非常小,可以缓存在 ChunkServer 的内存中。

3.2.1 PolarSwitch

PolarSwitch 是一个部署在数据库服务器上的守护进程,以及一个或多个数据库实例。在每个数据库进程中,libpfs 将 I/O 请求转发给 PolarSwitch 守护进程。每个请求都包含诸如 volume 标识符、偏移量和长度等寻址信息,从中可以识别出相关的 chunk。一个 I/O 请求可能跨越多个 chunk,在这种情况下,请求被进一步划分为多个子请求。最后,一个基本请求被发送到 ChunkServer ,该 ChunkServer 位于 chunk 的头所在的位置。

PolarSwitch 通过查找与 PolarCtrl 同步的本地元数据缓存来找出属于一个 chunk 的所有副本的位置。一个群体的副本形成了一个共识体,一个是 Leader ,另一些是  Followers。只有 Leader 能响应 I/O 请求。共识小组的领导层变动也同步进行,并缓存在 PolarSwitch 的本地缓存中。如果发生响应超时,PolarSwitch 将继续以指数后退方式重试,同时检测是否发生 Leader 选举,切换到新的 Leader,如果发生,立即重新传输。

3.2.2 ChunkServer

ChunkServers 部署在存储服务器上。有多个 ChunkServer 进程运行在一个存储服务器上,每个 ChunkServer 拥有一个独立的 NVMe SSD 磁盘,并绑定到一个专用的 CPU 核心,因此两个 ChunkServer 之间没有资源争用。ChunkServer 负责存储 chunk 并提供对 chunk 的随机访问。每个 chunk 包含一个预写日志(Write-Ahead Log,WAL),对 chunk 的修改在更新 chunk blocks 之前附加到日志中,以确保原子性和持久性。ChunkServer 使用一块固定大小的 3D XPoint SSD 缓冲区作为 WAL 的写缓存,日志最好放在 3D XPoint 缓冲区中。如果缓冲区已满,ChunkServer 将尝试回收已过期的日志。如果 3D XPoint 缓冲区中仍然没有足够的空间,则将日志写入 NVMe SSD 。chunk blocks 最终会写入 NVMe SSD。

ChunkServer 使用 ParallelRaft 协议相互复制 I/O 请求,并形成一个共识组。 ChunkServer 由于各种原因离开了它的共识组,可能会有不同的处理方式。有时是偶然的、暂时的故障造成的,比如网络暂时不可用,或者服务器升级重启。在这种情况下,最好等待断开连接的 ChunkServer 恢复联机,再次加入组并赶上其他组。在其他情况下,故障是永久性的或往往会持续很长时间,例如服务器损坏或离线。然后,丢失的 ChunkServer 上的所有块都应该迁移到其他块,以便重新获得足够数量的副本。

断开连接的 ChunkServer 总是努力自动重新加入共识组,以缩短不可用时间。然而,PolarCtrl 可以做出补充决定。PolarCtrl 定期收集以前断开连接的 ChunkServer 的列表,并从 ChunkServers 中挑选出似乎存在永久性故障的 ChunkServer。有时很难做出决定。例如,一个具有慢速磁盘的 ChunkServer 可能比其他服务器有更长的延迟,但它总是能够响应存活探测。基于机器学习性能指标的机器学习算法是有用的。

3.2.3 PolarCtrl

PolarCtrl 是 PolarFS 集群的控制面板。它部署在一组专用计算机(至少三台)上,以提供高可用性服务。PolarCtrl 提供集群控制服务,如节点管理、volume 管理、资源分配、元数据同步、监控等。PolarCtrl 负责:(1) 跟踪所有在群集中 ChunkServer 的成员和活跃度,如果 ChunkServer 过载或不可用的时间超过阈值,则开始将块副本从一个服务器迁移到另一个服务器。(2) 维护元数据数据库中所有 volume 和 chunk 位置的状态。(3) 创建 volume 并将 chunk 分配给 ChunkServers。(4) 同时使用 push 和 pull 方法将元数据同步到 PolarSwitch。(5) 监视每个 volume 和 chunk 的延迟、IOPS 指标,沿 I/O 路径收集跟踪数据。(6) 定期安排内部副本和副本间的 CRC 检查。

PolarCtrl 通过控制面板命令定期与 PolarSwitch 同步集群元数据(例如卷的块位置)。PolarSwitch 将元数据保存在其本地缓存中。当收到来自 libpfs 的 I/O 请求时,PolarSwitch 根据本地缓存将请求路由到相应的 ChunkServer。偶尔,如果本地缓存落后于中心元数据存储库,PolarSwitch 会从 PolarCtrl 获取元数据。

作为一个控制面板,PolarCtrl 不在关键 I/O 路径上,它的服务连续性可以通过传统的高可用性技术来实现。即使在 PolarCtrl 崩溃和恢复之间的短时间内,由于 PolarSwith 上缓存的元数据和 ChunkServer 的自我管理,PolarFS 中的 I/O 流也不太可能受到影响。

4. I/O 执行模型

当 POLARDB 访问其数据时,它将通过 PFS 接口(通常通过 pfs_pread 或 pfs_pwrite)将文件 I/O 请求委托给 libpfs。对于写请求,几乎不需要修改文件系统元数据,因为设备块(device blocks)是通过 pfs_fallocate() 预先分配给文件的,从而避免了读写节点之间昂贵的元数据同步。这是数据库系统的常规优化。

在大多数常见的情况下,libpfs 只是根据装载时已经构建的索引表将文件偏移量映射到 block 偏移量中,并将文件 I/O 请求切割成一个或多个较小的固定大小 block I/O 请求。在转换之后,block I/O 请求通过它们之间的共享内存由 libpfs 发送到 PolarSwitch。

共享内存被构造成多个环形缓冲区。在共享内存的一端,libpfs 将 block I/O 请求排队到以循环方式入队到环形缓冲区中,然后等待其完成。在另一端,PolarSwitch 不断地轮询所有的环形缓冲区,其中一个线程专用于一个环形缓冲区。一旦找到新的请求,PolarSwitch 会将来自环形缓冲区的请求取消排队,并使用从 PolarCtrl 传播的路由信息将其转发到 ChunkServer。

ChunkServer 使用预写日志(Write-Ahead Logging,WAL)技术来确保原子性和持久性,在提交和应用 I/O 请求之前,将它们写入日志。日志被复制到一个副本集合中,并使用一个名为 ParallelRaft 的共识协议(将在下一节中详细介绍)来保证副本之间的数据一致性。在将 I/O 请求持久地记录到大多数副本的日志中之前,它不会被识别为已提交。只有在这之后,这个请求才能被响应到客户端并应用于数据块。

图4 显示了如何在 PolarFS 中执行写 I/O 请求。(1) POLARDB 通过 PolarSwitch 和 libpfs 之间的环形缓冲区向 PolarSwitch 发送写 I/O 请求。(2) PolarSwitc 根据本地缓存的集群元数据将请求传输到相应 chunk 的 Leader 节点。(3) 当新的写入请求到达时, Leader 节点中的 RDMA NIC 将把写请求放入预先注册的缓冲区,并将请求条目添加到请求队列中。 I/O 循环线程不断轮询请求队列。一旦它看到一个新的请求到达,它立即开始处理该请求。(4) 请求通过 SPDK 写入磁盘上的日志块,并通过 RDMA 传播到 follower 节点。这两个操作都是异步调用,实际的数据传输将并行触发。(5) 当复制请求到达 follower 节点时,follower 节点中的RDMA NIC也会将复制请求放入预先注册的缓冲区,并将其添加到复制队列中。(6) 然后触发 follower 上的 I/O 循环线程,并通过 SPDK 异步将请求写入磁盘。(7) 当 write 回调成功返回时,一个确认响应将通过 RDMA 发送回 Leader。(8) 当大多数 follower 成功接收到响应时,Leader 通过 SPDK 向数据块发出写请求。(9) 之后,Leader 通过 RDMA 回复 PolarSwitch。(10) PolarSwitch 然后标记请求完成并通知客户端。

读 I/O 请求(更简单地说)由 Leader 单独处理。 ChunkServer 中有一个名为 IoScheduler 的子模块,它负责仲裁并发 I/O 请求发出的磁盘 I/O 操作的顺序,以便在 ChunkServer 上执行。IoScheduler 保证读取操作始终可以检索最新提交的数据。

ChunkServer 使用轮询模式事件驱动的有限状态机作为并发模型。I/O 线程保持 RDMA 和 NVMe 队列中的事件轮询,在同一线程中处理传入的请求。当发出一个或多个异步 I/O 操作并且需要处理其他请求时,I/O 线程将暂停处理当前请求并将上下文保存到状态机中,然后切换到处理下一个传入事件。每个 I/O 线程使用一个专用的内核,并使用分离的 RDMA 和 NVMe 队列对。因此,即使在一个 ChunkServer 上有多个 I/O 线程,由于 I/O 线程之间没有共享的数据结构,所以实现 I/O 线程时不会产生锁开销。

5. 一致性模型

5.1 Raft 修订

用于生产的分布式存储系统需要一个共识协议,以保证在任何情况下都不会丢失所有已提交的修改。在设计过程的开始。考虑到实现的复杂性,我们选择了 Raft 。然而,一些隐患很快就出现了。

Raft 的设计是高度序列化的,简单易懂。它的日志不允许在 Leader 和 Follower 上都有漏洞,这意味着日志条目由 Follower 确认、Leader 提交并按顺序应用于所有副本。因此,当 write 请求并发执行时,它们将按顺序提交。队列末尾的那些请求在之前的所有请求被持久地存储到磁盘并得到响应之前是无法提交和响应的,这会增加平均延迟并降低吞吐量。我们观察到随着 I/O 深度从 8 增加到 32,吞吐量下降了一半。

Raft 不太适合使用多个连接在主从之间传输日志的环境。当一个连接阻塞或变慢时,日志条目将无序地到达 Follower。换句话说,队列前面的一些日志条目实际上比后面的日志条目到达的晚。但是 Raft Follower 必须按顺序接受日志条目,这意味着在前面丢失的日志条目到达之前,它不能发送一个确认来通知 Leader 后续的日志条目已经被记录到磁盘上。此外,当大多数 Follower 在一些丢失的日志条目上被阻塞时,Leader 会陷入困境。然而,实际上,在高度并发的环境中使用多个连接是很常见的。我们需要修改 Raft 协议来解决这种情况。

在事务处理系统(如数据库)中,并发控制算法允许事务以交错和无序的方式执行,同时生成可序列化的结果。这些系统自然可以容忍由传统存储语义引起的无序 I/O 完成,并自行处理以确保数据一致性。实际上,像 MySQL 和 AliSQL 这样的数据库并不关心底层存储的 I/O 序列。数据库的锁系统将保证在任何时候,只有一个线程可以在一个特定的页面上工作。当不同的线程同时工作在不同的页面上时,数据库只需要成功地执行 I/O,它们的完成顺序无关紧要。因此,我们利用这一点来放宽 PolarFS 中 Raft 的一些限制,从而开发出一个更适合高 I/O 并发性的一致性协议。

本文在 Raft 上提出了一种改进的一致性协议 ParallelRaft,下面将介绍 ParallelRaft 如何解决上述问题。 ParallelRaft 的结构与 Raft 相当相似。它通过复制日志实现复制状态机。有 Leader 和 Followers,Leader 将日志条目复制到 Followers。我们采用与 Raft 相同的问题分解方法,将 ParallelRaft 划分为更小的部分:日志复制、领导人选举和 Catch up

5.2 无序日志复制

Raft 的序列化分为两个方面:(1) 当 Leader 向 Follower 发送一个日志条目后, Follower 需要确认它,以通知该日志条目已经被接收和记录,这也暗示着前面所有的日志条目都已被看到并保存。(2) 当 Leader 提交一个日志条目并将此事件广播给所有 Follower 时,它也承认之前的所有日志条目都已提交。ParallelRaft 打破了这些限制,无序地执行所有这些步骤。因此,ParallelRaft  Raft 有一个基本的区别。在 ParallelRaft 中,当一个条目被识别为已提交时,并不意味着所有先前的条目都已成功提交。为了保证该协议的正确性,我们必须保证:(1) 在没有这些串行限制的情况下,所有提交的状态将与经典且合理的数据库使用的存储语义相冲突。(2) 所有提交的修改在任何情况下都不会丢失。

ParallelRaft 的无序日志执行遵循以下规则:如果日志项的写入范围不重叠,则认为这些日志项没有冲突,可以按任何顺序执行。否则,冲突条目将在到达时按严格的顺序执行。这样,较新的数据就不会被旧版本覆盖。ParallelRaft 可以很容易地知道冲突,因为它存储了未应用的任何日志项的 LBA 范围摘要。下面介绍如何优化 ParallelRaft 的 Ack-Commit-Apply 步骤,以及如何维护必要的一致语义。

无序确认(Out-of-Order Acknowledge)。Raft Follower 在接收到从 Leader 复制的日志条目之后不会立即确认,而是直到前面所有的日志条目都被持久地存储起来后才确认。这就增加了额外的等待时间,并且当有大量并发的 I/O 写入正在执行时,平均延迟会显著增加。然而, ParallelRaft 中,一旦成功写入日志条目,Follower 可以立即确认它。这样,避免了额外的等待时间,从而优化了平均延迟。

无序提交(Out-of-Order Commit)。 Raft Leader 以串行顺序提交日志条目,在提交所有前面的日志条目之前,无法提交日志条目。然而,在日志中的大多数副本可以立即提交。这种提交语义对于不像 TP 系统那样承诺强一致性语义的存储系统而言是可以接受的。例如, NVMe 不检查读或写命令的 LBA,以确保并发命令之间的任何类型的排序,也不保证这些命令的完成顺序[13]。

允许日志上有漏洞(Apply with Holes in the Log)。 像 Raft 一样,所有的日志条目在 ParallelRaft 中被记录时都是按照严格的顺序应用的,这样数据文件在所有副本中都是一致的。但是,对于无序的日志复制和提交,ParallelRaft 允许日志中存在漏洞。在前面的一些日志条目仍然丢失的情况下,如何安全地应用日志条目?这给 ParallelRaft 带来了挑战。

ParallelRaft 在每个日志条目中引入了一种新的数据结构 look behind buffer 来解决这个问题。Look behind buffer 包含由前 N 个日志条目修改的 LBA ,因此 look behind buffer 充当在日志中可能的漏洞上构建的桥梁。N 是这座桥的跨度,也是允许的最大日志漏洞。请注意,尽管日志中可能存在多个漏洞,但所有日志条目的 LBA 摘要始终是完整的,除非任何漏洞大小大于 N。通过这种数据结构,Follower 可以判断出日志条目是否冲突,这意味着由该日志条目修改的 LBA 与一些先前缺失的日志条目重叠。可以安全地应用与任何其他条目不冲突的日志条目,否则应将它们添加到挂起的列表中,并在检索到丢失的条目后进行答复。根据我们在使用 RDMA 网络的 PolarFS 中的经验,N 设置为 2 对于它的 I/O 并发性来说已经足够了。

基于上述无序执行方法和规则,可以成功地实现所需的数据库存储语义。此外,通过消除 PolarFS 并行处理中不必要的串行限制,还可以缩短多副本并发写入的延迟。

5.3 Leader 选举

在进行新的 Leader 选举时,与 Raft 相同,ParallelRaft 可以选择最新项和日志项最长的节点。在 Raft 中,新当选的 Leader 包含了所有以前任期的提交条目。然而,由于日志可能有漏洞,ParallelRaft 中选出的 Leader 最初可能无法满足这一要求。因此,在开始处理请求之前,需要一个额外的 Merge 阶段,使 Leader 拥有所有已提交的条目。在合并阶段完成之前,新选择的节点是唯一的 Leader 候选人,在合并阶段完成后,它拥有所有提交的条目,成为真正的 Leader。在合并阶段,Leader 候选人需要合并仲裁其他成员看不见的条目。在那之后,Leader 将开始提交以前任期的参与者到法定人数,这和 Raft 一样。

合并条目时,ParallelRaft 还使用与 Raft 类似的机制。具有相同任期(term)和索引的条目将被提交为相同条目。有几个不正常的情况:(1) 对于一个提交但缺失的条目,Leader 候选人总能从至少一个 Follower 候选中找到相同的提交条目,因为该提交条目已被大多数法定人数接受。(2) 对于一个没有在任何候选对象上提交的条目,如果该条目也没有保存在其中的任何一个上,由于根据 ParallelRaft  Raft 机制,它之前不可能被提交,所以 Leader 可以安全地跳过该日志条目。(3) 如果一些候选者保存了这个未提交的条目(具有相同的索引但是不同的 term),那么 Leader 候选者选择具有最高 term 的条目版本,并将此条目识别为有效条目。我们必须这样做是因为另外两个事实:(3.a) ParallelRaft 的合并阶段必须在新的 Leader 能够为用户请求服务之前完成,这决定了如果一个条目设置了一个较高的 term,那么具有相同条目索引的较低 term 之前一定没有被提交过,并且较低的 term 条目必须从未参与过之前成功完成的合并阶段,否则较高的 term 条目不能具有相同的索引。(3.b) 当系统崩溃时,候选项中保存了一个未提交的条目,则该条目的确认可能已发送给前一个 Leader 并回复给用户,因此我们不能简单地放弃它,否则用户数据可能会丢失。(更确切地说,如果失败节点的总数加上具有此未提交项的节点(具有相同索引的最高 term)超过同一仲裁中的其他节点数,则此项可能已由失效的 Leader 提交。因此,我们应该保证用户数据安全。)以 3 个副本的情况为例,图5 显示了领导人选举的过程。

(1) Follower 候选者将其本地日志条目发送给 Leader 候选者。Leader 候选者接收这些条目并与自己的日志条目合并。(2) Leader 候选者和 Follower 候选者同步状态。(3) Leader 候选人可以提交所有的条目,并通知 Follower 候选人提交。最后,Leader 候选人升级为 Leader ,Follower 候选人也升级为 Follower 。

通过上述机制,所有提交的条目都可以由新的 Leader 恢复,这意味着 ParallelRaft 不会丢失任何提交状态。在 ParallelRaft 中,不定期地建立一个检查点(checkpoint),检查点之前的所有日志项都应用于数据块,并且允许该检查点包含一些在检查点之后提交的日志项。为了简单起见,检查点之前的所有日志条目都可以被视为修改过的,尽管实际上它们会保留一段时间,直到资源不足或达到某个阈值为止。

在我们的实际实现中,ParallelRaft 选择检查点最新的节点作为候选节点,而不是日志最长的节点,以达到追赶的目的。在合并阶段,很容易看出,新领导人将达到相同的状态时,开始服务。因此,这种选择并不损害 ParallelRaft 的正确性。

5.4 追赶

当一个落后的 Follower 想要跟上 Leader 的当前数据状态时,它将使用 fast-catch-up 或 streaming-catch-up 来重新与 Leader 同步。使用哪一个取决于 Follower 的状态有多陈旧。快速追赶(fast catch up)机制是为主从之间的增量同步而设计的,前提是当它们之间的差异相对较小时。但 Follower 远远落后于 Leader。例如,Follower 已经离线好几天了,因此完全的数据重新同步是不可避免的。 PolarFS 为此提出了一种称为 streaming catch up 的方法。

图6 显示了重新同步开始时,Leader 和 Follower 的不同情况。在 案例1 中,Leader 的 checkpoint 比 Follower 的最新日志索引更加的新,它们之间的日志条目将被 Leader 修改。因此,fast catch 无法处理这种情况,应该使用 streaming catch up。案例2案例3 可以通过 fast catch 解决。

在以下情况下,我们总是可以假设 Leader 的检查点比 Follower 的检查点更新,因为如果不满足这个条件, Leader 可以立即创建一个比任何其他检查点都新的检查点。

检查点之后的日志条目可以分为四类:已提交和已应用、已提交但未应用、未提交和漏洞

快速追赶(Fast Catch Up)。 Follower 可能在其检查点后有漏洞,在快速追赶时会被填满。首先,利用 look-behind-buffer 重新填充 Follower 检查点和 Leader 检查点之间的漏洞,通过该缓冲区我们可以发现所有缺失修改的 LBA。这些漏洞是直接从 Leader 的数据块中复制来填充的,这些数据块包含比 Follower 的检查点更新的数据。第二,通过读取 Leader 的日志块来填充 Leader 检查点后的块。在 案例3 中, Follower 可能包含一些旧 term 中未提交的日志条目。像 Raft 一样,这些条目被修改,然后将按照上述步骤进行处理。

流式追赶(Streaming Catch Up)。在流式追赶中,比检查点更旧的日志历史记录被认为对完成数据重新同步是无用的,并且必须使用检查点之后的数据块和日志项的内容来重建状态。当复制块时,整个块被分割成 128KB 的小块并使用大量的小任务,每个任务只同步一个 128KB 的块。我们这样做是为了建立一个更可控的再同步。7.3 节进一步的讨论了设计动机。

5.5 ParallelRaft 的正确性

Raft 为协议的正确性确保了以下属性:选举安全性、仅限 Leader 追加、日志匹配、 Leader 完备性和状态机安全性。由于 ParallelRaft 并没有改变它们的性质,所以很容易证明 ParallelRaft 具有选举安全性、只附加 Leader 和日志匹配的特性

ParallelRaft 的故障承诺引入了与标准 Raft 的关键区别。ParallelRaft 日志可能缺少一些必要的条目。因此,我们需要考虑的关键场景是,新当选的 Leader 应该如何处理这些缺失的条目。 ParallelRaft 为 Leader 选举添加合并阶段。在合并阶段, Leader 将复制丢失的日志条目,重新确认是否应该提交这些条目,然后在仲裁中提交它们(如果需要)。在第5.3节的合并阶段之后,保证了 Leader 的完整性。

此外,如第5.2节所述,对于数据库,PolarFS 中的无序提交是可以接受的。然后我们将证明无序应用不会违反状态机安全属性。尽管 ParallelRaft 允许节点无序的独立应用,由于后面找了缓冲区(5.2节),冲突日志只能应用于一个严格的序列,这意味着状态机(数据+提交日志条目)在同一群体中的所有节点将相互一致。利用这些性质,保证了并行程序的正确性。

5.6 ParallelRaft vs Raft

我们做了一个简单的测试来展示 ParallelRaft 如何提高 I/O 并发性。图7 显示了当 I/O 深度从 1 到 32 以及单个 FIO 作业时 Raft 和 ParallelRaft 的平均延迟和吞吐量。这两种算法都是用 RDMA 实现的。由于它们是独立实现的不同软件包,所以在开始时的细微性能差异可以忽略不计。随着 I/O 深度的增加,两个协议之间的性能差距变得更大。 I/O 深度增加到 32 时,Raft 的延迟大约是并行 Raft 延迟的 2.5 倍,只有 ParallelRaft 达到 IOPS 的一半以下。值得注意的是,当 I/O 深度大于 8 时,Raft 的 IOPS 显著下降,而 ParallelRaft 则保持了一个稳定的高水平。结果证明了我们提出的优化机制在 ParallelRaft 上的有效性。无序的确认和提交提高了 I/O 并行性,使 PolarFS 即使在繁重的工作负载下也能保持优异和稳定的性能。

6. FS中层的实现

在 PolarFS 的文件系统层,元数据管理可以分为两部分。第一种方法是组织元数据来访问和更新数据库节点中的文件和目录。二是协调和同步数据库节点之间的元数据修改。

6.1 元数据组织

文件系统元数据有三种:目录项、索引节点和块标记(directory entry、inode and block tag)。一个目录条目保存一个文件路径的名称组件,并引用一个 inode。目录项的集合被组织到一个目录树中。inode 描述常规文件或目录。对于常规文件,inode 保留对一组块标记的引用,每个块标记描述文件块号到卷块号的映射(each describing the mapping of a file block number to a volume block number);对于目录,inode 保留对父目录中一组子目录项的引用(上面提到的引用实际上是元对象标识符,如下所述)。

这三种元数据被抽象为一种称为 metaobject 的数据类型,metaobject 具有一个字段,用于保存目录条目、inode 或块标记的特定数据。此公共数据类型用于访问磁盘上和内存中的元数据。创建新文件系统时,元对象在磁盘上以连续 4K 大小的扇区初始化,每个元对象都分配了一个唯一的标识符。在 pfs_mount 中,所有的元对象都被加载到内存中,并被分成 chunk 和 type 组。

使用元组(元对象标识符,元对象类型)在内存中访问元对象。标识符的高位用于查找元对象所属的 chunk,type 用于查找 chunk 中的组,最后将标识符的低位作为索引访问组中的元对象。

要更新一个或多个元对象,需要准备一个事务,并将每次更新记录为一个事务操作。事务操作保留其修改对象的旧值和新值。完成所有更新后,就可以提交事务了。提交过程需要数据库节点之间的协调和同步,如下小节所述。如果提交失败,将使用事务中保存的旧值回滚更新。

6.2 协调与同步

为了支持文件系统元数据的同步,PolarFS 将元数据修改作为事务记录到日志中文件。那个日志文件由数据库节点轮询以获取新事务。一旦找到新事务,节点将检索和重放新事务(new transactions are retrieved and replayed by the nodes)。

通常,只有一个实例可以写入日志文件,而多个实例可以读取日志文件。在网络分区或管理的情况下,可能有多个实例写入日志文件。在这些情况下,我们需要一种机制来协调对日志文件的写入。 PolarFS 为此使用了磁盘 Paxos 算法[10]。请注意,这里使用的磁盘 Paxos 算法的目的与 ParallelRaft 大不相同。后者是为了保证块副本之间的数据一致性。

磁盘 Paxos 算法运行在一个由 4K 大小的页面组成的文件上,这些页面可以原子性地读或写。这些页被解释为每个数据库节点的一个 Leader 记录加上数据块( These pages are interpreted as one Leader record plus data blocks for each database node )。每个数据库节点使用数据块页来编写自己的提议和读取其他节点的提议( The data block pages are used by each database node to write the proposal of itself and to read the proposals of others )。Leader record 页面包含当前 Paxos winner 的信息和日志定位点(log anchor)。磁盘 Paxos 算法只由一个写节点运行。只读节点不运行磁盘 Paxos 。相反,他们通过检查 Leader record页面中的日志定位点来进行投票。如果日志定位点更改,只读节点知道日志文件中有新事务,它们将检索这些新事务以更新其本地元数据。我们用一个示例来解释事务提交过程,如 图8 所示。其步骤如下:

(1) 在将块 201 分配给文件 316 之后,节点1 获取 Paxos 锁,该锁最初是空闲的。(2) 节点1 开始将事务记录到日志。最新写入的目录被标记为 pending tail。在所有目录被存储之后,pending tail 成为日志的有效末尾。(3) 节点1 使用修改后的元数据更新超级块。同时,节点2 尝试获取 节点1 已经持有的互斥锁。节点2 必然会失败,它稍后将重试。(4) 在 节点1 释放锁之后,节点2 成为互斥锁的合法所有者,但是 节点1 附加的日志中的新条目确定 节点2 中的本地元数据缓存已过时。(5) 节点2 扫描新条目并释放锁。然后,节点2 回滚未记录的事务并更新本地元数据。最后,节点2 重试事务。(6) 节点3  开始自动同步元数据,只需要加载增量项并在本地内存中重演。

7. 设计选择和经验教训

除了系统架构、I/O 模型和共识协议外,还有一些有趣的设计问题值得讨论。其中一些属于 PolarFS 本身,而另一些则针对数据库特性。

7.1 集中或分散

分布式系统有两种设计模式:集中式和分散式。集中式系统,如GFS[11]和HDFS[5],包含一个主节点,负责元数据的保存和成员管理,这种系统实现起来相对简单,但从可用性和可扩展性的角度来看,单个 master 可能成为整个系统的瓶颈。像 Dynamo[8] 这样的分散系统则恰恰相反。在该系统中,节点之间是对等关系,元数据是分片的,节点之间是冗余的。分散系统被认为是更可靠的,但实现和推理也更加的复杂。

PolarFS 在集中式和分散式设计之间进行了权衡。一方面,PolarCtrl 是一个集中式主机,负责诸如资源管理之类的管理任务,并接受控制面板( control plane )请求(如创建 volume)。另一方面, ChunkServers 正在互相交谈,运行一个共识协议,在不涉及 PolarCtrl 的情况下自主处理故障和恢复。

7.2 自下而上的快照

快照是数据库的常见需求。PolarFS 支持快照功能,简化了上层 POLARDB 快照设计。

在 PolarFS 中,存储层为上层提供可靠的数据访问服务,POLARDB 和 libpfs 有自己的日志机制来保证事务的原子性和持久性。PolarFS 存储层提供了磁盘中断一致性的快照, POLARDB 和 libpfs 从底层 PolarFS 快照重建自己的一致数据映像。

这里的磁盘中断一致性是指,如果快照命令在时间点 T 触发,则存在某个时间点 t0,因此t0 之前的所有 I/O 操作都应包含在当前快照中,T 之后的 I/O 操作必须排除在外。然而,在间隔 [T0,T] 内 I/O 操作的状态通常是很短的时间跨度,是不确定的。这种行为类似于磁盘仍在写入时关机时发生的情况。

当发生意外灾难或需要进行主动审计时,PolarCtrl 会将最新的数据快照分配给 POLARDB  实例,POLARDB 和 libpfs 将使用存储的日志重建一致的状态。

PolarFS 以一种新颖的方式实现了磁盘中断一致性快照,在快照过程中不会阻塞用户的 I/O 操作。当用户发出快照命令时,PolarCtrl 通知 PolarSwitch 生成快照。从那时起,PolarSwitch 在以下请求中添加了一个 snapshot 标记,它指示它的主机请求发生在快照请求之后。收到带有快照标记的请求时,ChunkServer 将在处理此请求之前生成快照。 ChunkServer 通过复制块映射元数据信息来创建快照,这是一种快速的方法,并处理将来的请求,这些请求将以一种 写时复制 的方式修改这些块。在带有 snapshot 标记的请求完成后,PolarSwitch 停止向传入的请求添加 snapshot 标记。

7.3 外部服务与内部可靠性

工业系统的可靠性是极其重要的,特别是对于像 PolarFS 这样的系统,它正在承担 7X24公共云服务。在这样的系统中,应该有各种各样的可靠性维护任务,以防止服务和相关收益的损失。对于 PolarFS 来说,如何高效地运行这些任务,同时为繁重的用户负载提供平滑的服务,这是一个巨大的挑战。

以 streaming catch up in ParallelRaft 为例,当 Leader 在处理大量的工作负载时,会不断地生成大量的日志,即使在长时间的日志提取之后,Follower 也无法赶上。由于 I/O 限制,复制整个块的时间也可能相当长,而且不可预测。这可能会导致我们进行权衡:恢复时间越短,所需资源越多,对系统性能的牺牲就越大;而如果恢复需要很长时间,则系统可用性将面临风险。

我们的解决方案是将一个 chunk 水平分割成小的逻辑块(logical pieces),例如, 128KB 的块。chunk 的完整数据同步被分成许多小的子任务,每个子任务只重新同步一个 128KB 的片段。这些较小的子任务的运行时要短得多,而且更容易预测。此外,还可以在次同步任务( sub-resync-tasks )之间插入空闲周期,以控制 streaming catch up 所需的网络/磁盘带宽成本。

其他耗时的任务,如检查副本之间的一致性的完整数据验证例程,也可以类似地实现。我们必须考虑使用同步流控制来平衡外部服务质量和 PolarFS 的系统可靠性。由于篇幅的限制,我们将不在这里进一步讨论这种权衡的细节。

8. 价值评估

我们已经实现了 PolarFS ,并在阿里云上发布了 POLARDB 作为公共数据库服务。在本节中,我们评估和分析了 PolarFS 和 POLARDB 的性能和可伸缩性。对于 PolarFS,我们在集群上对其进行了评估,将其与本地 NVMe 上的 Ext4 和 Ceph[35] 上的 CephFS 进行了比较,后者是一种广泛使用的开源分布式存储系统。对于 POLARDB,我们将其与我们最初的 MySQL 云服务 RDS 和 localext4 上的 POLARDB 进行了比较。

8.1 PolarFS 实验配置

在文件系统性能方面,我们重点研究了三个系统( PolarFS 、CephFS 和 Ext4 )的端到端性能,包括不同工作负载和访问模式下的延迟和吞吐量。

PolarFS 和 CephFS 的实验结果是在一个由 6 个存储节点和一个客户端节点组成的集群上收集的。节点通过 RDMA-enabled NIC 相互通信。

Ceph 的版本是 12.2.3,我们将其存储引擎配置为 bluestore,将通信消息器类型配置为 async + posix 。Ceph 的存储引擎可以配置为使用 RDMA verbs 运行,但其文件系统仅支持 TCP/IP 网络堆栈,因此我们将 CephFS 配置为使用 TCP/IP 运行。对于 Ext4,我们在丢弃所有旧数据后在 SSD 上创建一个新的 Ext4。

我们使用 FIO[1]生成具有不同 I/O 请求大小和并行性的各种类型的工作负载。FIO 将首先创建文件并将其扩展到给定的长度(这里是10G),然后再进行性能评估。

8.2 I/O 延迟

如 图9 所示,PolarFS 对于 4K 随机写入的延迟约为 48µs,与 CephFS 的延迟(约760µs)相比,非常接近于Ext4在本地 SSD 上的延迟(约10µs)。 PolarFS 的平均随机写入延迟是本地 Ext4 的1.6~4.7倍,CephFS  的平均随机写入延迟是本地 Ext4 的6.5~75倍,这意味着分布式 PolarFS 几乎提供了与 local Ext4 相同的性能。PolarFS 和 CephFS 与 Ext4 的平均顺序写入延迟比分别为 1.6-4.8 和 6.3-56。 PolarFS 和 CephFS 与 Ext4 的平均随机读取延迟比分别为 1.3-1.8 和 2-4。 PolarFS 和 CephFS 与 Ext4 的平均顺序读取延迟比分别为 1.5-8.8 和 3.4-14.9。Ext4 的 PolarFS /CephFS 的大请求(1M)性能降低不同于小请求(4K),因为网络和磁盘数据传输占用了大部分的大请求执行时间。

PolarFS 的性能比 CephFS  好得多,这有几个原因。首先,PolarFS 用一个线程处理有限状态机的 I/O ,如第 4 节所述。这避免了由多个线程组成的 I/O 管道所需的线程上下文切换和重新调度,就像 CephFS  所做的那样。第二,PolarFS 优化了内存分配和分页,在 I/O 生命周期中使用内存池来分配和释放内存,减少了对象的构建和销毁操作,使用了大量的页面来减少 TLB 的丢失和分页,CephFS 中没有对应的方法。第三,PolarFS 中关于数据对象的所有元数据都保存在内存中,而 CephFS 中的每个 PG(Placement Group)中只有部分元数据保存在一个小缓存中,因此 PolarFS 大多数 I/O 操作不需要额外的元数据 I/O ,但 CephFS 有时需要对元数据进行额外的 I/O 操作。最后,PolarFS 中的用户空间 RDMA 和 SPDK 比 CephFS 中的内核 TCP/IP 和块驱动具有更低的延迟。

8.3 I/O 吞吐量

图10 显示了三个文件系统的总体 I/O 吞吐量。对于随机读写,本地 SSD 上的 Ext4、 PolarFS 和 CephFS IOPS都是随着客户端作业数的变化而变化的;Ext4 的可扩展性瓶颈是单个本地 SSD IOPS,PolarFS 的扩展瓶颈是网络带宽,CephFS  的瓶颈是数据包处理能力(packet processing capacity)。Ext4 和 PolarFS 的随机 4K 写/读的 I/O 吞吐量比 CephFS 分别高 4.4/5.1,4/7.7。

对于顺序读/写,对于本地系统和分布式系统,几乎所有的请求都由一个磁盘来处理。Ext4和 PolarFS 都可以很好地随客户端作业的数量进行扩展,直到由于 I/O 瓶颈而达到极限,而 CephFS 的瓶颈是其软件,它不能使磁盘 I/O 带宽达到饱和。

在没有额外的网络 I/O 的情况下,Ext4 在 1 台客户端上的顺序读吞吐量远远高于 PolarFS 和 CephFS,但随着客户端数量增加到2台,其顺序读吞吐量急剧下降,吞吐量与 2 台客户端的随机读吞吐量基本相同。我们重复了几次评估,结果是可重复的。我们猜测这是由我们的 NVMe SSD 的特性造成的。SSD-NVM 内置了一个 DRAME 缓冲区。当固件猜测工作负载看起来像顺序读取时,它会将后续数据块预取到 DRAM 缓冲区中。我们猜测预取机制在非交错 I/O 模式下会工作得更好。

8.4 评估 POLARDB  

为了证明我们的超低 I/O 延迟 PolarFS 对于数据库的好处,我们对 POLARDB  进行了一些初步测试,POLARDB 是一个专门与 PolarFS 一起开发的共享存储云数据库。我们比较了三个系统的吞吐量:(1) POLARDB 在 PolarFS 上,(2) POLARDB 在本地 Ext4,以及(3) 阿里云数据库MySQL云服务RDS参考。测试 (1) 和 (2) 与以前的 PolarFS 硬件环境处于相同的硬件环境下。

我们分别在模拟的只读、只写(update:delete:insert=2:1:1)和读/写混合(read:write=7:2)OLTP 工作负载下运行 Sysbench[21]来分别评估三个数据库。本实验使用的数据集由 250 个表组成,每个表有 850 万条记录。测试数据的总大小为 500GB。

如 图11 所示, PolarFS 上 POLARDB 的吞吐量非常接近本地 Ext4 上的吞吐量,而 PolarFS 提供了额外的 3 副本高可用性。 PolarFS 上的 POLARDB 实现了 653K 读/秒、160K 写/秒和173K (读写)/秒。

除了与 PolarFS 集成外, POLARDB 还应用了一系列基于 Aliyun RDS 的数据库优化。结果, POLARDB 的读、写、读写混合吞吐量几乎是 RDS 的 2.79、4.72、1.53倍,如图所示。数据库级优化超出了本文的范围。

9. 相关工作

存储系统。GFS[11] 及其开源实现 HDFS[5] 提供分布式系统服务,GFS 和 HDFS 均采用主从架构,主机维护系统元信息、leader 租约机制,并负责故障恢复。与 GFS 和 HDFS 不同, PolarFS 采用混合架构。PolarFS 的主节点主要负责资源管理,一般不增加 I/O 路径的干扰,使主机升级和容错更容易。

Ceph[35] 是一种广泛部署的存储系统。它使用 CRUSH 散列算法来定位数据对象,为正常的 I/O 执行和容错带来了一个简单的设计。RAMCloud[29]是一个低延迟的 KV 系统,它通过在集群之间随机复制数据并利用集群资源并行恢复故障来提供快速恢复。随机的数据放置会使一台机器在云环境下承载上千个用户的数据,这意味着一台机器的崩溃会影响到上千个用户,这是云服务提供商无法接受的。PolarFS control master 在将数据块分配给实际服务器时,在故障影响和 I/O 性能之间提供了细粒度的权衡。

ReFlex[20]使用 NVMe-SSD 来实现远程磁盘,它提出了一个 QoS 调度器,可以强制实现尾部延迟和吞吐量服务级别目标,但与 PolarFS 不同,它的远程磁盘是单副本的,没有可靠性支持。CORFU[3] 将 flash 设备集群组织为单个共享日志,多个客户端可以通过网络并发访问该日志。共享日志设计简化了分布式环境下的快照和复制,但增加了现有系统使用 CORFU 的工程难度,否则 PolarFS 将为用户提供块磁盘抽象和类似 POSIX 的文件系统接口。

Aurora[32] 是亚马逊在其 ECS 服务之上的云关系数据库服务。Aurora 通过将数据库重做日志处理推送到存储端,解决了计算端和存储端之间的网络性能约束。Aurora 还修改了 MySQL 的 innodb 事务管理器,以便于日志复制,而 POLARDB 在 PolarFS 上投入了大量资金,但使用了类似 POSIX 的 API,最终对数据库引擎本身几乎没有修改。Aurora 的 redo 逻辑下推设计打破了数据库和存储之间的抽象,使得每一层的修改更加困难,而 PolarFS 和 POLARDB 保留了每一层的抽象,使得每一层更容易采用其最新技术。

一致性协议。Paxos [22]是分布式系统中最著名的一致性算法之一。但是,它很难理解和正确实施。Paxos 的作者只证明了 Paxos 可以解决一致性问题的一个实例,而没有说明如何使用 Multi Paxos 来解决实际问题。目前,大多数一致性算法,如 Zookeeper 使用的Zab[16],被认为是 “Multi Paxos” 算法的变种。Zab 用于提供原子广播原语属性,但也很难理解。 Raft [28] 是为可理解性而设计的,也是 “Multi Paxos 算法。它引入了两个主要约束,第一个是提交日志必须是连续的,第二个是提交顺序必须序列化。这两个约束使得 Raft 很容易理解,但也会对并发 I/O 性能产生影响。有些系统使用多组 Raft ( Multi Groups Raft )来提高 I/O 并行性,将键分成多个组,但它不能解决提交日志包含多个组的键的问题。ParallelRaft 通过不同的方式实现这一目标。提交日志允许有漏洞,并且可以无序提交日志条目,但冲突日志条目将以严格的顺序提交。

RDMA 系统。与传统的 TCP/UDP 网络协议相比,RDMA 在降低 CPU 利用率的同时,将往返时延降低了一个数量级,许多新的系统使用 RDMA 来提高性能。FaRM[9] 是一个基于 RDMA 的分布式共享内存,用户可以使用它的程序原语来构建自己的应用程序。DrTM[34] 是一个基于 RDMA 和硬件事务存储器的分布式内存数据库,RDMA 可以快速地从远程访问数据,也可以导致硬件事务内存中的本地事务中止,DrTM 结合 RDMA 和硬件事务存储器来实现分布式事务。FaRM 和 DrTM 都专注于内存事务,而 PolarFS 使用 RDMA 构建分布式存储。

Pilaf[27] 是一种基于 RDMA 和自验证数据结构的键值存储,但与 PolarFS 不同,Pilaf 的键值接口相对简单,没有分布式共识协议。

APUS[33] 使用 RDMA 构建可伸缩的 Paxos,PolarFS 提供了一个对高并行 I/O 友好的 ParallelRaft。PolarFS 使用 RDMA 来构建可靠的块设备,由于 RDMA 比传统网络协议消耗更少的 CPU,PolarFS 可以以运行到完成的方式处理 I/O ,避免了上下文切换。结合 RDMA 和 SPDK,存储节点可以在整个 I/O 生命周期内不接触负载完成 I/O,所有负载的移动都由硬件完成。

10. 结论

PolarFS 是一个分布式文件系统,它提供了极高的性能和高可靠性。PolarFS 采用了新兴的硬件和最先进的优化技术,如 OS-bypass 和零拷贝,使其延迟与 SSD 上的本地文件系统相当。为了满足数据库应用的高 IOPS 要求,我们开发了一种新的一致性协议 ParallelRaft。 ParallelRaft 在不牺牲存储语义一致性的前提下,放宽了 Raft 严格的顺序写入,提高了 PolarFS 的并行写入性能。在重负载下,我们的方法可以将平均延迟减半,系统带宽增加一倍。PolarFS 在用户空间中实现了类似 POSIX 的接口,这使得 POLARDB 只需稍加修改就可以实现性能改进。

原论文中的声明:

本作品根据 Creative Commons AttributionNoDerivatives 4.0 国际许可证授权。要查看此许可证的副本,请访问http://creativecommons.org/licenses/by-nc-nd/4.0/。对于本许可证涵盖范围以外的任何用途,请通过电子邮件获得许可info@vldb.org。版权归所有人/作者所有。授权 VLDB 基金会出版权。

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

  1. 面向云数据库,超低延迟文件系统PolarFS诞生了

    阿里妹导读:随着国内首款Cloud Native自研数据库POLARDB精彩亮相ICDE 2018的同时,作为其核心支撑和使能平台的PolarFS文件系统的相关论文"PolarFS: An ...

  2. 面向云数据库,超低延迟文件系统PolarFS诞生了 1

    摘要: 如同Oracle存在与之匹配的OCFS2,POLARDB作为存储与计算分离结构的一款数据库,PolarFS承担着发挥POLARDB特性至关重要的角色.PolarFS是一款具有超低延迟和高可用能 ...

  3. 腾讯云快直播——超低延迟直播技术方案及应用

    正文字数:4361  阅读时长:7分钟 随着直播业务的发展,在线教育,连麦直播.赛事直播等高实时性直播场景的出现,用户对于直播流畅度.低延迟等性能的要求愈加严苛.腾讯云直播技术高级工程师陈华成 从5G ...

  4. 阿里云国际 CDN 和低延迟全球云解决方案

    延迟与隐藏程度成反比.C 数据存在,并且连接已建立,但在接收客户端请求响应和显示用户请求的内容时存在明显延迟.将数据从源移动到目标时,会出现即时丢失.延迟是数据发送器和数据接收器之间的时间差.此外,当 ...

  5. 光棍节促销热火朝天,百度云“0.2超低折”堪称一朵奇葩

    光棍节促销热火朝天,百度云"0.2超低折"堪称一朵奇葩 文/王易见 时下,一年一度的光棍节成了电商网站重磅出击的一个特殊节日,包括天猫.京东商城等电商巨头纷纷展开"血拼& ...

  6. FatNet:一个用于三维点云处理的特征关注网络

    点击上方"3D视觉工坊",选择"星标" 干货第一时间送达 小黑导读 论文是学术研究的精华和未来发展的明灯.小黑决心每天为大家带来经典或者最新论文的解读和分享,旨 ...

  7. 如何使用python实现一个优雅的词云?(超详细)

     什么是词云 "词云"就是对网络文本中出现频率较高的"关键词"予以视觉上的突出,形成"关键词云层"或"关键词渲染". 从 ...

  8. 【线上分享】边缘云跨区域超低延时架构设计与网络优化实践

    今晚19:30,我们邀请到全球边缘云服务商Zenlayer产品和平台副总裁陈硕,以及Telin Singapore全球业务负责人庄文杰.技术与运营副总裁Sendang,探索不同场景下延时要求.网络架构 ...

  9. 火山引擎、阿里云、腾讯云联合发布“超低延时“直播技术标准

    2 月 25 日,在火山引擎举办的视频云科技原力峰会上,火山引擎与阿里云.腾讯云联合发布一项"超低延时直播协议信令标准".该标准首次正式定义了直播"客户端-服务器&quo ...

最新文章

  1. git遇到的一些小问题
  2. C#编程语言与面向对象——继承
  3. Hibernate映射详解(二)--多对一,一对多单向,一对多双向关联映射
  4. EOS账户系统(4)账户权限分级
  5. noi linux硬盘启动,NOI Linux + Windows 10双系统(Win10引导)安装记录
  6. 动态时间规整_动态规划-数组系列(10%)
  7. 数据库:30种SQL语句优化,进阶必备!
  8. 2020中国奢侈品消费者数字行为洞察报告
  9. 学java出来工作会很忙吗?
  10. 空间apiLinux系统调用及用户编程接口(API)学习
  11. cesium加载倾斜优化_干货 | 6款倾斜摄影裸眼3D采集软件推荐给大家
  12. OpenDDS系列(1) —— OpenDDS 简介
  13. 黑色星期五 问题描述   有些西方人比较迷信,如果某个月的13号正好是星期五,他们就会觉得不太吉利,用古人的说法,就是“诸事不宜”。请你编写一个程序,统计出在某个特定的年份中,出现了多少次既是13号又
  14. 【算法】ACO蚂蚁寻路最短路径TSP问题-多篇文章总结
  15. js 根据链接生成二维码
  16. IP2188-Datasheet
  17. 人工神经网络(ANN/NN)、感知机(PLA)
  18. JavaSE第21篇:Java之IO流下篇
  19. shell 中 ()、(())、[]、[[]]、{} 的作用
  20. 什么软件可以把照片变成漫画?试试这几款图片处理工具

热门文章

  1. 【Boost】boost库中function和bind一起使用的技巧(二)
  2. DLL入门浅析(2)——如何使用DLL
  3. Go channel 的妙用
  4. 美团--美团骑手包裹区间分组
  5. 腾讯2020校园招聘----逛街
  6. 开启未来十年的编码技术之门
  7. 5G时代探索互动立体视频信息承载的新可能
  8. 李大龙:音视频技术是互联网品质生活的连接器
  9. 数据结构与算法之递归题目
  10. 裸眼 3D 是什么效果?