暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

原创 吴松 云和恩墨 2021-11-15
1905

摘要

PolarFS 是一个有极低时延和高可用性的分布式文件系统,为 POLARDB 数据库服务所设计。PolarFS 在用户态运行,有着轻量级的网络和 I/O 栈,能够充分利用 RDMA、NVMe 及 SPDK 等新兴技术。通过这种方式,PolarFS 的端到端时延大幅减小,实践证明 PolarFS 的写时延与 SSD上的本地文件系统非常接近。为了保持PolarFS副本一致性,同时最大化 I/O吞吐,开发了 ParallelRaft,一种基于 Raft 的协商一致性协议。ParallelRaft 利用数据库的容忍乱序I/O的能力,解决了 Raft 协议需要严格序列化的问题。ParallelRaft 继承了 Raft 的可理解性和易实现性,同时为 PolarFS 提供了更好的 I/O 扩展性。本文也介绍了给 POLARDB 极大支持的 PolarFS 的共享存储架构。


1. 介绍

近年来,存储计算分离成为云计算行业的趋势。这样的设计使得架构更加灵活,而且可以充分利用共享存储的能力:
(1)计算和存储节点可以使用不同的硬件,可以分别进行配置。例如:计算节点不需要再考虑内存和磁盘容量的比率,这个数值高度依赖实际应用场景且难以预测。
(2)一个集群内存储节点上的磁盘可以组成存储池,减少碎片产生,同时可以解决节点之间磁盘使用不均衡和空间浪费等问题。存储集群的容量和吞吐量可以轻松透明地横向扩展。
(3)由于数据全部存储在存储集群上,计算节点上没有本地持久化状态,使得数据库迁移更容易也更快。数据可靠性因为底层分布式存储系统的副本和其他高可用机制也得到了提升。

云数据库服务也可以在这种架构中受益。

首先,数据库可以建立在一个更安全可靠、更容易扩展的环境中,这个环境基于虚拟化技术,如 Xen、KVM 或者 Docker。其次,数据库的一些重要特性,如多个 read-only 实例、checkpoint 技术可以通过存储集群提供的能力得到加强。这些能力包括 fast I/O、数据共享、快照等。


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

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

本文描述了 PolarFS 分布式文件系统的设计和实现,它通过采用以下机制提供了超低延迟、高吞吐量和高可用性。

首先,PolarFS 充分利用了 RDMA 和 NVMe SSD 等新兴硬件,在用户空间实现了轻量级的网络栈和 I/O 栈,避免陷入内核和处理内核锁。
其次,PolarFS 提供了一个类似于 POSIX 的文件系统 API,其目的是编译到数据库进程中,替代操作系统提供的文件系统接口,使整个 I/O 路径可以保留在用户空间。
第三,PolarFS的数据面的 I/O 模型设计为在关键数据路径上消除锁和避免上下文切换:去掉所有不必要的内存拷贝,同时大量使用 DMA 在主存和 RDMA 网卡/NVMe 磁盘之间传输数据。

有了所有这些特性,PolarFS 的端到端延迟已经大大降低,非常接近 SSD 上的本地文件系统。

部署在云生产环境中的分布式文件系统通常有数千台机器。在这样的规模内,由硬件或软件错误引起的故障很常见。因此,需要一个共识协议来确保所有提交的修改在极端情况下不会丢失,并且副本始终可以达成一致并且数据相同。

Paxos 系列协议在解决共识方面得到广泛认可。Raft 是 Paxos 的一个变体,更容易理解和实现。许多分布式系统都是基于 Raft 开发的。

然而,当 Raft 应用于 PolarFS 时,我们发现 Raft 在使用超低延迟硬件(如 NVMe SSD 和 RDMA,其延迟在几十微秒量级)时严重阻碍了 PolarFS 的 I/O 可扩展性。
所以我们开发了 ParallelRaft,一个基于 Raft 的增强共识协议,它允许无序的日志确认、提交和应用,同时让 PolarFS 符合传统的 I/O 语义。
通过该协议,PolarFS 的 I/O 并发性得到了显著提升。

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


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 栈的开销成为瓶颈。
正如之前的研究所揭示的那样,仅完成一个 4KB I/O 请求就需要执行大约 20,000 条指令。最近,英特尔发布了存储性能开发套件 (SPDK),这是一套工具和库,用于构建基于高性能 NVMe 设备的、可扩展的用户态存储应用程序。它将所有必需的驱动程序移动到用户空间,并通过轮询而不是中断来实现高性能,这种方式避免了内核上下文切换并消除了中断处理开销。

RDMA。 RDMA 技术提供了数据中心内,服务器之间的低延迟网络通信机制。例如,在连接到同一交换机的两个节点之间传输 4KB 数据包大约需要 7µs,这比传统的 TCP/IP 网络堆栈快得多。 之前的一些研究表明 RDMA 可以提高系统性能。应用程序通过 Verbs API 访问队列对 (QueuePair) 与 RDMA 网卡交互, 一个 QP 由一个发送队列和一个接收队列组成,此外,每个 QP 关联另一个完成队列 Completion Queue(CQ)。CQ 通常作为完成事件/信号的轮询目标。 Send/Recv 通常被称为双向操作,因为每个 Send 操作都需要由远程进程调用一个与之匹配的 Recv 操作,而 Read/Write 被称为单向操作,因为远端内存由网卡操作而不涉及任何远端 CPU。

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


3. 架构

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


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

3.1 File System Layer

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

3.1.1 libpfs


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

当数据库节点启动时,pfs_mount 将指定卷挂载到数据库节点上并初始化文件系统状态。volname 是分配给 POLARDB 实例的磁盘卷的全局标识符,hostid 是数据库节点的下标,在 disk paxos 投票算法中用作标识符(参见 6.2 节)。
在挂载过程中,libpfs 从卷加载文件系统元数据,并构建起数据结构,例如主存中的目录树、文件映射表和块映射表(见 6.1 节)。pfs_umount 在数据库销毁期间与卷分离并释放资源。在磁盘卷空间增长之后,应该调用 pfs_mount_growfs 来识别新分配的块并将它们标记为可用。
其他函数是文件和目录操作函数,相当于 POSIX API 中的对应项。文件系统元数据管理的细节在第 6 节中描述。

3.2 Storage Layer

存储层为文件系统层提供管理和访问磁盘卷的接口。为每套数据库实例分配一个卷,它由一个 chunk 列表组成。卷的容量从 10GB 到 100TB,可以满足绝大多数数据库的需求,并且可以通过在卷中追加 chunk 来按需扩容。
可以像传统存储设备一样以 512B 对齐方式随机访问(读取、写入)卷。单个 I/O 请求中对同一个 chunk 的多个修改是原子的。

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

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

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

Block. 一个 chunk 在 ChunkServer 内部被进一步划分为多个 block,每个 block 设置为64KB。 block 是按需分配并映射到 chunk 以实现精简配置。 一个 10 GB 的 chunk 包含 163840 个 block。 chunk 的 LBA(Logical Block Address,线性地址范围从0到10GB,表示 block 在 chunk 内的偏移)到 block 物理地址的映射表,本地存储在 ChunkServer 的中,同时存储在 ChunkServer 中的还有每个磁盘上空闲块的位图。 单个 chunk 的映射表占用 640KB 内存,这个空间相当小,可以缓存在 ChunkServer 的内存中。

3.2.1 PolarSwitch

PolarSwitch 是一个部署在数据库服务器上的守护进程,与一个或多个数据库实例一同部署。在每个数据库进程中,libpfs 将 I/O 请求转发到 PolarSwitch 守护进程。每个请求都包含寻址信息,如卷标识符、偏移量和长度,从中可以识别相关的 chunk 。
一个 I/O 请求可能跨越多个 chunk,在这种情况下,请求被进一步划分为多个子请求。最后一个原始的请求(elemental request)被发送到 chunk 的 leader 所在的 ChunkServer 。

PolarSwitch 通过查找本地元数据缓存,找出一个 chunk 的所有副本的位置,本地元数据缓存会与 PolarCtrl 同步。 一个 chunk 的副本形成一个共识组,其中一个是 leader,其他是 follower。只有leader 可以应答 I/O 请求。共识组中的 leader 发生变化也同步和缓存到 PolarSwitch 的本地缓存中。如果发生响应超时,PolarSwitch 将继续以指数补偿的方式重试,同时检测是否发生了 leader 选举,如果发生了,则切换到新的 leader 并立即重传。

3.2.2 ChunkServer

ChunkServer 部署在存储服务器上。 一个存储服务器上运行着多个 ChunkServer 进程,每个 ChunkServer 拥有一个独立的 NVMe SSD 盘,并绑定到一个专用的 CPU 核上,这样两个 ChunkServer 之间就不会发生资源争用。
ChunkServer 负责存储 chunk 并提供对 chunk 的随机访问。每个 chunk 都包含一个预写日志 (WAL),在更新 chunk blocks 之前,对 chunk 的修改会追加到日志中,以确保原子性和持久性。
ChunkServer 使用 3D XPoint SSD 和普通 NVMe SSD 混合的 WAL 的写缓存,日志优先放置在 3D XPoint 缓冲区中。如果缓冲区已满,ChunkServer 尝试回收过期的日志。如果 3D XPoint 缓冲区中仍然没有足够的空间,则将日志写入 NVMe SSD。而 chunk blocks 始终写入 NVMe SSD。

ChunkServer 使用 ParallelRaft 协议相互复制 I/O 请求并形成共识组。一个 ChunkServer 可能因为各种原因离开其共识组,需要根据具体的原因进行处理。有时它是由偶然和临时故障引起的,例如网络暂时不可用,或服务器升级并重新启动。这种情况最好等待断开的 ChunkServer 重新上线,重新加入赶上其他人;另一些情况下,故障是永久性的或往往会持续很长时间,例如服务器损坏或脱机。这时候应该将对应的 ChunkServer 上的所有 chunk 迁移到其他 ChunkServer 上,以重新获得足够数量的副本。

断开连接的 ChunkServer 将始终努力自主重新加入共识组,以缩短不可用时间。然而,PolarCtrl 可以做出互补的决策。PolarCtrl 会定期收集之前断开连接的 ChunkServers 列表,并挑选出看起来像永久故障的 ChunkServers 。有时很难准确做出决策。例如,某个 ChunkServer 的磁盘非常慢导致其延迟可能比其他的 ChunkServer 要长得多,但这个 ChunkServer 始终可以响应存活探测。基于关键组件性能指标统计的机器学习算法对此很有帮助。

3.2.3 PolarCtrl

PolarCtrl 是 PolarFS 集群的控制面。它部署在一组专用机器(至少三台)上,以提供高可用服务。PolarCtrl 提供集群控制服务,例如 节点管理、卷管理、资源分配、元数据同步、监控等。
PolarCtrl 负责:

  1. 跟踪集群中所有 ChunkServer 的成员资格和活跃度,如果 ChunkServer 过载或不可用的持续时间超过阈值,则开始将块副本从一台服务器迁移到其他服务器。
  2. 在元数据存储的数据库中维护所有卷的状态和 chunk 位置信息。 
  3. 创建卷并将 chunk 分配给 ChunkServers。 
  4. 使用 push 和 pull 方法将元数据同步到 PolarSwitch。
  5. 监控每个卷和 chunk 的延迟/IOPS 指标,沿 I/O 路径收集跟踪数据。 
  6. 定期调度副本内和副本间的 CRC 校验。

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

作为控制平面,PolarCtrl 不在关键 I/O 路径上,可以使用传统的高可用性技术保证其服务连续性。 即使在 PolarCtrl 崩溃和恢复之间的短暂间隔期间,PolarFS 中的 I/O 流也不太可能因为 PolarSwith 上缓存的元数据和 ChunkServer 的自管理受到影响。


4. I/O 执行模型

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

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

共享内存的结构为多个环形缓冲区(ring buffer)。在共享内存的一端,libpfs 将块 I/O 请求放入一个环形缓冲区,环形缓冲区通过轮询的方式进行选择,然后等待其执行完成。在另一端,PolarSwitch 通过一个专用的线程不断轮询所有环形缓冲区。
一旦发现新请求,PolarSwitch 将请求从环形缓冲区取出,并将这些请求根据从 PolarCtrl 获得的路由信息转发到对应的 chunk 的 leader 节点。

ChunkServer 使用预写日志 (WAL) 技术来确保原子性和持久性,其中 I/O 请求在提交和应用之前先写入日志。日志被复制到其他副本集,并使用名为 ParallelRaft(下一节详述)的共识协议来保证副本之间的数据一致性。在 I/O 请求被持久化到大多数副本的日志中之前,它不会被识别为已提交。只有在此之后,该请求才能对客户端响应并应用于数据块。


图 4 显示了如何在 PolarFS 内部执行写 I/O 请求。

  1. POLARDB 通过 PolarSwitch 和 libpfs 之间的环形缓冲区向 PolarSwitch 发送写 I/O 请求。
  2. PolarSwitch 根据本地缓存的集群元数据,将请求传递给对应的 chunk 的 leader 节点。
  3. 当新的写请求到达时,Leader 节点中的 RDMA 网卡会将写请求放入提前分配好的缓冲区,并据此构造一个请求对象加入请求队列中。I/O 循环线程不断轮询请求队列。一旦它看到一个新的请求到达,它就会立即开始处理。
  4. 请求通过 SPDK 写入 chunk 对应的 WAL 日志块,通过 RDMA 传播到 follower 节点。这两个操作都是异步调用,所以数据传输是并发进行的。
  5. 当请求到达一个 follower 节点时,follower 节点中的 RDMA 网卡也会将复制请求放入提前分配的缓冲区中,并加入到复制队列中。
  6. 然后 follower 上的 I/O 循环线程被触发,将请求通过 SPDK 异步写入 WAL 日志块。
  7. 当 follower 上的 write 成功后,回调函数通过 RDMA 向 leader 发回一个 acknowledge response。
  8. 当成功收到大多数 follower 的响应后,leader 通过 SPDK 向数据块申请写请求。
  9. 之后,leader 通过 RDMA 回复 PolarSwitch。
  10. PolarSwitch 然后将请求标记为完成并通知客户端。

读取 I/O 请求(更简单)由 leader 直接处理。在 ChunkServer 中有一个名为 IoScheduler 的子模块,它负责仲裁并发的 I/O 请求,确定这些请求在 ChunkServer 上磁盘的 I/O 操作顺序。IoScheduler 保证读操作总能获到最新已提交的数据。

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


5. 一致性模型

5.1 A Revision of Raft

生产环境的分布式存储系统需要一个共识协议来保证所有提交的修改在任何极端情况下都不会丢失。 在开始设计时,考虑到实现的复杂性,我们选择了 Raft。 然而,其中的一些问题很快就出现了。

Raft 被设计为高度序列化,以达到简单和可理解的目的。leader 和 follower 上的日志都不允许有空洞,这意味着日志项在 follower 确认,leader 提交及应用到所有副本时都是按照顺序执行的。
所以当写请求并发执行时,它们在提交时是按顺序执行的。因此在前面所有的请求都持久存储到磁盘并得到响应之前,队列尾部的那些请求无法提交和响应,这会增加平均延迟并降低吞吐量。实验中观察到,随着 I/O 深度从 8 增加到 32,吞吐量下降了一半。

Raft 不太适合使用多条连接在 leader 和 follower 之间传输日志。当一个连接阻塞或变慢时,日志项将无序到达 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 和 follower,leader 将日志项复制到 follower。
我们遵循与 Raft 相同的问题分解方法,将 ParallelRaft 划分成更小的模块:日志复制、leader 选举和 catch up。

5.2 Out-of-Order Log Replication

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 按顺序提交日志条目,一条日志项只有在它之前所有的日志项提交后,才能提交

而在 ParallelRaft 中,可以在大多数副本确认后就立即提交日志项。对于不像 TP 系统那样承诺强一致性语义的存储系统而言,这种提交语义是可以接受的。例如,NVMe 不检查读取或写入命令的 LBA 以确保并发命令之间的任何类型的顺序,并且也不保证这些命令的完成顺序。

Apply with Holes in the Log.   Raft 中,所有日志项都严格按照 Raft 中记录日志的顺序被应用,因此数据文件在所有副本中都是一致的。 然而,通过乱序的日志复制和提交,ParallelRaft 允许日志中存在空洞。 在某些先前的日志项仍然缺失的情况下,如何安全地应用某条日志项? 这给 ParallelRaft 带来了挑战。

ParallelRaft 在每个日志条目中,引入了一个名为 look behind buffer 的新数据结构来解决这个问题。look behind buffer 包含被之前的 N 个日志项修改过的 LBA,look behind buffer 在日志中可能存在的空洞的情况下,扮演了沟通桥梁的角色。
N 是这座桥的跨度,同时也是允许的最大空洞的数量。请注意,尽管日志中可能存在多个空洞,但所有日志项的 LBA 统计始终是完整的,除非某些空洞的大小大于 N。
通过这个数据结构,follower 可以判断一个日志项是否有冲突,冲突意味着这个日志项修改的 LBAs 与一些它之前缺失的某些日志项之间有交集。
可以安全地应用与任何其他条目不冲突的日志项,否则应该将这些日志项添加到待处理列表中,并在之前缺失的日志项被应用后再进行处理。
根据我们使用 RDMA 网络的 PolarFS 的经验,N 设置为 2 足以满足其 I/O 并发性。

基于上述的 Out-of-Order 执行方法和规则,可以成功实现数据库想要的存储语义。 此外,还可以通过消除 ParallelRaft for PolarFS 中不必要的串行限制,来缩短多副本并发写入的延迟。

5.3 Leader Election

在进行新的 leader 选举时,ParallelRaft 和 Raft 一样,选择包含最新数据项、拥有的日志项最多的节点。
Raft 中新选举出的 leader 中包含之前所有已提交项的内容。但是,由于日志中可能存在空洞,ParallelRaft 中选出的 leader 开始可能无法到达这个要求。
因此,在开始处理请求之前,需要一个额外的合并阶段(merge stage)来使 leader 拥有之前已提交的所有条目。
在合并阶段完成之前,新选出的节点只是 leader 的候选,当合并阶段执行完成,新选出的节点拥有所有之前已提交的项,它变成真正的 leader 。
在合并阶段,leader  候选人需要合并来自共识组其他成员的,之前不可见的项。之后,leader 开始将之前任期内的数据提交到大多数,这与 Raft 相同。

在执行合并的时候,ParallelRaft 也使用了和 Raft 类似的机制。

具有相同 term 和 index 的条目将被视为相同的日志项,term 和 index 的概念参考 Raft 协议。

有几种异常情况:

(1) 对于一个已提交(提交指已被共识组中大多数节点确认)但不在 leader 中的项,leader 候选人总是可以从至少一个 follower 中找到,因为这个提交的项已被大多数接受。
(2) 对于没有在任何一个候选上提交的项,如果这个项也没有被任何一个候选保存,则 leader 可以安全地跳过,因为根据 ParallelRaft 或 Raft 机制,这个项不可能已经被提交。
(3) 如果某些候选人保存了一个未提交的项( index 相同但 term 不同),则 leader 候选人选择其中其中 term 版本最高的,并认为该项有效。
这样做有两个主要原因:
(3.a) ParallelRaft 的合并阶段必须在新的 leader 可以为用户请求服务之前完成,如果版本更高的 term 被设置到一个条目,那么具有相同 index 但 term 版本更低的项之前一定没有被提交,并且较低 term 的条目一定从未参与过之前成功完成的合并阶段,否则 term 更高的条目不可能具有相同的 index。
(3.b) 当系统崩溃时,如果候选人的保存了未提交项,该项的确认可能已经发送给之前一个 leader 并返回给了用户,所以我们不能简单地放弃它,否则用户数据可能会丢失。 (更准确地说,如果失败的节点的总数加上拥有此未提交条目(具有相同 index 的条目的 term 版本最高的)的节点总数超过同一共识组中剩余节点的数量,则此条目可能已由之前的 leader 提交。因此,为了用户数据安全,我们应该提交它。)


以一个 3 副本的场景为例展示选举过程,如图 5 所示。首先,follower 候选人将本地的日志项发送给 leader 候选人。leader 候选人将收到的日志项与它本地的日志项做合并。接着,leader 候选人向其他的 follower 候选人同步状态。之后, leader 候选人可以进行提交,通知 follower 候选人执行提交。最后,leader 候选人升级为 leader,follower 候选人成为 follower 。

通过上述机制,所有已提交的项可以被新的 leader 恢复出来,表明 ParallelRaft 不会丢失已提交状态。

在 ParallelRaft 中,会时不时地创建一个检查点,检查点之前的所有日志条目都已经应用于数据块,并且检查点允许包含一些在检查点之后提交的日志条目。 

为简单起见,可以将检查点之前的所有日志条目视为已修剪,尽管实际上它们会保留一段时间,直到资源不足或达到某个阈值。

在实际实现中,ParallelRaft 选择具有最新检查点的节点作为 leader 候选人,而不是具有最长日志的节点,这主要是考虑 Catch Up 的实现。 通过合并阶段,很容易证明这两种方式下新的 leader 在开始服务时会处于相同的状态。 因此,这个选择并没有损害 ParallelRaft 的正确性。

5.4 Catch Up

当一个滞后的 follower 想要跟上 leader 的当前数据状态时,它会使用 fast-catch-up 或 streaming-catch-up 来与 leader 进行同步。
具体使用哪一个取决于 follower 的状态有多旧。 fast-catch-up 机制是为 leader 和 follower 之间的增量同步而设计的,这时候它们之间的差异相对较小。
但 follower 可能远远落后于 leader。例如,follower 已经离线好几天了,所以一次完整的数据重新同步是不可避免的。因此,PolarFS 为该任务提出了一种名为 streaming-catch-up 的方法。


图 6 展示了同步开始时 leader 和 follower 的不同场景。 在 case 1 中,leader 的检查点比 follower 的最新日志还要新,它们之间的日志项将被 leader 裁剪。 因此,fast-catch-up 无法处理这种情况,而应使用 streaming-catch-up。 

而 case 2 和 case 3 中的情况可以通过 fast-catch-up 来解决。

在以下情况下,可以总是假设 leader 的检查点比 follower 的更新,因为如果不满足这个条件,leader 可以立即创建一个比任何其他节点都更新的检查点。检查点之后的日志项可以分为四类:committed-and-applied、committed-but-unapplied、uncommitted, and holes 。

Fast Catch Up.  follower 在它的检查点之后可能存在空洞,空洞通过 fast-catch-up 可以被填补。首先,follower 的检查点和 leader 检查点之间的空洞在 look behind buffer 的帮助下可以填补。通过 look behind buffer 可以发现所有缺失的修改操作的 LBAs。这些空洞直接通过从 leader 的数据块中复制来填充,其中包含比 follower 检查点更新的数据。 其次,leader 检查点之后的空洞是通过读 leader 的 log blocks 来填补。

case 3 中,follower 可能包含老的任期中未提交的日志项,和 Raft 一样,这些项被裁剪掉了,然后按照上面的步骤进行处理。

Streaming Catch Up. 在 streaming catch up 中,比检查点更老的日志被认为对于全数据同步是无用的,并且必须使用检查点之后的数据块和日志条目的内容来重建状态。 在复制 chunk 时,chunk 被分成 128 KB 的小块并使用很多小任务,每个小任务只同步一个 128 KB 的小块。 这样做是为了建立更可控的同步。 更详细的设计考虑在 7.3 节中讨论。

5.5  Correctness of ParallelRaft

Raft 为了协议的正确性需要包含以下属性:Election Safety, Leader Append-Only , Log Matching, Leader Completeness, and State Machine Safety.

很容易证明 ParallelRaft 有 Election Safety, Leader Append-Only , Log Matching 的属性,因为 ParallelRaft 在这些属性上没有做任何修改。

ParallelRaft 引入乱序提交是其与标准 Raft 的关键差异。ParallelRaft 日志中可能缺失一些必要的项。因此,要考虑的关键的场景是新选出的 leader 如何处理这些缺失项。
ParallelRaft 为 leader 选举增加了合并阶段。在合并阶段,leader 会复制缺失的日志条目,重新确认这些条目是否应该提交,如果需要则在共识组中提交。 5.3 节中在合并阶段之后,Leader Completeness 可以得到保证。

此外,如 5.2 节所提到的,数据库可以接受 PolarFS 中的乱序提交。

现在需要证明乱序应用日志不会违反 State Machine Safety 属性。虽然 ParallelRaft 允许节点独立执行日志的乱序应用,但是由于有 look behind buffer(5.2 节),有冲突的日志项只能严格按顺序执行,这意味着同一个共识组中的所有节点的状态机(数据以及已提交的日志项)是一致的。

综上,ParallelRaft 的正确性可以得到保证。

5.6 ParallelRaft  VS  Raft

我们做了一个简单的实验来说明 ParallelRaft  相比 Raft 有更好的并发性。图 7 展示了同样的任务 I/O 队列深度从 1 到 32 时 ParallelRaft 和 Raft 的平均时延和吞吐。两者的算法实现都使用了 RDMA。一开始时的细微性能差异可以忽略,因为它们是由不同软件包实现的。

随着 I/O 队列深度增加,两个协议之间的差距越来越大。当深度达到 32 时, Raft 协议的时延大概是 ParallelRaft 的 2.5 倍,IOPS 不到 ParallelRaft 的一半。Raft 协议的 IOPS 在队列深度超过 8 以后有明显的下降,而 ParallelRaft 仍然可以维持在一个较高的水平。

实验结果展示了 ParallelRaft 的优化是有效的,乱序的确认和提交提升了并发能力,这使得 PolarFS 即使在大压力下依然可以达到较高和稳定的性能。


6. File System Layer 实现

在 PolarFS 的 File System Layer,元数据管理可以分为两部分。第一部分是数据库节点内如何组织元数据,用于访问和更新数据库节点上的文件和目录。第二部分是数据库节点之间如何交互及同步修改过的元数据。

6.1 Metadata Organization

卷是块设备层的概念,为数据库实例提供服务前,需要在卷上格式化一个 PolarFS 文件系统(PFS)实例,PFS 会在卷上存放文件系统的元数据。

有三种文件系统的元数据:directory entry,  inode 及 block tag。directory entry 保存文件路径的名称,并具有对 inode 的引用。directory 的集合被组织成目录树。

inode 描述一个常规文件或一个目录。 对于常规文件,inode 保留对一组 block tag 的引用,每个 block tag 描述文件的块号到卷块号的映射关系;对于 directory,inode 在父目录中保存对子目录的引用。(上面提到的引用实际上是 metaobject identifiers,下面会给出解释)。

这三种元数据被抽象成一个称为 metaobject 的数据类型,它有一个字段用于保存 directory entry、inode 或 block tag 的特定数据。metaobject  作为一个通用的数据类型,用于磁盘和内存中的元数据访问。

创建一个新的文件系统时,metaobjects 在磁盘上被初始化为连续的 4K 大小的段,每一个 metaobject  被分配一个唯一的 identifier。在 pfs_mount 中,所有 metaobjects 都被加载到内存中,并划分到 chunks 和 type groups中。

metaobject 在内存中通过 tuple(metaobject identifier 及 metaobject type)进行访问。metaobject identifier 的高位用于查找 metaobject 所属的 chunk,metaobject type 在 chunk 内查找到 group,最后 metaobject identifier 的低位被用作在 group 内访问 metaobject 的索引。

如果要更新一个或多个 metaobject ,需要一个事务,每个更新是事务内的一条操作。事务内的一条操作持有 metaobject 的旧值和新值。当所有的更新都准备好了,事务可以提交了,提交操作需要在数据库的节点之间进行同步,这在下面会描述。如果提交失败,则通过事务中 metaobject 的旧值进行回滚操作。

6.2 Coordination and Synchronization

与传统文件系统不同,PolarFS 是一个共享文件系统,一个卷被多个计算节点挂载,同时可能出现多个客户端对文件系统的读写,PolarFS 通过 disk paxos 来保证修改元数据的安全。

为支持文件系统元数据的同步,PolarFS 以事务的方式记录元数据的修改到 journal 文件。当有新的事务时,journal file 被数据库节点轮询获得。一旦发现新的事务,这些事务被获取到,然后在节点上重放。

通常只有一个实例写 journal 文件,多个实例读 journal 文件。在出现网络分区的情况下,可能出现多个实例写 journal 文件。在这种情况下,需要有机制来协调对 journal 文件的多个写入。 PolarFS 通过 disk paxos 算法来处理这种场景,这里使用 disk paxos 算法的目的和 ParallelRaft 不同,后者是为了 chunk 副本集之间的数据一致性,而 disk paxos 是为了多个实例对 journal 文件的写互斥。每个客户端在写 journal 文件之前需要使用 paxos 文件执行 disk paxos 算法,从而实现了对 journal 文件的互斥访问。

disk paxos 算法在由 4K 大小的页面组成的文件上运行,这些页面可以原子地读取或写入。这些页面包括一个 leader record page 以及来自每个数据库节点的 data block pages。data block page 被数据库节点用来写自己的提议及读取其他人的提议。leader 的 record page 包含当前 paxos 获胜者的信息和 journal 的日志锚点。
disk paxos 算法仅在写节点上运行,只读节点不运行 disk paxos。只读节点轮询检查 leader 的 record page 中的日志锚点。如果日志锚点发生变化,只读节点知道 journal file 中有新的事务,它们获取这些事务来更新本地元数据。


通过一个例子来展示事务提交的过程,如图 8 所示,主要有以下步骤:

(1)Node1 在把卷的第 201 个 block 分配给 FileID 为 316 的文件后,通过 paxos 文件请求互斥锁, 并成功获得锁。

(2)Node1 开始在 journal 中记录事务信息,最新的写入位置标记为 pending tail。当所有写入都存储完成后,pending tail 成为 journal 真正的 tail。

(3)Node1 用修改过的元数据更新 super blocks,同时 Node2 尝试获得被 Node1 持有的互斥锁,Node2 会获取失败然后重试。

(4)Node2 在 Node1 释放锁后获取到互斥锁,但是 Node1 在 journal 新写入的项使得 Node2 本地缓存的元数据不是最新的。

(5)Node2 扫描这些新写入的项,然后释放锁。然后,Node2 回滚掉还没有被记录的事务,更新本地元数据。最后,Node2 进行事务重试。

(6)Node3 开始自动同步元数据,它只需要加载增量,然后在本地内存进行回放。


7. DESIGN CHOICES AND LESSONS

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

7.1 Centralized or Decentralized

分布式系统有两种设计范式:中心化和去中心化的方案。

中心化的系统,如 GFS 和 HDFS,包含一个 master,负责保存元数据和集群成员管理,这种系统实现起来相对简单,但单个 master 可能成为整个系统在可用性和可扩展性上的瓶颈。
像 Dynamo 这样的去中心化系统则相反。在这个系统中,节点是点对点的关系,元数据在所有节点之间是分片和冗余的。去中心化系统理论上更可靠,但实现和逻辑也更复杂。
PolarFS 在中心化和去中心化的方案之间进行了权衡。一方面,PolarCtrl 是一个中心化的 master,负责管理资源等管理任务,并接受创建卷等控制平面的请求。另一方面,ChunkServer 之间彼此通信,运行共识协议来自主处理故障和恢复,而无需 PolarCtrl 参与。

7.2 Snapshot from Bottom to Top

对数据库而言 snapshot 是一个常见的需求,PolarFS 本身支持 snapshot 特性,这简化了上层 POLARDB snapshot 的设计。

在 PolarFS 中, storage layer 为上层提供可靠的数据访问服务,POLARDB 和 libpfs 有他们自己的日志机制来保证事务的原子性和持久性。

PolarFS  storage layer 提供磁盘中断一致性(disk outage consistency)快照,POLARDB 和 libpfs 从底层 PolarFS 快照重建自己的一致性数据镜像。

这里 disk outage consistency 的意思是,如果一个快照命令在时间点 T  触发,存在一个时间点 T0  ,所有 T0 之前的 I/O 操作都包含在当前的快照中。而 T 之后的 I/O 操作不包含在快照中。

但是在时间区间 [T0, T] 内的 I/O 操作的状态可能是不确定的。这和磁盘在执行写入时突然断电的情况有些类似。

当发生意外事故或需要主动审计时,PolarCtrl 将最新的数据快照给 POLARDB 实例,POLARDB 和 libpfs 将使用它们存储的日志来重建到一致状态。

PolarFS 以一种全新的方式实现了磁盘中断一致性快照,在快照过程中不会阻塞用户 I/O 操作。

当用户发出快照命令时,PolarCtrl 通知 PolarSwitch 执行快照。此后,PolarSwitch 会在后面的请求中添加一个快照标签,表示它的主机请求发生在快照请求之后。
收到带有快照标签的请求时,ChunkServer 会在处理请求之前先制作快照。ChunkServer 通过拷贝块映射的元数据信息来制作快照,速度很快,然后以写时复制的方式处理那些即将到来的修改请求。带有快照标签的请求完成后,PolarSwitch 对新的请求将不会添加快照标签。

7.3 Outside Service vs. Inside Reliability

可靠性对一个生产系统而言至关重要,尤其是像 PolarFS 这样承载 7 X 24 公有云服务的系统。在这样的系统中,应该有各种可靠性维护任务,以防止服务及相关的损失。 PolarFS 在为繁重的用户请求提供流畅的服务的同时,如何高效地运行这些可靠性任务是一个很大的挑战。

举个实际的例子,ParallelRaft 中的 streaming catch up,当工作负载很高时,leader 会不断产生大量的日志,即使经过长时间的日志获取,follower 也赶不上。由于 I/O 的限制,复制整个块的时间也可能相当长且不可预测。

这需要做一个权衡:恢复时间越短,占用的资源越多,对系统性能的牺牲就越大; 而如果需要很长时间才能恢复,系统可用性就会有风险。

我们的解决方案是将一个 chunk 水平分割成小的逻辑块,比如 128 KB 的块。一个 chunk 的完整数据同步也被分成许多小的子任务,每个子任务只同步一个 128 KB 的块。
这些较小的子任务的运行时间更短且更可预测。此外,可以在子同步任务之间插入空闲周期,以控制 streaming catch up 的网络/磁盘带宽开销。

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

8. Evaluation

我们实现了 PolarFS,也在阿里云上发布了 POLARDB 数据库服务。这一节评估和分析 PolarFS 和 POLARDB 的性能和扩展性。PolarFS 通过集群的方式与 基于本地 NVMe 的 Ext4 以及 基于 Ceph 的 CephFS 进行对比。 而 POLARDB 则与我们原生的 MySQL 云服务 以及基于本地 Ext4 的 POLARDB 进行对比。

8.1PolarFS Experimental Setup

对于文件系统的性能,主要集中在三个系统(PolarFS,CephFS 及 Ext4)的对比,包括在不同负载和访问模式下的实验和吞吐。

PolarFS 和 CephFS 的实验数据来自一个有 6 个存储节点和 1 个客户端节点的集群,节点之间通过 RDMA 通信。

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

使用 FIO 产生很多不同类型,I/O 大小和并发也不一样的负载。 在测试性能之前,FIO 会首先创建文件,然后将这些文件填充到预设的大小(这里是 10G)。

8.2 I/O Latency


如图 9 所示,对于 4k 的随机写,PolarFS 的时延大约 48µs,非常接近基于本地 SSD 的 Ext4(大约 10µs),而 CephFS 的时延大约为 760µs 。 PolarFS 随机写的平均时延是本地 Ext4 的 1.6-4.7 倍,而 CephFS 是本地 Ext4 的 6.5-75 倍,这意味着分布式 PolarFS 已经几乎可以提供和本地 Ext4相同的性能。顺序写 PolarFS 时延大约是本地 Ext4 的 1.6-4.8 倍,而 CephFS 是 6.3-56 倍。随机读 PolarFS 时延大约是本地 Ext4 的 1.3-1.8 倍,而 CephFS 是 2-4 倍。顺序读 PolarFS 时延大约是本地 Ext4 的 1.5-8.8 倍,而 CephFS 是 3.4-14.9 倍。

PolarFS/CephFS 大请求(1M)的性能性能下降与小请求(4K)不同,因为对于大请求而言,网络和磁盘的数据交换占用了大部分执行时间。

PolarFS 的性能比 CephFS 要好很多,有几个主要原因。首先,如第四节所述 PolarFS 通过单线程运行有限状态机来处理 I/O,避免了像 CephFS 中多个线程组成 I/O 管道这种方式带来的线程上下文切换和调度。其次,PolarFS 优化了内存分配和分页,使用内存池来管理 I/O 生命周期中的内存分配和释放,减少对象的构造和析构操作,通过使用 huge page 来减少 TLB 未命中和分页,而 CephFS 没有对应的处理逻辑。第三,PolarFS 中所有数据对象的元数据都在内存中,而 CephFS 中每个 PG(Placement Group) 只有部分元数据存储在一小块缓存中,因此 PolarFS 对于大多数 I/O 不需要额外的访问元数据的 I/O 操作。最后但也很重要的一点是,PolarFS 中的用户空间 RDMA 和 SPDK 比 CephFS 中的内核态的 TCP/IP 和块驱动程序具有更低的延迟。

8.3 I/O Throughput

图10 展示了三种文件系统 I/O 吞吐的对比。对于随机读/写,对于本地系统或者分布式系统几乎所有的请求都由一块磁盘处理。Ext4 和 PolarFS 通过增加客户端线程数,能获得较好的扩展性,直到由于 I/O 瓶颈达到限制。而 CephFS 的瓶颈是它自身的软件,在 I/O 带宽未饱和的情况下它已经到达瓶颈。


由于没有额外的网络 I/O,单线程情况下 Ext4 的顺序读吞吐比 PolarFS 和 CephFS 要高得多。但当客户端数量增加到 2 时,顺序读的吞吐有明显的下降,此时的吞吐和 2 客户端场景下的随机读很接近。对此,我们重复了多次实验,得到的结果都比较接近。对此,我们猜测是由于我们使用的 NVMe SSD 的属性导致的。 NVMe SSD 有一个内置的 DRAM buffer,当固件猜测工作负载看起来像顺序读时,它会将后续数据块预取到 DRAM buffer 中。 我们猜测预取机制在非交叉 I/O 模式下会工作得更好。

8.4 Evaluating POLARDB

为了展示 PolarFS 低 I/O 延迟给数据库带来的收益,在此展示 POLARDB 几组的测试数据。我们对比了三个系统的吞吐:(1)POLARDB on PolarFS,(2)POLARDB on local Ext4,(3)阿里云 MySQL 服务。其中测试 (1)和 (2)是在和之前 PolarFS 测试相同的硬件环境进行。

分别在三个数据库上运行 sysbench,模拟 read-only,write-only( update : delete : insert = 2 : 1 : 1) 以及 read/write 混合(read : write = 7 : 2)这三种 OLTP 场景。实验中使用的数据集包含 250 张表,每张表有 850W 条记录。测试数据集的总大小为 500G。


如图 11 所示,POLARDB on PolarFS 吞吐与 PolarDB on local Ext4 非常接近,但同时  PolarFS 可以提供 3 副本的高可用性。POLARDB on PolarFS 可以达到 653K read/sec ,160K write/sec 以及 173K read-write/sec 。

另外,为了对接 PolarFS ,POLARDB 本身基于阿里云的 RDS 也做了很多的优化。最终,如图中所示,POLARDB 在 read/write/read-write 三种场景下能够达到 RDS 的 2.79/4.72/1.53 倍的吞吐。数据库相关的优化不在本文介绍了。


9. RELATED WORK

Storage System. GFS 及其开源实现 HDFS 提供了分布式系统服务,GFS 和 HDFS 都采用了主从架构,主节点维护系统元数据信息,data chunk 的 leader ,以及负责故障恢复。和 GFS 及 HDFS 不同的是,PolarFS 采用混合架构。主节点主要负责资源管理,一般不会对 I/O 路径产生影响,这使主节点的升级和容错更容易。

Ceph 是被广泛使用的存储系统,它使用 CRUSH  hash 算法来定位数据对象,这可以简化 正常的 I/O 执行以及容错。RAMCloud 是一个低时延的 KV 存储系统,它通过以随机方式在集群中复制数据,并利用集群资源并行恢复故障来提供快速恢复的能力。随机放置数据会使一台机器在云环境中承载数千个用户的数据,这意味着单台机器崩溃将影响数千个用户,这对于云服务提供商来说是不可接受的。

在将数据 chunk 分配给真实服务器时,PolarFS control master 节点在故障影响和 I/O 性能之间提供了细粒度的权衡。

ReFlex 使用 NVMe SSD 来实现 remote disk,它提出了一个 QoS 调度器,可以控制长尾延迟和吞吐,达到服务级别目标。但与 PolarFS 不同的是,它的 remote disk 是单副本,没有可靠性支持。

CORFU 将闪存设备集群组织为单个共享日志,多个客户端可以通过网络同时访问该日志。共享日志设计简化了分布式环境下的快照和复制,但增加了现有系统使用 CORFU 的工程难度,而 PolarFS 为用户提供了块磁盘抽象和类似 POSIX 的文件系统接口。

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

Consensus Protocols.  Paxos 是最著名的分布式系统共识算法之一,但是它很难理解和正确实现。Paxos 的作者只证明了 Paxos 可以解决一个实例的共识问题,并没有展示如何使用“MultiPaxos”来解决实际问题。目前,大部分共识算法,例如 zookeeper 使用的 Zab,都被认为是“Multi-Paxos”算法的变种。Zab 用于提供原子广播原语属性,但也很难理解。Raft 是为可理解性而设计的,这也是种“Multi-Paxos”算法。它引入了两个主要约束,第一个是提交日志必须是顺序的,第二个是提交顺序必须是序列化的。这两个约束使 Raft 易于理解,但也会对并发 I/O 性能产生影响。有的系统通过将 key 拆分成多个 group 来受用多组 Raft 提高 I/O 并行度,但是不能解决 commit log 中 key 跨越多个 group 的问题。ParallelRaft 通过一个不同方式实现了这个目标, commit log 允许有空洞,日志项如果没有冲突可以乱序提交,有冲突的日志项按顺序提交。

RDMA Systems.  与传统的 TCP/UDP 网络协议相比,RDMA 在 CPU 使用率更低的情况下将往返延迟降低了一个数量级,很多新系统都使用 RDMA 来提高性能。FaRM 是一个基于 RDMA 的分布式共享内存,用户可以使用它的程序原语来构建他们的应用程序。DrTM 是一个基于 RDMA 和硬件事务内存的分布式内存数据库,RDMA 可以快速访问远程端的数据,也可以触发硬件事务内存中的本地事务中止,DrTM 结合 RDMA 和硬件事务内存来实现分布式事务。FaRM 和 DrTM 都专注于内存事务,但 PolarFS 使用 RDMA 构建分布式存储。

Pilaf 是基于 RDMA 和自验证数据结构的 KV 存储,但与 PolarFS 不同的是,他的 KV 接口比较简单,而且 Pilaf 中没有分布式共识协议。

APUS 使用 RDMA 构建可扩展的 Paxos,而 PolarFS 使用对并发 I/O 更友好的 ParallelRaft 。PolarFS 使用 RDMA 构建可靠的块设备,因为 RDMA 比传统的网络协议消耗更少的 CPU,PolarFS 以一种运行到完成(run-to-completion)的方式处理 I/O,避免了上下文的切换。通过结合 RDMA 和 SSPDK,存储节点可以在整个 I/O 生命周期中不接触有效载荷的情况下完成 I/O ,所有有效载荷的移动都是由硬件完成的。


10. Conclusion

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


11. ACKNOWLEDGEMENTS

感谢对本文有帮助的所有人...


个人理解

从整体架构上 PolarFS 和常见的一些分布式系统并没有大的差异。PolarCtrl 是控制节点,PolarSwitch 负责路由,ChunkServer 是数据节点。PolarFS 主要的优化点应该是使用了新型的硬件和一些新的技术,如 NVMe SSD、RDMA,SPDK 及 优化了的 Raft 协议 ParallelRaft,总之尽可能的使用各种方法降低时延,如 用户态的网络和 I/O 栈,使用 RDMA 而不是传统网络协议,ParallelRaft 加快复制,最终使得整个系统的时延控制得非常出色。


参考文档:https://www.vldb.org/pvldb/vol11/p1849-cao.pdf

最后修改时间:2021-11-22 10:44:31
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论

王维
关注
暂无图片
获得了157次点赞
暂无图片
内容获得30次评论
暂无图片
获得了18次收藏
目录
  • 摘要
  • 1. 介绍
  • 2. 背景
  • 3. 架构
    • 3.1 File System Layer
      • 3.1.1 libpfs
    • 3.2 Storage Layer
      • 3.2.2 ChunkServer
      • 3.2.3 PolarCtrl
  • 4. I/O 执行模型
  • 5. 一致性模型
    • 5.1 A Revision of Raft
    • 5.2 Out-of-Order Log Replication
    • 5.6 ParallelRaft  VS  Raft
  • 6. File System Layer 实现
    • 6.1 Metadata Organization
    • 6.2 Coordination and Synchronization
  • 7. DESIGN CHOICES AND LESSONS
    • 7.1 Centralized or Decentralized
    • 7.3 Outside Service vs. Inside Reliability
  • 8. Evaluation
    • 8.1PolarFS Experimental Setup
    • 8.2 I/O Latency
  • 9. RELATED WORK
  • 11. ACKNOWLEDGEMENTS
  • 个人理解