(undone) 阅读 Google File System 论文笔记

url: https://pdos.csail.mit.edu/6.824/papers/gfs.pdf

大致浏览论文结构,6,8,9 这三节建议跳过


Abstract

我们设计并实现了Google文件系统(GFS)——一个面向大规模分布式数据密集型应用的可扩展分布式文件系统。该系统在廉价的商用硬件上运行,既能提供容错功能,又能为大量客户端提供高聚合性能。

虽然与先前分布式文件系统有着诸多共同目标,但我们的设计源自对应用负载特性和技术环境的观察,这些既包含现状也涵盖预期变化,显著区别于早期文件系统的某些假设。这促使我们重新审视传统设计方案,探索根本不同的设计路径。

该文件系统已成功满足我们的存储需求。作为数据生成与处理的存储平台,GFS在谷歌内部被广泛部署:既支撑着服务运营所需的数据处理,也支持需要海量数据集的研发工作。迄今最大的集群包含上千台机器,通过数千块磁盘提供数百TB存储空间,可同时响应数百个客户端的并发访问。

本文中,我们将阐述为支持分布式应用而设计的文件系统接口扩展方案,深入讨论系统设计的多个关键方面,并通过微观基准测试和实际应用场景的数据进行性能评估。
总结:我们设计了一个谷歌内部使用的分布式文件系统,它和传统分布式文件系统有些不同


1. INTRODUCTION

我们设计并实现了Google文件系统(GFS),以满足谷歌数据处理需求快速增长的要求。GFS与以往分布式文件系统有许多共同目标,如性能、可扩展性、可靠性和可用性。但我们的设计受到了对当前及预期应用负载与技术环境的关键观察所驱动,这些观察反映出与早期文件系统设计假设的显著差异。我们重新审视了传统选择,在系统设计空间探索了截然不同的方向。

首先,组件故障是常态而非例外。该系统由数百甚至数千台采用廉价商用部件的存储机器组成,被数量相当的客户端机器访问。组件的数量和质量决定了在任意给定时间总有部分组件无法正常工作,有些甚至无法从当前故障中恢复。我们遇到过应用程序错误、操作系统漏洞、人为失误、磁盘/内存/连接器/网络/电源故障等问题。因此,持续监控、错误检测、容错和自动恢复必须成为系统的核心功能。
挑战1:节点故障常常出现

其次,按传统标准衡量文件体积巨大。多GB文件十分常见,单个文件通常包含网页文档等众多应用对象。当我们日常处理由数十亿对象组成、快速增长的TB级数据集时,即便文件系统支持,管理数十亿个约KB大小的文件也极为不便。这要求我们必须重新审视I/O操作和块大小等设计假设与参数。
挑战2:文件体积大、文件数量多是分布式文件系统的挑战,我们需要重新设计 I/O 操作和块大小

第三,多数文件通过追加新数据而非覆盖现有数据来修改。文件内的随机写入几乎不存在。文件一旦写入就只进行读取操作(且多为顺序读取)。各类数据都具有这些特征:有些是分析程序扫描的大型资料库;有些是运行应用持续产生的数据流;有些是归档数据;有些是在某台机器生成、在另一台处理(同步或异步)的中间结果。针对大文件的这种访问模式,追加操作成为性能优化与原子性保证的重点,而客户端缓存数据块则失去意义。
挑战3:根据实践中的观察,大多数文件操作是追加新数据和读取数据,文件内随机写入相对少见。因此,追加操作的性能优化是重点。相对的,在客户端缓存数据块就没有那么有吸引力了。

第四,应用程序与文件系统API的协同设计通过提升灵活性使整体系统受益。例如我们放宽了GFS的一致性模型,在避免给应用增加繁重负担的同时极大简化了文件系统。我们还引入了原子追加操作,允许多客户端无需额外同步即可并发追加文件。这些将在后文详细讨论。
挑战4:放宽了 GFS 的一致性模型,具体后文详细讨论

目前多个GFS集群服务于不同用途。最大集群包含逾1000个存储节点、超过300TB磁盘存储空间,持续承受来自数百台独立客户端机器的高频访问。


2. DESIGN OVERVIEW

2.1 Assumptions

在设计符合我们需求的文件系统时,我们遵循了一系列既带来挑战又蕴含机遇的基本假设。前文已提及部分关键观察点,现将我们的核心假设详细阐述如下:

  • 系统由大量廉价商用组件构成,故障频发
    系统必须持续自检,能够例行性地实现组件故障的监测、容错与快速自愈。
  • 系统存储中等数量的大文件
    我们预期管理数百万个文件,单个文件通常为100MB或更大。多GB文件属于常规情况,需高效管理。虽支持小文件存储,但无需为其特别优化。
  • 工作负载以两类读取为主
    大流量顺序读取:单次操作通常读取数百KB(常见1MB以上),同一客户端的连续操作多集中于文件的相邻区域;
    小规模随机读取:在任意偏移量读取几KB数据。注重性能的应用常对小读取操作进行批处理和排序,以实现稳定顺序读取而非反复跳转。(注重性能的应用对小读取操作进行批处理和排序,以实现稳定顺序读取而非反复跳转)
  • 工作负载包含大量追加式顺序写入
    写入操作的典型数据规模与读取相当。文件一经写入很少修改。虽支持任意位置的小规模写入,但无需保证其效率。
  • 系统需高效支持多客户端并发追加的明确定义语义
    我们的文件常被用作生产者-消费者队列或多路归并载体。数百个生产者(每台机器运行一个)会并发追加文件。必须实现低同步开销的原子性操作。文件可能被后续读取,或被消费者同时读取。
  • 高持续吞吐量比低延迟更重要
    我们的目标应用大多重视大批量高速数据处理,仅有少数场景对单次读写操作的响应时间有严苛要求。
    这里阐述了我们分布式系统可能会面对的场景

2.2 Interface

GFS 提供了符合常规认知的文件系统接口,但并未实现 POSIX 等标准 API。文件以目录树形式分层组织,并通过路径名进行标识。我们支持常规的文件创建、删除、打开、关闭、读取和写入操作。

此外,GFS 还提供快照(snapshot)和记录追加(record append)两项特色操作。快照能够以极低成本创建文件或目录树的副本;记录追加允许多个客户端并发地向同一文件追加数据,同时确保每个客户端追加操作的原子性。该特性对于实现多路归并结果和生产者-消费者队列尤为实用——众多客户端无需额外加锁即可同步追加数据。实践证明这两类文件操作对构建大型分布式应用具有不可替代的价值。快照与记录追加功能将分别在第 3.4 节和第 3.3 节详细阐述。
GFS 支持的API包括 “创建”、“删除”、“打开”、“关闭”、“读取”、“写入”、“快照”、“记录追加”
“快照”、“记录追加” 对构建大型分布式应用具有不可替代的价值。

2.3 Architecture

如下,图1是 GFS 架构图
在这里插入图片描述

一个GFS集群由单个主控节点(master)、多个数据块服务器(chunkserver)以及多个客户端组成,其架构如图1所示。这些组件通常都是运行用户级服务进程的普通Linux服务器。只要机器资源允许,并且能够接受因运行可能存在缺陷的应用程序代码而降低的可靠性,完全可以在同一台机器上同时运行数据块服务器和客户端。
分布式系统的组件通常都是普通 Linux 机器,若能接收可靠性降低,可以在同一台机器上运行数据块服务器和客户端

文件被分割成固定大小的数据块(chunk)。每个数据块在创建时由主控节点分配一个不可变且全局唯一的64位标识符(chunk handle)。数据块服务器将数据块以Linux本地文件的形式存储,并通过数据块标识符和字节范围来读写数据。为确保可靠性,每个数据块会在多个数据块服务器上保存副本。默认配置为3个副本,但用户可以为文件命名空间的不同区域指定不同的副本数量。
主控节点为每个数据块维护不可变且全局唯一的标识符。为确保可靠性,每个数据块会在多个数据块服务器上保存副本

主控节点维护着整个文件系统的元数据,包括:命名空间、访问控制信息、文件到数据块的映射关系,以及数据块的实时位置信息。它还负责管控以下系统级操作:数据块租约管理、孤立数据块的垃圾回收,以及数据块在服务器间的迁移。主控节点通过定期发送心跳(HeartBeat)消息与各数据块服务器保持通信,既传递指令也收集状态信息。
主控节点维护整个文件系统的元数据,包括 xxxx 等等,还通过发送心跳信息得知各节点是否故障

嵌入应用程序的GFS客户端代码实现了文件系统API,代表应用程序与主控节点和数据块服务器交互以完成数据读写。客户端仅向主控节点请求元数据操作,所有承载实际数据的通信都直接与数据块服务器进行。由于不提供POSIX API,我们的系统无需挂载到Linux的vnode层。
嵌入应用程序的GFS客户端代码实现了文件系统API:客户端仅向主控节点请求元数据操作,所有承载实际数据的通信都直接与数据块服务器进行。

客户端和数据块服务器均不缓存文件数据。对客户端而言,缓存收益甚微——大多数应用要么持续流式读写大文件,要么工作集规模远超缓存容量。取消客户端缓存不仅简化了客户端设计,还避免了缓存一致性问题(但客户端仍会缓存元数据)。数据块服务器则无需额外缓存,因为数据块本就存储为本地文件,Linux的缓冲区缓存(buffer cache)已自动将频繁访问的数据保留在内存中。
客户端和数据块服务器均不缓存文件数据,但客户端仍会缓存元数据

2.4 Single Master

采用单一主控节点的设计极大简化了系统架构,使主控节点能够基于全局信息做出精细化的数据块分布与副本策略决策。然而,我们必须尽量减少其在读写流程中的参与度,以避免其成为性能瓶颈。客户端从不通过主控节点读写文件数据,而是向主控节点查询应访问的数据块服务器,并在限定时间内缓存该信息,后续操作直接与数据块服务器交互。

参考图1,我们以一个简单读取流程为例说明交互过程:首先,客户端根据固定数据块大小,将应用程序指定的文件名和字节偏移量转换为文件内的数据块索引。随后向主控节点发送包含文件名和数据块索引的请求。主控节点返回对应数据块标识符及其副本位置信息,客户端以文件名和数据块索引为键缓存这些元数据。

接着,客户端会选择一个最近的副本服务器发起请求,该请求携带数据块标识符及块内字节范围。在缓存信息失效或文件重新打开前,对该数据块的后续读取都不需要再次联系主控节点。实际上,客户端通常会单次请求多个连续数据块的信息,主控节点也会主动返回请求数据块相邻的预读信息——这种机制以近乎零成本显著减少了未来的客户端-主控节点交互次数。
总结:
1.图1结构采用单一主控节点,极大简化系统架构
2.为避免主控节点成为性能节点,客户端仅向主控节点查询应访问的数据块服务器,后续直接和数据块服务器交互
3.交互过程:
3.1.客户端根据固定数据块大小,将应用程序指定的文件名和字节偏移量转换为文件内的数据块索引。随后向主控节点发送包含文件名和数据块索引的请求。
3.2.主控节点返回对应数据块标识符及其副本位置信息,客户端以文件名和数据块索引为键缓存这些元数据。
3.3.接着,客户端会选择一个最近的副本服务器发起请求,该请求携带数据块标识符及块内字节范围。在缓存信息失效或文件重新打开前,对该数据块的后续读取都不需要再次联系主控节点。

性能提升的地方:实践中,客户端通常会单次请求多个连续数据块的信息,主控节点也会主动返回请求数据块相邻的预读信息——这种机制以近乎零成本显著减少了未来的客户端-主控节点交互次数。

2.5 Chunk Size

数据块大小是关键设计参数之一。我们选择了64MB这一远超传统文件系统块大小的规格 (Linux 似乎一个磁盘块/扇区是 512/1024 字节),每个数据块副本在数据块服务器上以普通Linux文件形式存储,并采用按需扩展的空间分配策略。这种惰性空间分配机制避免了因内部碎片导致的空间浪费——这可能是针对如此大块设计最常见的质疑。

大块设计带来多重显著优势:
1.减少客户端与主控节点交互:对同一数据块的读写仅需向主控节点获取一次位置信息。这一优势对我们的工作负载尤为明显,因为应用程序主要进行大文件顺序读写。即便是小规模随机读取,客户端也能轻松缓存数TB工作集的所有数据块位置信息。
2.降低网络开销:大块使得客户端更可能在较长时间内持续访问同一数据块,通过维持与数据块服务器的持久TCP连接有效减少网络握手开销。
3.压缩元数据规模:较小的元数据量使主控节点能将全部元数据保存在内存中,这一特性还带来第2.6.1节将讨论的额外优势。

但大块设计也存在缺陷,即便是惰性空间分配也无法完全规避。小文件可能仅包含少量数据块(甚至单个),当多客户端集中访问时,存储这些数据块的服务器可能成为热点。实际应用中这并未造成严重问题,因为我们的工作负载以顺序读取多块大文件为主。

然而,当GFS首次被批处理队列系统使用时,确实出现了热点问题:可执行文件以单块形式写入GFS后,同时在数百台机器上启动,导致存储该文件的少数数据块服务器因承受数百个并发请求而过载。我们通过双重措施解决该问题:

  • 提高此类可执行文件的副本数
  • 使批处理队列系统错开应用启动时间

未来可能的解决方案是允许客户端在此类场景下从其他客户端读取数据,这将成为长期改进方向。

总结:
GFS 采用 64MB这一远超传统文件系统块大小的规格 (Linux 似乎一个磁盘块/扇区是 512/1024 字节),有几个好处:
1.减少客户端与主控节点交互 (这可能是最主要的优势)
2.大块使得客户端更可能在较长时间内持续访问同一数据块,降低网络开销
3.压缩元数据规模:较小的元数据量使主控节点能将全部元数据保存在内存中
缺点有:较小的热点文件、磁盘空间碎片化
可以用以下方法缓解:
磁盘空间碎片化 ----> 惰性按需分配
较小的热点文件 ----> 增加副本、错开使用时间

2.6 Metadata

主节点存储三大类元数据:文件与数据块命名空间、文件到数据块的映射关系,以及各数据块副本的存储位置。所有元数据都保存在主节点内存中。前两类元数据(命名空间和文件-数据块映射)还会通过操作日志实现持久化——这些记录变更操作的日志存储在主节点本地磁盘,并同步复制到远程机器。采用日志机制能让我们以简单可靠的方式更新主节点状态,即使主节点崩溃也不会引发数据不一致问题。主节点不会持久化存储数据块位置信息,而是在启动时或当有数据块服务器加入集群时,主动向各数据块服务器查询其持有的数据块信息。
都是重点

2.6.1 In-Memory Data Structures

由于元数据存储在内存中,主节点的操作速度极快。此外,主节点能够轻松高效地在后台定期扫描其完整状态数据。这种周期性扫描机制被用于实现以下功能:数据块垃圾回收、数据块服务器故障时的副本重制、以及跨服务器的负载均衡与磁盘空间优化(通过数据块迁移)。第4.3和4.4节将对这些功能进行详细讨论。

这种纯内存存储方式可能引发的顾虑是:数据块数量(进而整个系统的容量)会受限于主节点的内存大小。但实践中这个限制并不严苛——主节点为每个64MB数据块维护的元数据不足64字节。由于大多数文件都包含多个数据块(仅末尾块可能存在部分填充),绝大多数数据块都是满容量存储。同理,文件命名空间数据通常每文件占用不足64字节,因为系统采用前缀压缩技术紧凑存储文件名。

即使需要支持更庞大的文件系统,为主节点扩容内存的成本也物超所值:我们通过内存存储元数据获得的架构简洁性、系统可靠性、卓越性能与部署灵活性,远超过硬件升级的边际成本。
元数据存于内存的好处:
1.主节点操作速度很快,为复杂功能提供性能支持
2.简化架构,比硬件升级的边际成本更好
可能的顾虑:
1.数据块数量受限于主节点内存大小,但在实际中这个问题并不严苛。主节点为每个64MB数据块维护的元数据不足64字节,且大多数文件都包含多个数据块。且增加内存也不是什么昂贵的事情。

2.6.2 Chunk Locations

主节点不会持久化记录哪些数据块服务器存有特定数据块的副本,而是在启动时主动轮询数据块服务器来获取这些信息。此后,主节点通过控制所有数据块放置策略,并定期接收心跳消息来监控数据块服务器状态,即可保持信息的实时更新。

我们最初尝试在主节点持久化存储数据块位置信息,但最终发现更简单的方案是:在启动时向数据块服务器请求这些数据,并后续定期更新。这种做法完美解决了主节点与数据块服务器之间的同步难题——当数据块服务器加入或离开集群、更名、故障、重启等情况发生时(在数百台服务器的集群中这类事件频繁发生),无需再维护两者的信息一致性。

理解这一设计决策的另一个角度是:数据块服务器对其磁盘上存储的数据块拥有最终解释权。试图在主节点强求维护这些信息的一致性视图并无意义,因为数据块服务器的异常(如磁盘损坏导致数据块突然消失)或运维人员对服务器的重命名操作,都可能使主节点维护的信息失效。
主节点主动轮询数据块服务器来得到文件的数据块位置信息

2.6.3 Operation Log (介绍了操作日志的功能、必要性,以及设计操作日志时的一些哲学)

操作日志是GFS系统的核心组件,它完整记录了所有关键元数据的变更历史。这一设计具有双重核心价值:不仅是元数据持久化存储的唯一载体,更是界定并发操作时序的逻辑时间基准。系统通过创建时记录的逻辑时间戳,为文件、数据块及其版本(详见4.5节)赋予全局唯一且永久的身份标识。
操作日志是核心组件,具备几个重要作用:1.数据持久化 2.节点并发操作时序

为确保这一关键组件的可靠性,我们实施了严格的存储策略:在元数据变更完全持久化之前,相关修改绝不会对客户端可见。这种机制至关重要——否则即使数据块实体完好无损,仍可能导致整个文件系统崩溃或近期客户端操作集体丢失。为此,我们采用多节点远程镜像方案:只有当相关日志记录在本地和远程磁盘均完成写入后,系统才会响应客户端请求。主节点通过批量聚合多条日志记录进行统一刷盘,显著降低了持久化操作对系统整体吞吐量的影响。
在元数据变更完全持久化之前,相关修改绝不能对客户端可见。

系统恢复时,主节点通过重放操作日志重建文件系统状态。为优化启动效率,我们严格控制日志体积——当日志超过阈值时,主节点会生成状态检查点。这使得恢复过程只需加载最新的本地磁盘检查点,再重放少量后续日志即可完成。检查点采用高度压缩的类B树结构,可直接内存映射供命名空间查询使用,无需额外解析,极大提升了恢复速度和系统可用性。
系统恢复时,主节点通过重放操作日志重建文件系统状态。为优化启动效率,我们严格控制日志体积——当日志超过阈值时,主节点会生成状态检查点。

考虑到检查点构建耗时较长,主节点内部采用特殊状态结构,确保检查点创建过程不会阻塞新请求处理。具体实现包含两个关键步骤:首先切换至新日志文件,随后在独立线程中生成检查点(包含切换前的全部变更)。对于包含数百万文件的集群,该过程通常可在1分钟内完成。最终生成的检查点会同步写入本地和远程存储节点。
考虑到检查点构建耗时较长,主节点内部采用特殊状态结构,确保检查点创建过程不会阻塞新请求处理。

系统恢复仅依赖最新的完整检查点及后续日志文件。虽然旧检查点和日志可安全删除,但我们仍会保留部分历史文件作为灾难恢复保障。值得注意的是,检查点生成过程中出现故障不会影响系统正确性——恢复程序能够自动识别并跳过不完整的检查点文件。
系统恢复仅依赖最新的完整检查点及后续日志文件。

2.7 Consistency Model

GFS采用了一种宽松的一致性模型,这种设计既很好地支撑了我们高度分布式的应用需求,又保持了相对简单高效的实现方式。下面我们将阐述GFS提供的一致性保证及其对应用程序的意义,同时概要说明GFS如何维护这些保证,具体实现细节将在本文其他部分展开讨论。

2.7.1 Guarantees by GFS (TODO: here)

GFS采用了一套宽松但严谨的一致性模型,其核心设计理念可归纳如下:

一、元数据操作的原子性保证
文件命名空间操作(如文件创建)具有原子性特征,这些操作由主节点集中处理:
1.通过命名空间锁机制确保原子性和正确性(详见4.1节)
2.主节点操作日志定义了全局唯一操作序列(详见2.6.3节)

二、数据操作的一致性分级
文件区域在数据变更后的状态取决于:

操作类型(写入/追加记录)

操作成功与否

是否存在并发操作

我们定义以下术语:

一致性区域:所有客户端读取结果一致(无论访问哪个副本)

已定义区域:具备一致性且完整反映某次操作结果

具体场景分析:

成功且无并发的操作:产生已定义区域(必然一致)

并发成功操作:区域未定义但一致(数据可能混合多个操作片段)

失败操作:区域不一致且未定义(不同客户端可能看到不同数据)

三、数据操作类型

指定偏移写入:在客户端指定位置写入数据

记录追加:原子性追加(至少一次),由GFS分配偏移量(详见3.3节)

返回的偏移量标记包含记录的已定义区域起始点

可能包含填充数据或重复记录(标记为不一致区域)

四、系统保证机制

顺序控制:对所有副本按相同顺序应用操作(3.1节)

版本检测:通过块版本号识别过期副本(4.5节)

过期副本不参与操作且不会分配给客户端

通过定期垃圾回收清理

五、故障处理机制

客户端缓存:可能短暂读取过期副本(受缓存超时机制限制)

数据完整性:

定期心跳检测故障节点

校验和机制识别数据损坏(5.2节)

分钟级快速恢复(4.3节)

极端情况:仅当所有副本同时丢失才会永久性数据丢失

系统返回明确错误而非损坏数据

该模型为应用程序提供清晰的状态区分:

可明确识别已定义/未定义区域

无需区分不同类型的未定义区域

追加操作天然更适合分布式场景

TODO: here


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值