课程主页: https://pdos.csail.mit.edu/6.824/schedule.html
本节课是解释谷歌的分布式文件系统GFS, 强烈建议阅读其论文
1 GFS简介
GFS(Google File System)是Google特别为应对大规模数据处理而设计的分布式文件系, 其设计目的是满足对处理大量数据集、高吞吐量的数据访问、可靠性、可扩展性的需求。其特性包括:
分布式架构:
GFS是一个分布式的文件系统,它将数据存储在多个网络连接的机器上。这种设计可以提供高容错性和高数据可靠性。容错能力:
GFS通过副本机制来确保数据的安全。即使某些硬件组件或机器出现故障,系统仍能确保数据不丢失,并继续提供服务。一致性模型:
GFS采用了一致性模型,以容忍网络延迟和系统故障,同时保持数据的一致性。大文件优化:
GFS被设计来存储大文件。它将每个文件分割成多个固定大小的块(chunks),每个块一般大小为64MB或更大。这种设计简化了文件管理,并且对于大规模数据处理非常有效。高吞吐量:
GFS针对高吞吐量的数据访问进行了优化,尤其是顺序读写操作,这对于数据挖掘和内容索引等操作非常重要。主/从架构:
GFS采用了主/从架构,有一个主服务器(Master)负责管理文件系统的命名空间、控制元数据和调度访问请求。数据被存储在多个分布式的块服务器(Chunkserver)上。写入模型:
GFS的写入模型设计用于最小化由于远程写入导致的网络拥堵。它采用了写入一次,多次读取的模式,这意味着一旦写入,文件就不再修改,只允许追加操作。客户端和服务器交互:
GFS客户端和服务器之间的交互尽可能地简单,并且大部分复杂的操作逻辑都在服务器端完成。
GFS的设计论文被广泛研究,并激发了其他分布式文件系统的开发,如Apache Hadoop的HDFS(Hadoop Distributed File System)等。
2 GFS架构
2.1 架构图
下图所示为摘自论文中的GFS架构:

架构图中包括如下术语:
MasterGFS中的中心节点,负责管理文件系统的全局视图。Master节点保存了所有文件的元数据,这包括文件和目录的命名空间结构、每个文件的chunks信息、每个chunk的位置以及每个操作的日志。Master调度客户端对chunks的读写请求,并处理命名空间的修改和访问控制。ChunkserverChunkserver是GFS中的工作节点,负责存储实际的文件数据。文件数据被分割成多个chunks,每个chunk通常大小为64MB(可进行配置)。每个Chunkserver可能会存储数千个这样的chunks,并负责处理来自客户端的读写请求。它们向Master报告存储的chunks的状态,并根据Master的指示执行诸如创建副本或删除旧副本等操作。本质上,Chunkserver就是一个Linux的管理文件的进程。ClientClient指的是使用GFS存储数据的应用程序。客户端通过GFS提供的API(其实就是链接一个库)与文件系统进行交互。客户端会先与Master节点通信以获取文件元数据和chunks的位置信息,然后直接与一个或多个chunkservers通信进行数据的读写操作。Chunk
文件分割的基本单位,是数据存储的块。每个chunk由Chunkserver存储和管理,具有固定的大小,例如64MB。通过使用固定大小的chunks,GFS能够简化存储管理,并且优化大规模数据的读写效率。文件有多少个Chunk, 每个Chunk在什么位置, 这些信息都存在Master的内存中。Chunk HandleChunk Handle是GFS分配给每个chunk的唯一标识符。它是一个全局唯一的64位值,用于标识和引用存储在chunkservers上的特定chunk。Chunk Handle在chunk的整个生命周期中保持不变,即使其副本在不同的chunkservers之间移动或复制。Chunk LocationChunk Location是指存储特定chunk的Chunkserver的网络地址。Master节点跟踪每个chunk的所有副本的位置,并将这些位置信息提供给客户端,以便客户端可以直接与存储所需chunks的Chunkserver建立连接进行数据读写。
2.2 架构特点
GFS的架构特点可以总结如下:
- 单
Master
所有文件系统元数据(包括文件系统命名空间、文件名与chunk的映射关系、chunk的位置、chunk版本号、日志等)都存储在单个Master节点的内存中 - 文件分块
件被切分成固定大小的chunks,chunkservers将chunks存储在本地磁盘上作为Linux文件,chunks在多个chunkservers中备份存储 - 心跳同步
chunkservers与master通过心跳函数进行信息交互 - 数据和指令分开传送
client与Master节点交互来进行元数据操作,比如打开文件、获取文件信息、获取chunk位置等。之后,client直接与Chunkserver节点交互来进行实际的数据读写 - 租约(
Leases)机制
(此机制未在架构图中体现)Master节点给某个chunk的一个副本授予租约,该副本被称为primary chunk。持有租约的Chunkserver有责任在写操作中协调其他副本(secondary chunks)的更新顺序。
3 GFS如何保证一致性?
GFS采用了一种宽松的一致性模型来在分布式的环境中保持系统性能和可用性。
3.1 原子性命名空间操作
- 文件命名空间的变更(例如,创建文件)是原子性操作,由
master服务器独家处理。 - 命名空间锁定和操作日志(记录操作的全局总顺序)保证了这些变更的原子性和正确性。
3.2 数据变更后的文件区域状态
- 在数据变更后,文件区域的状态取决于变更的类型、变更是否成功,以及是否存在并发变更。
- 如果所有客户端无论读取哪个副本都看到相同的数据,那么这个文件区域就是一致的。
- 如果数据变更后文件区域是一致的,并且客户端可以完整地看到变更写入的内容,那么这个区域就是已定义的。
- 当变更在没有并发写入者的干扰下成功时,受影响的区域是已定义的(也就是一致的)。
- 并发的成功变更会使区域变得未定义但仍然一致:所有客户端看到的是相同的数据,但可能不反映任何单一变更所写入的内容。通常包含来自多个变更的混合片段。
- 失败的变更会使区域变得不一致(因此也是未定义的):不同的客户端可能在不同时间看到不同的数据。
3.3 数据变更:写操作和记录追加
- 写操作在应用程序指定的偏移量处执行。
- 记录追加是一种原子操作,即使存在并发变更,也会在GFS选择的偏移量处至少追加一次数据。
GFS可能在中间插入填充或重复的记录,这些被视为不一致。- 在成功的变更之后,变更的文件区域是已定义的,并包含最后一次变更的数据。
3.4 确保一致性的手段
- 在序列成功的变更后,变更的文件区域保证是已定义的,并且包含最后一次变更写入的数据。
GFS通过在所有副本上以相同的顺序应用变更来实现这一点。- 使用块版本号来检测任何因为在其
chunkserver关闭时错过变更而变得陈旧的副本。这些陈旧副本不会参与变更,也不会提供给寻求块位置的客户端。这些副本会在最早的机会被垃圾回收。
3.5 处理故障
GFS通过定期的master与所有块服务器之间的握手以及校验和来识别失败的块服务器和数据损坏。- 数据会尽快从有效副本中恢复,且在
GFS能够反应之前,只有当所有副本丢失时,块才会不可逆转地丢失。即使在这种情况下,它也会变得不可用,而不是损坏:应用程序会收到清晰的错误而不是损坏的数据。
3.6 总结
- 依赖追加操作:而非覆盖操作来变更文件,这不仅可以应对一致性和并发问题,而且比随机写入更高效、更能抵御应用程序故障。
- 使用检查点(
Checkpointing):允许写入者在失败后能够部分地重新开始,同时使读取者避免处理尚未完成的文件数据。 - 写入自验证、自识别的记录:通过额外的信息如校验和来确保记录的有效性,以及通过唯一标识符来处理可能的重复记录。
PS: 这一部分论文中感觉有些泛泛而谈, 缺乏距离和图示, 感觉有些迷糊
4 租约(Leases)机制
4.1 租约(Leases)机制的工作流程
租约机制主要是用于协调分布式环境下的多个对相同chunk的变更操作, 因为多个变更操作的变更顺序很重要, 所有副本在应用变更时都遵循这个顺序。Leases的设计思路是将某一时间段内的处理任务交付给某一个chunkserver完成, 称为其获得了租约(lease),
具体流程为:
client询问master哪个数据块服务器持有当前对于该数据块的租约,以及其他副本的位置。如果没有任何副本持有租约,master会选择一个副本授予租约(未展示), 这个被授予租约的server称为primary replica, 其协调的server称为secondary replica。master回复client其primary replica的身份和其他(次级)副本的位置。client缓存这些数据以用于未来的变更。只有当primary replica变得无法联系,或者回复说它不再持有租约时,client才需要再次联系master。client将数据推送到所有副本。client可以按任何顺序这么做。每个数据块服务器都会将数据存储在一个内部的LRU缓冲区中,直到数据被使用或者过时。通过将数据流与控制流解耦,我们可以根据网络拓扑优化昂贵的数据流的调度,而不考虑哪个数据块服务器是primary replica。第3.2节将进一步讨论这一点。- 所有副本确认收到数据后,
client向primary replica发送写请求。该请求标识了之前推送给所有副本的数据。primary replica为它接收到的所有变更分配连续的序列号,可能来自多个client,这提供了必要的序列化。它按照序列号顺序将变更应用到自己的本地状态。 primary replica将写请求转发给所有次级副本。每个次级副本按照primary replica分配的相同序列号顺序应用变更。- 所有次级副本都回复
primary replica,表明它们已经完成了操作。 primary replica回复client。在任何副本遇到的任何错误都会报告给client。如果出现错误,写操作可能已经在primary replica和任意子集的次级副本上成功。(如果在primary replica上失败,它不会被分配序列号并转发。)client请求被视为失败,而且被修改的区域处于不一致状态。我们的client代码通过重试失败的变更来处理此类错误。在回退到从写操作开始的重试之前,它会在步骤(3)到(7)中尝试几次。
4.2 流程图
下图所示为摘自论文中的读写流程实例:

需要补充说明的是数据流动并不是随意的, 为了充分利用每台机器的网络带宽,数据是沿着数据块服务器的链线性推送的, 即每台机器都将数据转发到网络拓扑中尚未接收数据的“最近”的机器。
5 Master节点: 命名空间与锁
与许多传统文件系统不同,GFS没有列出该目录中所有文件的每个目录的数据结构。它也不支持文件或目录的别名(即硬链接或符号链接)。GFS在逻辑上将其名称空间表示为一个查找表,将完整路径名映射到元数据。通过前缀压缩,可以在内存中有效地表示这个表。名称空间树中的每个节点(无论是绝对文件名还是绝对目录名)都有一个关联的读写锁。
每个操作在运行前都会获取一组锁。通常,如果它涉及到/d1/d2/.../dn/leaf,它将获得目录名/d1、/d1/d2、...、/d1/d2/.../dn的读锁,以及完整路径名/d1/d2/.../dn/leaf的读锁或写锁。请注意,leaf可能是文件或目录,具体取决于操作。
一个获取锁的例子
需求:如何防止以下情况: 当创建
/home/user文件夹的快照到/save/user时, 文件/home/user/foo被创建。
快照操作获取的锁:
/home和/save的读锁/home/user和/save/user的写锁。
文件创建获取的锁/home和/home/user的读锁/home/user/foo的写锁。
可以看出, 上述2个操作存在/home/user的锁冲突, 因此将被序列化。
误区说明
文件创建不需要对父目录的写锁,因为没有目录或类似inode的数据结构需要防止修改。名称上的读锁足以防止父目录被删除。
6 小结
GFS论文中涉及很多的细节在此处并没有完全列出, 例如确保内容完整性的checksum, 主节点的recover等, 因为这些内容较为琐碎且个人认为不算GFS最出色的特性, 个人认为GFS最值得学习的特性包括:
- 单
master结构,master尽量只负责处理逻辑 - 租约机制: 将任务指派给
server解决, 用租约解决潜在的不一致问题 - 数据和逻辑指令分别传送: 简化了传送的内容与逻辑复杂度