Octopus: an RDMA-enabled Distributed Persistent Memory File System

概述

Octopus 通过 NVM + RDMA 实现了分布式文件系统,主要贡献总结如下:

  • 提出了基于 RDMA 的新型 I/O 流,它直接访问持久化共享内存池,而无需经过文件系统层层调用,并在客户端主动获取或发送数据以重新平衡服务器和网络负载。
  • 利用 RDMA 原语重新设计元数据机制,包括自标识元数据 RPC 来用于低延迟通知,以及收集-调度分布式事务实现低一致性开销。
  • 有效地利用了硬件性能,显著优于现有 RDMA 优化的分布式文件系统。

挑战

  1. 之前由于存储介质较慢,访问持久化存储介质的开销几乎占据了文件系统操作的全部,软件栈的优化对整体性能的影响微乎其微,所以之前的存储层与网络层往往采用松耦合的设计模式来使得系统更容易理解与实现。而对于 NVM 上的文件系统来说,由于 NVM 的访问时延接近内存,使得软件栈的开销几乎占据了文件系统操作的全部,现在优化软件栈开销成为优化系统最重要的手段。
  2. 现有的文件系统利用新硬件高带宽的效率低下。这主要有四个原因:(1)数据在应用缓冲区、文件系统页缓存、网卡缓冲等区域之间来回拷贝,增加了软件开销;(2)服务器每秒需要处理的大量请求,server 端 CPU 成为瓶颈;(3)基于事件驱动模型的传统 RPC 具有相对较高的延迟;(4)分布式文件系统中使用到的分布式事务等需要多次的网络来回,处理逻辑较为复杂,使得分布式文件系统用在一致性上的开销较大。

设计

总体架构

Octopus 文件系统数据分散在集群节点中,一个文件根据路径名使用一致性哈希保存在一个节点中,文件没有做冗余,RDMA 与存储层紧耦合设计。
每个节点的 NVM 分为私有部分和共享部分。私有部分保存该节点文件的元数据,只允许本节点内访问,客户端通过 RPC 访问;共享部分保存该节点中文件的数据,客户端可以通过 RDMA 单边原语直接读写。Octopus 使用 RDMA write-with-imm 进行 RPC。

数据布局

Octopus 每个节点的 NVM 划分为 6 个区域,每个区域是共享的或私有的。这六个区域的内容为:

  • Super Block:用于存储文件系统的超级块
  • Message Pool:元数据 RPC 的通信缓冲区
  • Metadata Index Zone:使用哈希表保存文件索引
  • Metadata Zone:具体的文件元数据保存区域
  • Data Zone:文件数据的保存区域
  • Log Zone:事务日志区域

High-Throughput Data I/O

Octopus 引入了共享持久化内存池来减少数据拷贝以获得更高的带宽,并且在客户端主动执行 I/O 来重新平衡服务器和网络开销以获得更高的吞吐量。

Shared Persistent Memory Pool

如图所示,GlusterFS 里同一个数据在传输过程中拷贝了 7 次,Octopus 利用共享持久化内存池取消层次抽象,以及通过 RDMA 取消缓存,将数据拷贝降低到了 4 次。

Client-Active Data I/O

Octopus 提出了 Client-Active Data I/O 数据访问模式,充分发挥了 RDMA 的优势,降低了服务端 CPU 的负载。

传统的数据交互为 Server-Active Data I/O 方式,如图中的(a)所示,客户端给服务端发送数据访问请求,服务端查找到数据的位置后读取对应的数据,并把最终的数据返回给客户端。而 Client-Active Data I/O 方式则与此不同,如图中的(b)所示,客户端使用自标识元数据 RPC 发送读写请求和访问元数据,然后根据元数据信息使用 RDMA Read/Write 直接读写文件数据。
Client-Active 与 Server-Active 相比,服务端 CPU 执行一个请求的操作较少,将读数据操作转移给了客户端进行,从而降低了服务端 CPU 的负载。

Low-Latency Metadata Access

RDMA 为远程数据访问提供微秒级访问延迟。为了在文件系统发挥这一优势,Octopus 通过合并 RDMA 写入和原子原语重构了元数据 RPC 和分布式事务。

Self-identified metadata RPC

RDMA 具有低延迟高带宽的优势,在 RPC 中使用 RDMA 能够提高吞吐量。以往的 RDMA RPC 大多使用双边 RDMA 原语,而双边 RDMA 具有相对较高的延迟和较低的吞吐量,减小了 RDMA 的优势。而单边 RDMA 原语不会在完成时通知 CPU,若使用单边 RDMA 进行 RPC,服务器需要有单独的线程轮询消息缓冲区,使 CPU 的负载进一步增大。
为了保持 RDMA 低延迟的优势并减少 CPU 的负载,Octopus 提出了 Self-identified metadata RPC(自标识元数据 RPC)。自标识元数据 RPC 使用 RDMA write_with_imm 命令将发送者的标识信息附加到 RDMA 请求中。write_with_imm 与传统的 RDMA Write 相比有以下两点不同:(1) 它能够在消息中携带一个立即数;(2)它能够在服务器网卡接收到该请求后立即通知服务器 CPU,这会消耗服务器的 QP 中的一个 Recv WR。因此,使用 write_with_imm 能够让服务器及时收到 RPC 请求,且无需 CPU 进行轮询。立即数字段中附加有客户端标识符 node_id 和客户端接收缓冲区的 offset。node_id 可帮助服务器定位对应消息而无需扫描整个缓冲区。请求处理完成之后,服务器使用 RDMA Write 原语将数据返回到标识符为 node_id 的客户端中偏移量地址 offset 处。

Collect-Dispatch Transaction

在文件系统中,某些操作可能会使用分布式事务进行,如 mkdir,mknod,rmnod 和 rmdir 等等。这些操作需要在多个服务器之间原子性地更新元数据。在之前的分布式文件系统中,往往使用两阶段提交(2PC)完成事务操作。然而两阶段提交由于其分布式的日志以及对锁和日志的协调而导致高昂的开销。
Octopus 设计了一个新的分布式事务协议:Collect-Dispatch Transaction,该协议分为收集阶段(Collect Phase)和分发阶段(Dispatch Phase)。该事务协议利用了 RDMA 原语进行服务器间的交互,关键思想在于两个方面,分别是崩溃一致性和并发控制:

  1. 带有远程更新的本地日志来实现崩溃一致性。在收集阶段,Octopus 从参与者收集读写集合,并在协调者中执行本地事务,记录日志。由于参与者不需要保留日志记录,因此无需为协调者和参与者之间的持久化日志进行复杂的协商,从而减少了协议的开销。在分发阶段,协调者使用 RDMA write 将更新的写集合分发给参与者,并使用 RDMA atomic 原语释放相应的锁,而不涉及到参与者 CPU。
  2. 混合使用 GCC 和 RDMA 锁来实现并发控制。在 Collect-Dispatch 事务中,协调者和参与者使用 GCC 的 Compare-and-Swap 命令在本地添加锁。解锁时,协调者使用 GCC 的 Compare-and-Swap 命令释放本地锁,并使用 RDMA 的 Compare-and-Swap 命令释放远端每个参与者的锁,解锁操作不涉及到参与者的 CPU,因此优化了解锁阶段

总的来说,Collect-Dispatch Transaction 需要一次 RPC(COLLECT-REQ 和 WRITE-SET)、一次 RDMA Write(UPDATE WRITESET)和一次 RDMA Atomic(REMOTE UNLOCK),而两阶段提交需要两次 RPC。Collect-Dispatch Transaction 与 2PC 相比具有较低的开销,这是因为:(1) 一次 RPC 比一次 RDMA Write/Atomic 原语具有更高的延迟;(2) RDMA Write/Atomic 原语不需要服务端 CPU 的介入。

总结

Octopus 其核心思想是 RDMA 与 NVM 紧耦合设计,设计了一系列机制来实现高吞吐量的数据 I/O 以及低延迟的元数据访问。但在分布式方面该文件系统没有做冗余,也没有考虑负载均衡等内容,只是通过一致性哈希将数据分散到不同节点上。

参考

  1. Lu, Youyou, et al. “Octopus: An RDMA-Enabled Distributed Persistent Memory File System.” USENIX ATC ’17 Proceedings of the 2017 USENIX Conference on Usenix Annual Technical Conference, 2017, pp. 773–785.

Google File System

背景

GFS 是 Google 针对大数据处理场景设计的分布式文件系统,Google 对数据量的持续增长的设想如下:

  • 需要能够运行在经常故障的物理机环境上。只有做到这点,这个系统才能运行在几百到上千台规模下,并采用相对便宜的服务器硬件
  • 大文件居多。存储在 GFS 上的文件的不少都在几个 GB 这样的级别
  • 大多数写是append写,即在文件末尾追加
  • 性能主要考量是吞吐而不是延时

接口

GFS 以目录树的形式组织文件,但是并没有提供类似 POSIX 标准的文件系统操作。操作只要包含 create, open, write, read, close, delete, append, snapshot,其中 snapshot 用于快速复制一个文件或者目录,允许多个客户端并行追加数据到一个文件。

架构

整体架构上,GFS 由单一的 master 节点、chunkserver 和提供给用户的 client 三大部分组成:

client 与 chunkserver 都不会缓存文件数据,为的是防止数据出现不一致的状况,但是 client 会缓存 metadata 的信息。

master

一方面存储所有的 metadata,负责管理所有的元信息,包括表示文件系统目录结构的 namespace、访问控制信息、文件与 chunk 的映射关系、chunk 的位置信息;另一方面管理 chunk 租约、chunk 迁移(如果 chunkserver 挂掉)、维护与 chunkserver 之间的心跳。

  • namespace 采用全内存的数据结构,以提高访问的吞吐
  • namespace 是一个查找表(lookup table),并且采用了前缀压缩的方式存储在内存中,它是一个树结构,树中每一个节点(文件名或目录名)都有一个读/写锁。在对文件或目录操作的时候需要获取锁,例如修改 /root/foo/bar,需要获得 /root/root/foo 的读锁,/root/foo/bar 的写锁
  • master 本身并不记录 chunk 的位置,而是在启动的时候,通过收取 chunkserver 的信息来构建。这种设计避免了 master 和 chunkserver 的信息不一致的问题(因为以 chunkserver 为准)
  • master 和 chunkserver 通过定期心跳来保持信息同步、感知 chunkserver 故障等
  • master 往磁盘上写操作日志,并将这些日志同步到其他物理机保存的方式来确保数据安全性。当前 master 机器故障的时候,可以通过这些日志和 chunkserver 的心跳内容,可以恢复到故障前的状态
  • 对于操作日志,会定期在系统后台执行 checkpoint;checkpoint 构建成一个类似B+树、可以快速的 load 到内存中直接使用的结构
  • master 需要定期检查每个 chunk 的副本情况,如果副本低于配置值,就需要将通知 chunkserver 进行复制;如果存在一些多余的 chunk (file 已经被删除了),就需要做一些清理工作

chunkserver

每个 chunk 有一个 64 位标识符(chunk handle),它是在 chunk 被创建时由 master 分配的,每一个 chunk 会有多个副本,分别在不同的机器上,每个副本会以 Linux 文件的形式存储在 chunkserver 的本地磁盘上。
GFS 中将 chunk 的大小定为 64 MB,它比一般的文件系统的块大小要大。优点:减少 metadata 的数量、减少 client 与 master 的交互、client 可以在一个 chunk 上执行更多的操作,通过 TCP 长连接减少网络压力;缺点:如果在一个 chunk 上有一个可执行文件,同时有许多 client 都要请求执行这个文件,它的压力会很大。
chunk 的位置信息在 master 中不是一成不变的,master 会通过定期的 heartbeat 进行更新,这样做能够减小开销,这样做就不用 master 与 chunkserver 时刻保持同步通信(包括 chunkserver 的加入、退出、改名、宕机、重启等)。chunkserver 上有一个 final word,它表示了哪个 chunk 在它的磁盘上,哪个 chunk 不在。

一致性模型

defined:状态已定义,从客户端角度来看,客户端完全了解已写入集群的数据
consistent:客户端来看chunk多副本的数据完全一致,但不一定defined

  • 串行写:客户端自己知道写入文件范围以及写入数据内容,且本次写入在数据服务器的多副本上均执行成功,每个客户端的写操作串行执行,因此最终结果是 defined
  • 并行写:每次写入在数据服务器的多副本上均执行成功,所以结果是 consistent,但客户端无法得知写操作的执行顺序,即使每次操作都成功,客户端无法确定在并发写入时交叉部分,所以是 undefined
  • 追加写:客户端能够根据 offset 确切知道写入结果,无论是串行追加还是并发追加,其行为是 defined,追加时至少保证一次副本写成功,如果存在追加失败,则多个副本之间某个范围的数据可能不一致,因此是 interspersed with inconsistent

GFS 租约

GFS 使用租约机制 (lease) 来保障 mutation (指的是改变了 chunk 的内容或者 metadata,每一次 mutation 都应该作用于所有的备份) 的一致性:多个备份中的一个持有 lease,这个备份被称为 primary replica (其余的备份为 secondary replicas),GFS 会把所有的 mutation 都序列化(串行化),让 primary 执行,secondary 也按相同顺序执行,primary 是由 master 选出来的,一个 lease 通常 60 秒会超时。

写流程

  1. client 向 master 请求持有 lease 的 chunk (primary replica) 位置和其他 replicas 的位置(如果没有 chunk 持有 lease,那么 master 会授予其中一个 replica 一个 lease)
  2. master 返回 primary 的信息和其他 replicas 的位置,然后 client 将这些信息缓存起来(只有当 primary 无法通信或者该 primary replica 没有 lease 了,client 才会向 master 再次请求)
  3. client 会将数据发送到所有的 replicas,每个 chunkserver 会把数据存在 LRU 缓存中
  4. 在所有的 replicas 都收到了数据之后,client 会向 primary 发送写请求。primary 会给它所收到的所有 mutation 分配序列号(这些 mutation 有可能不是来自于同一个 client),它会在自己的机器上按序列号进行操作
  5. primary 给 secondaries 发送写请求,secondaries 会按相同的序列执行操作
  6. secondaries 告知 primary 操作执行完毕
  7. primary 向 client 应答,期间的错误也会发送给 client,client 错误处理程序 (error handler) 会重试失败的 mutation

读流程

  1. client 向 master 发出 A 文件的读请求

  2. master 收到后返回 A 文件的 chunk handler 和 chunk 的位置

  3. client 携带 chunk handle 以及位偏移向对应的 chunkserver 发出请求

  4. chunkserver 读取并返回数据至 client

参考

  1. Ghemawat, Sanjay, Howard Gobioff, and Shun-Tak Leung. “The Google file system.” (2003).