
本期论文:The Google File System
Google File System(GFS)是一种适用于大型分布式数据密集型应用的可扩展分布式文件系统。它在大量普通机器上运行并提供容错能力,并为大量客户端提供高性能。
GFS设计原则
GFS集群架构

一个GFS集群由一个Master节点和若干个Chunk Server节点组成,可以被若干个客户端访问。每个节点作为用户级进程运行在Linux机器上。
在存储文件时,GFS会把文件切分成64MB的Chunk进行存储。Chunk由一个Master分配的不可改变且全局唯一的64位Chunk句柄进行标识。Chunk Server在本地磁盘上存储Chunk,通过指定Chunk句柄和字节范围读取或写入Chunk。为了保证可靠性,每一个Chunk会被备份到多个Chunk Server上,通常情况下存储三份Replica。
Master负责维护整个集群的元数据,包括文件和Chunk的命名空间、访问控制信息、文件与Chunk的映射和Chunk Replica的位置信息。此外,Master还负责Chunk的租期管理、回收和迁移。Master也会周期地通过心跳包和Chunk Server进行通信,以此收集Chunk Server的状态并向其发送指令。
客户端与Master交互获得集群的元数据。当客户端需要进行数据读写时,不会通过Master直接进行,而是询问Master应该与哪些Chunk Server通信,然后直接与Chunk Server通信进行数据读写,以此避免Master成为整个集群数据传输的瓶颈,同时,客户端会在一定时间内缓存Chunk Server信息,后续操作可以直接与Chunk Server进行通信。
Chunk大小的选取
通常情况下,Chunk大小为64MB,这比典型的文件系统block大小大得多,可以通过惰性空间分配策略,来避免因内部碎片造成的空间浪费。
选择较大的Chunk具有以下优点:
不过,较大的Chunk会使得小文件占据额外的存储空间。此外,小文件通常只会占据一个Chunk,当大量客户端访问这个小文件时,这个Chunk容易成为系统的负载热点。可以通过增加这类文件的Replica数量来进行负载均衡,更好的方法是允许客户端从其它客户端读取数据。
GFS元数据管理
GFS元数据全部保存在Master的内存中,主要包括以下三类信息:
元数据保存在Master内存中使得Master要对元数据作出变更变得极为容易;同时,这也使得Master可以简单高效地周期性扫描整个集群的状态,以实现Chunk回收、迁移、均衡等操作。唯一的不足在于这使得整个集群所能拥有的Chunk数量受限于Master的内存大小,然而实际中不必担心这种情况,因为一个64MB的Chunk,只需要保存64KB的元数据,况且提高Master内存容量的成本也很低。
Master会把前两类信息以日志形式持久化存储在Master的本地磁盘上,并在在其他机器上备份,但是不会持久化保存Chunk Replica的位置信息,而是在集群启动时由Master询问各个Chunk Server其当前所有的Repica。这样做可以省去由于Chunk Server离开集群、更改名字、重启等原因的Master与Chunk Server的同步问题。此后,Master通过心跳包来监控Chunk Server的状态并更新内存中的信息。
为了保证元数据的可用性,Master在对元数据做任何操作前对会用先写日志的形式将操作进行记录,只有当日志写入完成后才会响应客户端的请求,而这些日志也会备份到多个机器上。日志不仅是元数据的唯一持久化记录,也是定义操作执行顺序的时间线。文件、Chunk和他们的版本信息都由他们的创建时间唯一的永久的标识。
Namespace管理
在前面我们已经了解到,Namespace作为 GFS 元信息的一部分会被维持在Master的内存中,由 Master负责管理。在逻辑上,Master并不会根据文件与目录的关系以分层的结构来管理这部分数据,而是单纯地将其表示为从完整路径名到对应文件元数据的映射表,并在路径名上应用前缀压缩以减少内存占用。
为了管理来自不同客户端的并发请求对Namespace的修改,Master 、会为Namespace中的每个文件和目录都分配一个读写锁(Read-Write Lock)。由此,对不同Namespace区域的并发请求便可以同时进行。
所有Master操作在执行前都会需要先获取一系列的锁:通常,当操作涉及某个路径 /d1/d2/.../dn/leaf时,Master会需要先获取从/d1、/d1/d2到/d1/d2/.../dn的读锁,然后再根据操作的类型获取 /d1/d2/.../dn/lead的读锁或写锁。获取父目录的读锁是为了避免父目录在此次操作执行的过程中被重命名或删除。
由于大量的读写锁可能会造成较高的内存占用,这些锁会在实际需要时才进行创建,并在不再需要时被销毁。此外,所有的锁获取操作也会按照一个相同的顺序进行,以避免发生死锁:锁首先按Namespace树的层级排列,同一层级内则以路径名字典序排列。
Chunk租约(lease)和变更顺序
GFS使用租约(lease)机制来保持多个副本间变更顺序的一致性。在客户端对某个Chunk做出变更时,会把该Chunk的Lease交给某个Replica,使其成为Primary:Primary 会负责为这些变更安排一个执行顺序,然后其他Replica便按照相同的顺序执行这些修改。
设计租约机制的目的是为了最小化Master节点的管理负担。Chunk Lease在初始时会有60秒的超时时间。在未超时前,Primary可以向Master申请延长Chunk Lease的时间;必要时Master也可以直接撤回已分配的Chunk Lease。
GFS集群常见操作流程
文件写入

如果该过程有发生错误,可以认为修改已在 Primary 和部分 Secondary 上成功执行(如果在 Primary 上就出错了,那么写请求不会被转发出去)。此时可以认为此次修改操作没有成功,因为数据会处于不一致的状态。此时,客户端会重新尝试执行此次操作。
值得注意的是,这个流程特意将数据流与控制流分开:客户端先向Chunk Server提交数据,再将写请求发往Primary。这么做的好处在于GFS能够更好地利用网络带宽资源。
从上述步骤可见,控制流借由写请求从客户端流向Primary,再流向其他Secondary Replica。而数据流以一条数据管道进行传递的:客户端会把数据上传到离自己最近的Replica,该 Replica在接收到数据后再转发给离自己最近的另一个Replica,如此递归直到所有Replica都接收到数据,如此一来便能充分利用每台机器的带宽,避免网络瓶颈和高延时的连接,最小化推送所有数据的延时。
文件追加
文件追加操作的过程和写入的过程有几分相似:
当追加操作在部分Replica上执行失败时,Primary会响应客户端,通知它此次操作已失败,客户端便会重试该操作。和写入操作的情形相同,此时已有部分Replica顺利写入这些数据,重新进行数据追加便会导致这一部分的Replica上出现重复数据,不过GFS的一致性模型也确实并未保证每个Replica都会是完全一致的。
GFS只确保数据会以一个原子的整体被追加到文件中至少一次。由此我们可以得出,当追加操作成功时,数据必然已被写入到所有Replica的相同偏移位置上,且每个Replica的长度都至少超出此次追加的记录的尾部,下一次的追加操作必然会被分配一个比该值更大的偏移值,或是被分配到另一个新的块上。
文件快照
GFS 提供了文件快照操作,可为指定的文件或目录创建一个副本。
快照操作的实现采用了写时复制(Copy on Write)的思想:
文件读取

文件删除
当用户对某个文件进行删除时,GFS 不会立刻删除数据,而是采用惰性策略,在文件和Chunk两个层面上对数据进行移除。
首先,当客户端删除某个文件时,Master像对待其它修改操作一样,立刻把删除操作以日志的方式记录下来。但Master并不会立刻从Namespace中将其删除,而是将该文件重命名为另一个包含删除时时间戳的隐藏的名称。在Master周期扫描Namespace时,它会删除所有三天前的隐藏文件。在文件被彻底从Namespace删除前,客户端仍然可以利用这个重命名后的隐藏名称读取该文件,甚至再次将其重命名以撤销删除操作。
在对Chunk Namespace做类似的常规扫描时,Master找到孤儿Chunk(不被任何文件包含的 Chunk)并删除它们的元数据。在Chunk Server和Master进行的周期心跳通信中,Chunk Server会汇报自己所持有的Chunk Replica,此时Master便会告知Chunk Server哪些Chunk已不存在于元数据中,Chunk Server 则可自行移除对应的Replica。
采用这种删除机制主要有如下三点好处:
不过,如果在存储资源较为稀缺的情况下,用户对存储空间使用的调优可能就会受到该机制的阻碍。为此,GFS允许客户端再次删除该文件,以加速从Namespace移除该文件。除外,GFS 还可以让用户为Namespace中不同的区域指定不同的备份和删除策略。
Replica管理
Chunk的Replica位置选取主要有两个目标:最大化数据可靠性和可用性,以及最大化网络带宽利用率。为此,Replica不仅需要被保存在不同的机器上,还需要被保存在不同的机架上,这样如果整个机架不可用了,数据仍然得以存活。如此一来,不同客户端对同一个Chunk进行读取时便可以利用不同机架的出口带宽,但坏处就是进行写入时数据会在不同机架间流转,不过在GFS的设计者看来这是个合理的trade-off。
Chunk Replica可以由Chunk的创建、重新备份和重新负载均衡三种事件创建。
Chunk的创建
在Master创建一个新的Chunk时,首先它会需要考虑在哪放置新的Replica。Master会考虑如下几个因素:
Chunk的重备份
当某个Chunk的Replica数量低于用户指定的阈值时,Master就会对该Chunk进行重备份。这可能是由Chunk Server失效、Chunk Server存储的副本损坏、Chunk Server磁盘损坏或是用户提高了Replica数量阈值所触发。
首先,Master会按照以下因素为每个需要重备份的Chunk安排优先级:
Master选取当前拥有最高优先级的Chunk,并指定若干Chunk Server直接从现在已有的Replica上复制数据。Master具体会指定哪些Chunk Server进行复制操作同样会考虑上面提到的几个因素。除外,为了减少重备份对用户使用的影响,Master会限制当前整个集群正在进行的复制操作的数量,同时Chunk Server也会限制复制操作所使用的带宽。
Chunk的负载均衡
Master 会周期地检查每个Chunk当前在集群内的分布情况,并在必要时迁移部分Replica以更好地均衡各节点的磁盘利用率和负载。新Replica的位置选取策略和上面提到的大体相同。此外,Master还需要移除磁盘占用较高的Chunk Server上的Replica,以均衡磁盘使用率。
高可用机制
快速恢复
GFS 的组件被设计为可以在数秒钟内恢复它们的状态并重新启动。GFS的组件实际上并不区分正常退出和异常退出:要关闭某个组件时直接kill进程即可。
Chunk Server
作为集群中的Slave角色,Chunk Server失效的几率比Master要大得多。当Chunk Server失效时,其所持有的Replica对应的Chunk的Replica数量便会降低,当Master发现Replica数量低于用户指定阈值时,会进行重备份。
此外,当Chunk Server失效时,用户的写入操作还会不断地进行,那么当Chunk Server重启后,Chunk Server上的Replica便有可能是过期的。为此,Master会为每个Chunk维持一个版本号,以区分正常的和过期的Replica。每当Master将Chunk Lease分配给一个Chunk Server时,Master便会提高Chunk的版本号,并通知其他的Replica更新自己的版本号。如果此时有 Chunk Server失效,那么它上面的Replica的版本号就不会变化。
在 Chunk Server重启时,Chunk Server会向Master汇报自己所持有的Chunk Replica及对应的版本号。如果Master发现某个Replica版本号过低,过期的Replica便会在下一次的Replica回收过程中被移除。此外,Master向客户端返回Replica位置信息时也会返回Chunk当前的版本号,客户端在执行操作时都会验证版本号以确保不会读取到旧的数据。
Master
Master在对元数据做任何操作前对会用先写日志的形式将操作进行记录,只有当日志写入完成后才会响应客户端的请求,而这些日志也会备份到多个机器上。日志只有在写入到本地以及远端备份的持久化存储中才被视为完成写入。
在重新启动时,Master会通过重放已保存的操作记录来恢复自身的状态。为了保证Master能够快速地完成恢复,Master会在日志达到一定大小后为自身的当前状态创建Checkpoint,并删除Checkpoint创建以前的日志,重启时便从最近一次创建的Checkpoint及其后续的日志开始恢复。Checkpoint以B树的形式进行组织,可以直接映射到内存中,且不做其他额外解析的情况下检索其所存储的Namespace,进一步减少Master恢复所需的时间。
为了简化设计,同一时间只会有一个Master起作用。当Master失效时,外部的监控系统会侦测到这一事件,并在其他地方重新启动新的Master进程。
此外,集群还提供具有只读功能的Shadow Master。它们会同步Master的状态变更,但有可能延迟若干秒,其主要用于为Master分担读操作的压力。Shadow Master会通过读取 Master 操作日志的某个备份来让自己的状态与Master同步;它也会像Master那样,在启动时轮询各个Chunk Server,获知它们所持有的Chunk Replica信息,并持续监控它们的状态。在Master失效时,Shadow Master仍能为整个集群提供只读功能,在Master因创建和删除副本导致副本位置信息更新时,Shadow Master才和Master通信来更新自身状态。
数据完整性
为了保证数据完整,每个Chunk Server都会使用校验和来检测自己保存的数据是否有损坏;在侦测到损坏数据后,Chunk Server可以利用其它Replica来恢复数据。
每个Chunk都分成64KB大小的block。每个block都对应一个32位的校验和。和其它元数据一样,保存在内存和硬盘上,同时也记录操作日志。
当进行数据读取操作时,Chunk Server首先会利用校验和检查所需读取的数据是否有发生损坏,如此一来Chunk Server便不会把损坏的数据传递给其他请求发送者,无论它是客户端还是另一个Chunk Server。发现损坏后,Chunk Server会为请求发送者发送一个错误,并向Master告知数据损坏事件。接收到错误后,请求发送者会选择另一个Chunk Server重新发起请求,而Master则会利用另一个Replica为该Chunk进行重备份。当新的Replica创建完成后,Master便会通知该Chunk Server删除这个损坏的Replica。
当进行数据追加操作时,Chunk Server可以为位于Chunk尾部的校验和块的校验和进行增量式的更新,并且用所有追加来的新的校验和块来计算新的校验和。即使是被追加的校验和块在之前已经发生了数据损坏,增量更新后的校验和依然会无法与实际的数据相匹配,在下一次读取时依然能够检测到数据的损坏。
当进行数据写入操作时,Chunk Server必须读取并校验包含写入范围起始点和结束点的校验和块,然后进行写入,最后再重新计算校验和。
此外,在空闲的时候,Chunk Server也会周期地扫描并校验不活跃的Chunk Replica的数据,以确保某些Chunk Replica即使在不怎么被读取的情况下,其数据的损坏依然能被检测到,同时也确保了这些已损坏的Chunk Replica不至于让Master认为该Chunk已有足够数量的Replica。
数据一致性
首先,命名空间完全由单节点Master管理在其内存中,这部分数据的修改可以通过让Master 为其添加互斥锁来解决并发修改的问题,因此命名空间的数据修改是可以确保完全原子的。
文件的数据修改则相对复杂。在讲述接下来的内容前,首先我们先明确,在文件的某一部分被修改后,它可能进入以下三种状态的其中之一:

在修改后,一个文件的当前状态将取决于此次修改的类型以及修改是否成功。具体来说:
GFS 支持的文件数据修改数据包括两种:指定偏移值的数据写入(Write)以及数据追加(Record Append)。当写入时,指定的数据会被直接写入到客户端指定的偏移位置中,覆盖原有的数据。GFS并未为该操作提供太多的一致性保证:如果不同的客户端并发地写入同一块文件区域,操作完成后这块区域的数据可能由各次写入的数据碎片所组成,即进入不确定的状态。
经过了一系列的成功的修改操作之后,GFS 确保被修改的文件区域是已确定的,并且包含最后一次修改操作写入的数据。GFS通过以下措施确保上述行为:(a)对Chunk的所有副本的修改操作顺序一致,(b)使用Chunk的版本号来检测Replica是否因为它所在的Chunk Server宕机而错过了修改操作而导致其失效。失效的Replica不会再进行任何修改操作,Master Server也不再返回这个Chunk Replica的位置信息给客户端。它们会被垃圾回收系统尽快回收。
对应用的影响
GFS 的一致性模型是相对松散的,这就要求上层应用在使用GFS时能够适应GFS所提供的一致性语义。简单来讲,上层应用可以通过两种方式来做到这一点:更多使用追加操作而不是覆写操作;写入包含校验信息的数据。
GFS针对追加操作做出了显著的优化,这使得这种数据写入方式的性能更高,而且也能提供更强的一致性语义。尽管如此,追加操作at least once的特性仍使得客户端可能读取到填充或是重复数据,这要求客户端能够容忍这部分无效数据。一种可行的做法是在写入时为所有记录写入各自的校验和,并在读取时进行校验,以剔除无效的数据;如果客户端无法容忍重复数据,客户端也可以在写入时为每条记录写入唯一的标识符,以便在读取时通过标识符去除重复的数据。
参考文献
Ghemawat S, Gobioff H, Leung S T. The Google file system[C]. Proceedings of the 19th ACM Symposium on Operating Systems Principles. 2003: 29-43.
https://mr-dai.github.io/gfs/...
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69957675/viewspace-3147155/ ,如需转载,请注明出处,否则将追究法律责任。