GoogleFileSystem论文笔记
背景
GFS(Google File System)是为了解决Google 数据处理需求快速增长的需求而发明的。它是一个大型数据密集型应用分布式文件系统,运行在廉价的商业硬件上。GFS 与以前的分布式文件系统有许多相同的目标,例如性能、可扩展性、可靠性和可用性。不同之处在于它提供了容错能力。
GFS特点
组件容错性
首先组成GFS系统成本不高,都是运行在廉价的商业硬件上,并且是上百数千台。所以有部分机器损坏也是正常。而引起损坏的有可能是:
- 应用程序错误
- 操作系统错误
- 人为错误
- 以及磁盘、内存、连接器、网络和电源故障引起的问题。
为了解决以上问题,需要使用以下手段:
- 持续监控
- 错误检测
- 容错
- 自动恢复
数据处理以及文件操作设计
因为需要面对的是数据爆炸增长,以TB组成的快速增长数据集, 以及在传统标准下文件会非常庞大,常见情况是存在多个GB( Multi-GB)大小文件。每个文件又包含了应用对象,比如说Web文档,这些不是普通二进制块,而是类似于JSON数据等等更高层次的应用数据结构。所以GFS重新设计了IO操作和块大小。
文件写入与读取
- 文件写入方式:
- GFS使用仅追加方式(only-append): 在GFS中,文件的变更主要通过追加新数据到文件末尾的方式进行。这种追加方式是一种往文件中添加新数据而不是覆盖已存在数据的方式。这对于处理大规模数据的应用场景具有一定的优势。
- 其他文件系统通常使用覆盖已存在数据的方式: 与GFS不同,其他一些传统的文件系统在进行文件写入时一般采用覆盖已存在数据的方式,即直接替换或修改文件中的某一部分数据。
- 随机写几乎不存在: GFS中随机写几乎不存在,文件的变更主要通过追加操作。这是因为大多数文件在GFS中是通过追加新数据而不是在文件中随机位置写入数据。
- 文件的只读状态和顺序读取: 一旦文件被写入,就会被设置为只读状态,并且通常只以顺序读取的方式被访问。这强调了对于大文件来说,读取操作主要是按照文件的顺序进行的。
- 追加作为性能优化和原子性保证的焦点: 由于文件的追加方式,GFS将性能优化和原子性保证的焦点放在了追加操作上。这是因为在大文件的访问模式中,追加操作是主要的写入方式,对其进行性能优化和保证原子性非常关键。
- 客户端缓存数据块失去吸引力: 由于大文件主要以追加方式进行变更,并且很少涉及随机写入,因此在客户端缓存数据块方面失去了吸引力。这反映了对于GFS而言,优化追加操作的性能可能比优化客户端数据块缓存更为重要。
GFS设计和实现关键点
- 协同设计应用程序和文件系统API: GFS强调应用程序和文件系统API的协同设计,以提高整个系统的灵活性。这种协同设计有助于优化文件系统以满足特定应用程序需求,提高系统的性能和适应性。
- 一致性模型的放宽: 为了简化文件系统而不给应用程序带来沉重的负担,GFS放宽了一致性模型。这是一种在一致性和性能之间的权衡,使得文件系统更为简单,同时满足系统的要求。
- 原子追加操作的引入: GFS引入了原子追加操作,允许多个客户端同时向文件追加数据,而无需额外的同步。这提高了文件追加操作的并发性,有助于提高系统的性能。
- 大规模部署: 描述了GFS目前在不同目的下部署了多个集群,其中最大的集群拥有超过1000个存储节点,超过300TB的磁盘存储,且由数百个客户端在不同机器上持续访问。
系统设计
假设
- 监控与容错: GFS假设系统由许多廉价的通用组件组成,这些组件通常可能发生故障。因此,系统必须持续监测自身,能够检测、容忍并及时从组件故障中恢复。
- 存储大数据文件: 预期系统将存储几百万个文件,每个文件通常为100 MB或更大。多GB文件是常见情况,需要高效管理。尽管需要支持小文件,但不必为其进行优化。
- 工作负载读取方式
- 大型流式读取: 涉及每个操作读取数百KB,更常见的是1MB或更多。
- 小型随机读取: 在文件的某个任意偏移位置读取少量KB。性能敏感的应用程序通常会批量处理和排序它们的小型读取,以稳定地通过文件前进。
- 工作负载大型顺序写入
- 多客户端并发追加同一个文件: 系统必须有效地实现多个客户端并发追加到相同文件的明确定义的语义 文件经常被用作生产者-消费者队列或多路合并。数百个生产者,每台机器运行一个,将同时追加到一个文件。原子性以及最小的同步开销是至关重要的。文件可以稍后读取,或者消费者可能同时读取文件。
- 高持续带宽比低延迟更重要: 大多数目标应用程序更注重以高速率批量处理数据,而对于单个读取或写入的响应时间要求不那么严格。
接口设计
文件在目录中按层次结构组织,并由路径名标识。 支持创建、删除、打开、关闭、读取和写入文件的常规操作。需要注意的地方在于并没有按照POSIX等标准创建。并且它支持快照和记录追加操作。
Snapshot 以较低的成本创建文件或目录树的副本。 记录追加允许多个客户端同时将数据追加到同一个文件,同时保证每个客户端追加的原子性。
系统架构
GFS集群由一个master主节点和多个块服务器组成(chunkservers), 个节点通常是运行用户级服务器进程的通用Linux机器。并且支持多用户访问。
文件被划分为固定大小的块,每个块由主节点在块创建时分配的不可变且全局唯一的64位块句柄(chunk handle)标识。块服务器将块存储在本地磁盘上,作为Linux文件,并根据块句柄和字节范围读取或写入块数据。
为了提高可靠性,每个块在多个块服务器上进行复制,一般存储三个副本,但用户可以为文件命名空间的不同区域指定不同的复制级别。
master节点负责维护所有文件系统元数据,包括命名空间、访问控制信息、文件到块的映射以及块的当前位置。还有系统范围的活动,如块租约管理、孤立块的垃圾收集和块在块服务器之间的迁移。
GFS客户端代码嵌入到每个应用程序中,实现文件系统API,并与主节点和块服务器通信,代表应用程序读取或写入数据。客户端与主节点进行元数据操作,但所有承载数据的通信直接与块服务器进行。GFS不提供POSIX API,因此无需与Linux vnode层连接。
客户端块服务器不缓存文件数据。客户端缓存对于大多数应用程序的大文件或工作集过大而无法缓存的情况效益不大。这样做简化了客户端和整个系统,消除了缓存一致性问题。块服务器也不需要缓存文件数据,因为块被存储为本地文件,因此Linux的缓冲缓存已经将经常访问的数据保存在内存中。
master设计
设计成单master的原因在于,可以简化系统结构,它只需要接收客户端请求,告诉客户端块服务器地址即可。需要注意的是应该尽量不让master参与读写相关操作,容易成为瓶颈。
进行简单的读操作时,客户端首先使用固定的块大小将应用程序指定的文件名和字节偏移转换为文件中的块索引。然后,客户端向主节点发送一个请求,包含文件名和块索引。主节点回复包含相应的块句柄和副本的位置。客户端使用文件名和块索引作为键缓存这些信息。
客户端接着向其中一个副本发送请求,通常选择最近的副本。请求指定块句柄和块内的字节范围。对相同块的进一步读取不需要客户端与主节点的交互,直到缓存的信息过期或文件重新打开。参考架构图中(chunk handle,byte range)
客户端通常会在同一请求中请求多个块,而主节点也可以包含紧随所请求块之后的块的信息。这个额外的信息在几乎不增加额外成本的情况下,避免了未来的客户端-主节点交互。
chunk大小设计
chunk大小选择在64MB,远大于典型的文件系统块大小。每个块副本存储为块服务器上的普通Linux文件,并根据需要进行扩展。采用懒惰的空间分配避免了由于内部碎片而浪费空间。而选择64MB的原因在于:
- 减少与master节点交互频率: 因为在同一块上的读写只需要一次初始请求,获取块位置信息即可。对于大多数顺序读写大文件的应用程序,这一减少尤为显著。
- 降低网络开销: 大的chunk可以使客户端更有可能在给定的块上执行多个操作,通过在较长时间内保持到块服务器的持久TCP连接来降低网络开销。
- 减小master节点上元数据大小: 大的chunk减少了存储在主节点上的元数据的大小,使得可以将元数据保持在内存中,从而带来其他优势。
缺点在于:
- 潜在的热点问题:大的chunk可能导致小文件只包含少量块,这可能使存储这些块的块服务器成为热点,特别是当许多客户端访问同一文件时。实际上,热点问题在实践中并不是主要问题,因为应用程序大多数情况下是顺序读取大型多块文件。
- 潜在的解决方案
元数据
master节点存放三种主要类型的元数据:
- 文件和块的命名空间。
- 从文件到块的映射。
- 每个块副本的位置。
所有的元数据都存储在内存中,前两种也可以通过将变更记录到主节点本地磁盘上的操作日志来保持持久性,并在远程机器上进行复制。
主节点不会存储给定块的哪些块服务器有副本的持久记录。它在启动时简单地向块服务器轮询该信息。主节点保持自身的状态更新,因为它控制所有块的放置并通过定期的HeartBeat消息监视块服务器的状态。
初始尝试将块位置信息持久保存在主节点上,但决定在启动时从块服务器请求数据,以及之后定期请求。这消除了在块服务器加入和离开集群、更改名称、故障、重启等情况下保持主节点和块服务器同步的问题。
一致性模型
Google File System (GFS)采用的是一种松散的一致性模型,以支持其高度分布式的应用场景:
- 命名空间变更的原子性:
- 文件命名空间的变更(例如文件创建)是原子的,并由主节点专属处理。
- 命名空间锁定保证了原子性和正确性,主节点的操作日志定义了这些操作的全局总序列。
- 文件区域状态和数据变更:
- 数据变更的结果取决于变更的类型、是否成功以及是否存在并发变更。
- 表格1概述了数据变更后文件区域的状态。
- 一致的区域意味着所有客户端将始终看到相同的数据,定义的区域是一致的,并且客户端将看到变更写入的完整数据。
- 数据变更类型:
- 数据变更可以是写入或记录追加。写入在应用程序指定的文件偏移处写入数据。记录追加在至少一次原子地追加数据(“记录”),即使存在并发变更,但在GFS选择的偏移处进行(第3.3节)。
- 成功变更的保证:
- 在一系列成功的变更之后,变更的文件区域保证是定义的,并包含由最后一个变更写入的数据。
- GFS通过在所有副本上相同顺序应用变更(第3.1节)和使用块版本号来检测已变得陈旧的副本(第4.5节)来实现这一点。
- 陈旧的副本永远不会参与变更或提供给向主节点请求块位置的客户端。它们在最早的机会被垃圾回收。
- 缓存和数据一致性:
- 由于客户端缓存块位置,它们在刷新信息之前可能从一个陈旧的副本读取。这个窗口受缓存条目的超时和文件的下一次打开限制。
- 大多数文件是仅追加的,因此陈旧的副本通常会返回早于数据的结束而不是过时的数据。当读者重试并联系主节点时,它将立即获得当前的块位置。
- 组件故障和数据一致性:
- 长时间之后,成功的变更后,组件故障仍然可能会损坏或销毁数据。
- GFS通过主节点与所有块服务器之间的定期握手识别故障的块服务器,并通过校验和检测数据损坏(第5.2节)。
- 一旦问题出现,数据将尽快从有效副本中恢复(第4.3节)。
- 仅当所有副本在GFS能够做出反应之前全部丢失时,块才会不可逆地丢失。即使在这种情况下,它会变得不可用,而不是损坏:应用程序会收到明确的错误而不是损坏的数据。
应用程序通过以下技术来适应松散的一致性模型。主要的适应技术包括依赖于追加而不是覆盖、定期的检查点操作,以及编写自验证、自识别记录。
主要观点和技术:
- 使用追加而非覆盖: GFS应用程序中几乎所有的文件变更操作都是通过追加而不是覆盖来完成的。这种方式更加高效且更具弹性,特别是对于大规模的数据写入,追加操作更容易适应并发。
- 定期的检查点操作: 应用程序可以定期进行检查点操作,记录文件的当前状态。这样的检查点可以包含应用级别的校验和信息,用于验证数据的完整性。读取器只需处理到最后一个检查点的文件区域,确保处理的数据处于定义状态。
- 自验证、自识别的记录: 当多个写入者并发追加到文件时,记录追加的“至少一次”语义保留了每个写入者的输出。记录中包含额外信息(如校验和),使得读取器可以验证记录的有效性,并处理可能的填充和重复。如果读取器不能容忍偶尔的重复,可以使用记录中的唯一标识符进行过滤。
- 记录的顺序和去重: 应用程序中的记录写入操作通常按照特定的顺序进行,并使用记录中的唯一标识符确保顺序和去重。这样,最终交付给记录读取器的是相同顺序的记录序列,除了偶尔的重复。
系统交互
这一节主要是介绍client是如何和master以及chunk server交互的过程,涉及到以下几个操作:
- 数据变更
- 原子追加
- 快照
需要注意,应该减少master参与以上操作。
Leases and Mutation Order(租约与变更顺序)
租约:
- 租约用于保持所有副本之间一致的变更顺序。
- 主节点授予租约给一个副本,称为主副本,该副本为变更选择一个序列顺序。
- 所有副本都按照此顺序进行变更,定义了全局的变更顺序。
- 主节点授予一个块租约给其中一个副本,被称为主副本(primary)。
- 主副本选择了一种序列顺序,用于所有对该块的变更。
- 所有副本都按照主副本确定的序列顺序应用变更,确保全局的变更顺序。
- 租约的授予顺序由主节点决定,而在租约内部,由主副本分配的序列号决定。
租约管理:
- 租约初始超时为60秒。
- 只要块在变更,主副本可以请求并通常在主节点上无限期地接收租约延期。
- 租约延期请求和授予是通过主节点和所有块服务器之间定期交换的心跳消息进行的。
- 主节点有时可能尝试在租约到期之前撤销租约,例如,当主节点希望在正在重命名的文件上禁用变更时。
- 即使主节点失去与主副本的通信,它仍可以在旧租约过期后安全地向另一个副本授予新的租约。
租约撤销:
- 主节点有时可能在租约到期之前尝试撤销租约(例如,当禁用正在重命名的文件上的变更时)。
- 即使主节点与主副本失去通信,它也可以在旧租约到期后安全地向另一个副本授予新的租约。
数据写入流程:
- 请求租约信息,确定当前租约的主副本和其他副本的位置。
- master回复主副本和其他副本位置,客户端缓存此信息来提供未来变更。
- 将数据推送到所有副本,每个副本都在内部的LRU缓冲区中存储数据,以提高性能。
- 一旦所有副本都确认收到数据,客户端向主副本发送写入请求。
- 主副本为接收到的所有变更分配连续的序列号,将变更应用到本地状态中。
- 主副本将写入请求转发给所有次要(Secondary)副本,每个次要副本按主副本分配的序列号应用变更。
- 所有次要副本回复主副本,表示它们已完成操作。
- 主副本回复客户端,汇报任何在任何副本上遇到的错误。
错误处理
- 如果在任何副本上遇到错误,客户端会收到错误报告。
- 如果写入在主副本和部分次要副本上成功,但在其他副本上失败,客户端请求被视为失败,修改的区域处于不一致状态。
- 客户端代码通过在写入失败时重试来处理此类错误。如果步骤(3)到(7)之间的某个步骤失败,客户端将在写入的开始处重试。
大型写入操作
- 对于大型写入操作,GFS客户端代码将其分解为多个写入操作,按照上述控制流程进行。
- 写入操作可能会与其他客户端的并发操作相互交错,因此共享的文件区域可能包含来自不同客户端的片段,尽管副本是相同的。
数据流
这个没什么太多重点,数据流和控制流分开,这样做的好处是可以最大程度利用网络带宽。每次数据流动都是选择距离自己最近的节点。GFS使用TCP连接来最大程度来减少延迟,一旦chunkserver接收一些数据就会立马转发。因为使用的是全双工链路的交换网络。 立即发送数据不会降低接收速率。
原子追加(Atomic Record Appends)
传统写操作需要指定文件偏移量才能写入,这对于分布式系统来说是不能接受的,因为成本太高了,需要引入分布式锁以及同步等资源消耗。但是在GFS中使用记录追加操作,客户端仅指定数据。 GFS将其至少原子地附加到文件一次。
并将该偏移量返回给客户端。这类似于在Unix中写入一个以O APPEND模式打开的文件,而没有多个写入器并发执行的竞争条件。
操作流程如下:
- 客户端询问主服务器关于某块的租约和其他副本的位置。
- 主服务器回复主副本的身份和其他副本的位置。
- 客户端将数据推送到所有副本。
- 所有副本确认接收数据后,客户端向主副本发送写请求,标识之前推送的数据。
- 主副本为接收到的所有突变分配连续的序列号,按照序列号的顺序将其应用于其本地状态。
- 主副本将写请求转发给所有次要副本,次要副本按照主副本分配的序列号顺序应用突变。
- 所有次要副本回复主副本,表示它们已完成操作。
- 主副本回复客户端,报告操作的成功或错误。
对于”record append”,如果记录的大小超过了块的最大限制(64MB),主副本会将块填充到最大大小,通知次要副本执行相同的操作,并告知客户端在下一个块上重试操作。如果记录适合最大大小,主副本将数据追加到其副本,通知次要副本在确切的偏移量上写入数据,并向客户端回复成功。
“record append”保证了数据至少以原子单元的形式写入,即使在不同副本上,但不保证所有副本在字节级别上相同。这种设计使得GFS可以高效地处理许多客户端在不同机器上同时追加到同一文件的分布式应用场景
快照(Snapshot)
Snapshot使用标准的写时复制方式实现快照,像AFS一样。
通常用于快速创建大型数据集的分支副本,或在可以轻松提交或回滚的情况下进行实验性更改前创建当前状态的检查点。
操作过程如下:
- 撤销租约: 当主服务器接收到快照请求时,首先撤销文件中即将快照的块上的所有未完成的租约。撤销租约确保对这些块的后续写入将需要与主服务器的交互以查找租约持有者,从而给主服务器一个创建块的新副本的机会。
- 记录操作: 在撤销或等待租约到期后,主服务器将快照操作记录到磁盘。
- 复制元数据: 主服务器通过复制源文件或目录树的元数据来应用日志记录到其内存状态。新创建的快照文件指向与源文件相同的块。
- 创建新块: 当客户端在快照后想要写入块时,它向主服务器发送请求以查找当前的租约持有者。主服务器注意到块的引用计数大于1,推迟回复客户端的请求并选择一个新的块句柄(C’)。主服务器要求每个具有块C当前副本的块服务器创建一个名为C’的新块。这确保新块可以在与原始块相同的块服务器上本地创建,避免了网络传输。
- 租约和回复: 主服务器向新块(C’)的一个副本授予租约,并回复客户端。现在,客户端可以正常写入块,而不知道它刚刚是从现有块创建的。
Master操作
master执行所有namespace操作,同时还管理整个系统中的块副本:它做出放置决策,创建新的块和副本,并协调各种系统范围的活动以保持块完全复制,平衡所有块服务器的负载,并回收未使用的存储。
namespace管理与锁
GFS在逻辑上用一个完整路径名到元数据的查找表来表示命名空间。通过前缀压缩技术,这个查找表可在内存中高效地表示。在命名空间树上的每个节点(既可能是一个文件的绝对路径名,也可能是一个目录的绝对路径名)都有一个与之关联的读写锁(read-write lock)。
- 长时间操作的处理:
- 某些master操作可能需要很长时间,例如快照操作需要撤销所有涉及到快照范围内的chunk的租约。为了不延迟其他master操作,允许多个操作同时进行。
- 命名空间表示:
- GFS的命名空间没有像传统文件系统那样的每个目录都列出其中所有文件的数据结构。也不支持相同文件或目录的别名(即Unix术语中的硬链接或符号链接)。
- GFS在逻辑上将其命名空间表示为将完整路径映射到元数据的查找表。通过前缀压缩,可以在内存中高效表示这个表。
- 锁的分配:
- 每个命名空间树节点(绝对文件名或绝对目录名)都有一个相关的读写锁。
- 每个master操作在运行之前会获取一组锁。通常,如果涉及到路径/d1/d2/…/dn/leaf,它将获取目录名/d1,/d1/d2,…,/d1/d2/…/dn的读锁,以及/d1/d2/…/dn/leaf的读锁或写锁。
- 锁的冲突处理:
- 锁的机制可以防止在对/home/user进行快照到/save/user的过程中创建文件/home/user/foo。这是通过在快照操作上获取对/home和/save的读锁,以及对/home/user和/save/user的写锁,以及在文件创建操作上获取对/home和/home/user的读锁,以及对/home/user/foo的写锁来实现的。
- 并发修改的处理:
- 此锁定方案允许在同一目录中进行并发的变更操作。例如,可以同时执行多个文件创建操作:每个操作都在目录名上获取读锁,并在文件名上获取写锁。目录名的读锁足以防止目录被删除、重命名或进行快照。文件名的写锁用于串行化尝试使用相同名称创建文件的操作。
- 锁的懒惰分配和一致性排序:
- 由于命名空间可能有许多节点,锁对象是惰性分配的,并在不再使用时被删除。
- 锁是按照一致的总序列顺序获取,以防止死锁:首先按照命名空间树中的级别排序,然后在相同级别内按字典顺序排序。
副本分配
- 集群分布:
- GFS集群通常具有数百个chunkservers,分布在许多机架上。这些chunkservers又可以被来自相同或不同机架的数百个客户端访问。
- 不同机架上的两台机器之间的通信可能要穿越一个或多个网络交换机。此外,机架内进出的带宽可能小于机架内所有机器的总带宽。
- 分布挑战:
- 多级分布对数据的分发提出了挑战,需要在多个层面上实现可扩展性、可靠性和可用性。
- 复制策略目标:
- 复制策略有两个目标:最大化数据的可靠性和可用性,以及最大化网络带宽的利用率。
- 仅仅将副本分布在不同的机器上是不够的,这只是为了防范磁盘或机器故障,并充分利用每台机器的网络带宽。
- 必须同时将chunk副本分布在不同的机架上,以确保即使整个机架受损或离线(例如,由于共享资源(如网络交换机或电源电路)的故障),某些chunk的副本仍然存活并保持可用性。
- 这也意味着对于一个chunk的流量,特别是读取流量,可以利用多个机架的总带宽。然而,写入流量必须流经多个机架,这是一个权衡,但GFS选择了这种方式。
chunk创建,重做副本,重均衡
chunk副本的创建可能由三个原因引起:
- chunk创建
- 重做副本(re-replication)
- 重均衡(rebalance)
chunk创建
当master创建一个chunk时,它选择在哪里放置最初为空的副本,以下是它的考虑因素:
- 在磁盘空间利用率低于平均水平的chunkservers上放置新副本,以实现磁盘利用率的均衡。
- 限制每个chunkserver上的“最近”创建的数量,因为创建本身虽然廉价,但它可靠地预测到即将发生的大量写流量。
- 如前所述,确保chunk的副本分布在不同的机架上。
重做副本
- 当可用副本数量低于用户指定的目标时,master会立即重新复制一个chunk。
- 可能的原因包括:一个chunkserver变得不可用,报告其副本可能已损坏,其磁盘由于错误而被禁用,或者增加了复制目标。
- 需要重新复制的每个chunk都基于多个因素进行了优先级排序,例如距离其复制目标的距离,文件是否处于活动状态等。
重均衡
- master定期对副本进行重新平衡:它检查当前副本分布,并移动副本以实现更好的磁盘空间和负载平衡。
- 通过这个过程,master逐渐填充一个新的chunkserver,而不是立即用新的chunk淹没它以及伴随着它们的大量写流量。
- 新副本的放置标准类似于上面讨论的标准。此外,master还必须选择要删除的现有副本,通常优先删除在chunkservers上的副本,以实现磁盘空间使用的均衡。
垃圾收集
在文件被删除后,GFS不会立即回收可用的物理存储空间,而是在文件和chunk两个层面上定期进行垃圾收集。这种方法使系统更加简单和可靠。
回收机制
- 当应用程序删除文件时,master会立即记录删除操作,并将文件重命名为包含删除时间戳的隐藏名称。
- 定期扫描命名空间,master删除已存在超过三天的隐藏文件。在此期间,文件可以通过新的特殊名称读取,并可以通过将其重新命名回正常状态来取消删除。
- 当隐藏文件从命名空间中删除时,其内存中的元数据被清除,切断了与所有chunk的链接。
- 在定期扫描chunk命名空间时,master识别并删除孤立的chunks(不可从任何文件访问到的chunks)。
回收机制讨论
- 简单而可靠: 相较于急切的删除,垃圾收集方法在分布式系统中更为简单可靠。它提供了一种一致和可靠的方式来清理未知有用的副本。
- 合并到后台活动: 垃圾收集将存储回收合并到master的常规后台活动中,例如对命名空间的定期扫描和与chunkservers的握手。这有助于批量处理,成本分摊,而且只在master相对空闲时执行。
- 提供安全保障: 推迟回收存储提供了一种安全保障,防止意外和不可逆的删除。文件仍可以在被删除后的一段时间内读取,并在需要时可以重新使用。
- 用户灵活性: 允许用户对不同命名空间部分应用不同的复制和回收策略,使用户能够根据需求进行定制。
过期副本检测机制( Stale Replica Detection)
- Chunk版本号: 每个chunk在master上都有一个版本号,用于区分最新的副本和陈旧的副本。
- 租约分配时更新版本号: 每当master分配一个新的chunk租约时,它会增加chunk版本号并通知所有最新的副本。在客户端开始写入chunk之前,这些副本和master都会将新的版本号记录在它们的持久状态中。
- 检测陈旧副本: 如果一个副本的chunkserver当前不可用,它的chunk版本号将不会增加。当chunkserver重新启动并报告其chunks及其关联版本号时,master会检测到该chunkserver有一个陈旧的副本。
- 陈旧副本的处理: master会在定期的垃圾收集中移除陈旧的副本。在此之前,当客户端请求chunk信息时,master会在回复中将陈旧的副本视为根本不存在。为了增强安全性,master在通知客户端某个chunk由哪个chunkserver持有租约时,或在在克隆操作中指示一个chunkserver从另一个chunkserver读取chunk时,会包含chunk版本号。客户端或chunkserver在执行操作时验证版本号,以确保始终访问最新的数据。
容错性与诊断
GFS面临的主要挑战之一就是如何处理频繁组件故障,由于机器和磁盘的质量和数量,这些问题更常见而非例外:我们不能完全信任机器,也不能完全信任磁盘。组件故障可能导致系统不可用,甚至更糟,导致数据损坏。
以下是GFS如何应对这些挑战以及系统中集成的用于诊断问题的工具:
容错性:
- 副本: GFS通过在多个chunkservers上存储相同数据的多个副本来提高容错性。如果一个chunkserver发生故障,系统仍然可以从其他副本中获取数据。
- Lease机制: 使用租约机制确保在写入操作期间保持一致性。当master给一个chunkserver颁发租约时,该chunkserver成为主要的,负责处理所有关于该chunk的写入请求。即使其他副本所在的chunkserver发生故障,主要副本仍然可用,确保一致性。
诊断工具:
- 日志记录: GFS通过详细的日志记录来追踪系统中的各种活动。这使得在发生故障时可以审查日志以找到问题的根本原因。
- 版本号和租约: GFS使用版本号和租约机制来检测和处理陈旧副本。这有助于追踪副本的状态并确保系统不会使用陈旧的数据。
- 周期性的垃圾收集: 在定期的垃圾收集过程中,master会识别和删除不再需要的副本。这有助于释放存储空间并维护系统的整体健康状态。
- HeartBeat消息: 定期的HeartBeat消息允许master了解每个chunkserver的状态。如果一个chunkserver长时间没有响应,master可以将其标记为故障,并采取相应的措施。
高可用机制
过两个简单而有效的策略保持整个系统的高度可用性:快速恢复(fast recovery)和复制(replication)。
快速恢复(fast recovery)
无论master还是chunkserver,都设计成在几秒钟内恢复其状态并重新启动,无论它们如何终止。并不会区分是什么原因导致,直接通过终止进程来关闭。客户端和其他服务器在其未完成的请求上超时,重新连接到重新启动的服务器并重试时会经历短暂的中断。
复制
- chunk副本
- master副本
每个chunk在不同机架的多个chunkserver上都有副本。用户可以为文件命名空间的不同部分指定不同的副本级别,默认为三个。主要克隆现有副本,以确保每个chunk在chunkservers下线或通过校验和验证检测到损坏的副本时都能够完全复制。
为了提高可靠性,主节点的状态进行了复制。其操作日志和检查点在多台机器上进行了复制。对状态的变更只有在其日志记录已在本地磁盘上和所有主节点副本上刷新之后才被视为已提交。为了简化起见,一个主节点进程负责所有变更,以及改变系统内部的后台活动,如垃圾回收。当主节点失败时,它可以几乎立即重新启动。如果其机器或磁盘发生故障,则GFS外部的监控基础设施将在具有复制的操作日志的其他地方启动新的主节点进程。
此外,“shadow”主节点在主节点关闭时提供对文件系统的只读访问。它们是阴影而不是镜像,因为它们可能稍微滞后于主节点,通常是几分之一秒。它们增强了对不活跃进行变异的文件或不介意获得略旧结果的应用程序的读取可用性。实际上,由于文件内容是从chunkservers中读取的,应用程序不会观察到过时的文件内容。在短时间窗口内可能过时的是文件元数据,例如目录内容或访问控制信息。
为了保持自身的最新状态,阴影主节点读取操作日志的副本,并对其数据结构应用与主节点完全相同的变更序列。与主节点一样,它在启动时轮询chunkservers(以及之后不经常)以查找chunk副本,并与它们交换频繁的握手消息以监视它们的状态。它仅依赖于主要主节点,以获取由于主要的决定而导
数据完整性
在GFS中,为了保障数据的完整性,每个chunkserver使用校验和来检测存储数据的损坏。校验和计算基于64 KB的块,并通过在内存中和日志记录中持久存储校验和,与用户数据分开。这有助于在读取过程中验证数据块的完整性,并防止损坏的数据传播到其他机器。在发现损坏时,系统通过其他副本进行恢复,并删除损坏的副本。
在写入追加数据时,校验和的计算被优化,只需增量更新最后一个部分的校验和块,以及计算由追加填充的任何新的校验和块的新校验和。这样即使最后一个部分校验和块已经损坏,系统也可以在下次读取时检测到损坏。
此外,系统通过定期扫描和验证不活动chunk的内容,发现在很少读取的chunk中可能存在的损坏。一旦检测到损坏,master可以创建一个新的未损坏的副本并删除已损坏的副本,以防止不活动但已损坏的chunk副本影响系统的正确性。
在诊断方面,GFS采用广泛和详细的诊断日志记录。这些日志记录重要事件和所有RPC请求和响应,有助于隔离问题、调试系统以及进行性能分析。由于这些日志是顺序异步写入的,因此对性能几乎没有影响。它们还允许在匹配请求与响应的情况下,整理记录的RPC,以重建系统的整个交互历史,以诊断问题。
例子
文件追加
使用golang对比文件追加和文件覆盖两种方式在并发情况下的表现:
文件覆盖
仅追加
总结与思考
GFS帮助我们解决了大数据增长情况下的数据存储,但是在并没有进行小文件相关优化。在系统架构方面,采用了松散一致性模型,单master和多chunk server的架构设计。同时加入了多副本机制来保证数据的完整性。并且将数据拆分为64MB,比一般的文件系统的块都要大,这样做的好处在于可以最大程度的利用网络带宽。在数据写入方面,并没有使用覆盖的方式,而是使用了追加操作,这样做的性能更高。
- GFS解决了什么问题?
- GFS解决了大规模数据存储和处理的问题,特别是在Google的分布式计算环境中。它旨在提供高可用性、容错性和高性能的分布式文件系统,以支持大规模数据处理工作负载。
- GFS系统架构设计?
- GFS采用了主从架构,由一个Master节点和多个Chunk Server节点组成。Master负责维护文件元数据和命名空间,而Chunk Server存储实际的数据块。
- 在GFS中,对一个不存在的文件写入会发生什么?对一个存在的文件进行写入是什么样子的?
- 对不存在的文件写入将创建该文件。对已存在的文件进行写入将追加数据到文件末尾,采用的是append操作,而不是覆盖。
- 在GFS中,对文件进行读操作的时间复杂度是多少?
- GFS采用了顺序读取和大块数据的方式,使得文件的读取效率很高。时间复杂度主要受到磁盘I/O的影响,通常为O(1)或O(log N)。
- GFS如何拆分大文件?并且如何检索被拆分后的文件?
- GFS将大文件切分成固定大小的块(默认64MB),每个块分配一个唯一的块标识符。文件的元数据中包含了块的顺序和块标识符,从而实现了对文件的拆分和检索。
- 为什么GFS选用64MB作为块单位?
- 64MB的块大小在提高数据读写效率和最大程度利用网络带宽之间取得了平衡。大块减少了元数据的负担,同时也降低了网络开销。
- 版本号的作用是什么?为什么需要版本号?
- 版本号用于标识每个数据块的版本,确保数据的一致性。版本号的引入使得GFS能够区分最新的块副本,并在发生错误或冲突时进行正确的处理。
- 为什么GFS使用append?
- GFS使用append操作而不是覆盖写,这种方式更适合Google的工作负载,特别是在大规模数据追加的场景下,避免了多个写操作之间的冲突和同步问题。
- 客户端本地缓存如何保证数据一致性?
- GFS客户端本地缓存采用了“读多写少”的策略,不强制要求缓存与服务器的数据保持一致。在写入操作后,客户端通过通知Master来使得缓存无效,从而保证一致性。在读操作时,客户端会检查缓存是否过期或无效,必要时会从服务器重新获取数据。