1、GFS的设计
GFS(Google File System)是一个可扩展的分布式文件系统,用于大型的、分布式的、对大量数据进行访问的应用。它运行于廉价的普通硬件上。GFS 提供了一套类似传统文件系统的 API 接口函数,虽然并不是严格按照 POSIX 等标准 API 的形式实现的。文件以分层目录的形式组织,用路径名来标识。我们支持常用的操作,如创建新文件、删除文件、打开文件、关闭文件、读和写文件。 另外,GFS 提供了快照和记录追加操作。快照以很低的成本创建一个文件或者目录树的拷贝。记录追加操作允许多个客户端同时对一个文件进行数据追加操作,同时保证每个客户端的追加操作都是原子性的。
(1)GFS的整体架构
一个 GFS 集群包含一个单独的 Master 节点、多台 Chunk 服务器,并且同时被多个客户端访问,如图所示。所有的这些机器通常都是普通的 Linux 机器,运行着用户级别(user-level)的服务进程。
- GFS 存储的文件都被分割成固定大小的 Chunk。在 Chunk 创建的时候,Master 服务器会给每个 Chunk 分配一个不变的、全球唯一的 64 位的 Chunk 标识。Chunk 服务器把 Chunk 以 Linux 文件的形式保存在本地硬盘上,并且根据指定的 Chunk 标识和字节范围来读写块数据。出于可靠性的考虑,每个块都会复制到多个块服务器上。缺省情况下,我们使用 3 个存储复制节点,不过用户可以为不同的文件命名空间设定不同的复制级别。
- Master 节点管理所有的文件系统元数据。这些元数据包括名字空间、访问控制信息、文件和 Chunk 的映射信息、以及当前 Chunk 的位置信息。Master 节点还管理着系统范围内的活动,比如,Chunk 租用管理、孤儿 Chunk的回收、以及 Chunk 在 Chunk 服务器之间的迁移。Master 节点使用心跳信息周期地和每个 Chunk服务器通讯,发送指令到各个 Chunk 服务器并接收 Chunk 服务器的状态信息。 GFS 客户端代码以库的形式被链接到客户程序里。
- 客户端代码实现了 GFS 文件系统的 API 接口函数、应用程序与 Master 节点和 Chunk 服务器通讯、以及对数据进行读写操作。客户端和 Master 节点的通信只获取元数据,所有的数据操作都是由客户端直接和 Chunk 服务器进行交互的。我们不提供 POSIX 标准的 API 的功能,因此,GFS API 调用不需要深入到 Linux vnode 级别。
(2)单一 Master 节点
单一的 Master 节点的策略大大简化了我们的设计。单一的 Master 节点可以通过全局的信息精确定位Chunk 的位置以及进行复制决策。另外,我们必须减少对 Master 节点的读写,避免 Master 节点成为系统的瓶颈。客户端并不通过 Master 节点读写文件数据。反之,客户端向 Master 节点询问它应该联系的 Chunk 服务器。客户端将这些元数据信息缓存一段时间,后续的操作将直接和 Chunk 服务器进行数据读写操作。 读写流程如下:
- 首先,客户端把文件名和程序指定的字节偏移,根据固定的 Chunk 大小,转换成文件的 Chunk 索引。
- 然后,它把文件名和 Chunk 索引发送给 Master 节点。Master 节点将相应的 Chunk 标识和副本的位置信息发还给客户端。客户端用文件名和 Chunk 索引作为 key 缓存这些信息。
- 之后客户端发送请求到其中的一个副本处,一般会选择最近的。请求信息包含了 Chunk 的标识和字节范围。在对这个 Chunk 的后续读取操作中,客户端不必再和 Master 节点通讯了,除非缓存的元数据信息过期或者文件被重新打开。
实际上,客户端通常会在一次请求中查询多个 Chunk 信息,Master 节点的回应也可能包含了紧跟着这些被请求的 Chunk 后面的 Chunk 的信息。在实际应用中,这些额外的信息在没有任何代价的情况下,避免了客户端和 Master 节点未来可能会发生的几次通讯。
(3)Chunk 尺寸
Chunk 的大小是关键的设计参数之一。我们选择了 64MB,这个尺寸远远大于一般文件系统的 Block size。每个 Chunk 的副本都以普通 Linux 文件的形式保存在 Chunk 服务器上,只有在需要的时候才扩大。惰性空间分配策略避免了因内部碎片造成的空间浪费,内部碎片或许是对选择这么大的 Chunk 尺寸最具争议一点。
选择较大的 Chunk 尺寸有几个重要的优点:
- 首先,它减少了客户端和 Master 节点通讯的需求,因为只需要一次和 Mater 节点的通信就可以获取 Chunk 的位置信息,之后就可以对同一个 Chunk 进行多次的读写操作。这种方式对降低我们的工作负载来说效果显著,因为我们的应用程序通常是连续读写大文件。即使是小规模的随机读取,采用较大的 Chunk 尺寸也带来明显的好处,客户端可以轻松的缓存一个数 TB 的工作数据集所有的 Chunk 位置信息。
- 其次,采用较大的 Chunk 尺寸,客户端能够对一个块进行多次操作,这样就可以通过与 Chunk 服务器保持较长时间的 TCP 连接来减少网络负载。
- 第三,选用较大的 Chunk 尺寸减少了 Master节点需要保存的元数据的数量。这就允许我们把元数据全部放在内存中。
另一方面,即使配合惰性空间分配,采用较大的 Chunk 尺寸也有其缺陷:
- 小文件包含较少的 Chunk,甚至只有一个 Chunk。当有许多的客户端对同一个小文件进行多次的访问时,存储这些 Chunk 的 Chunk 服务器就会变成热点。在实际应用中,由于我们的程序通常是连续的读取包含多个 Chunk 的大文件,热点还不是主要的问题。
- 然而,当我们第一次把 GFS 用于批处理队列系统的时候,热点的问题还是产生了:一个可执行文件在GFS 上保存为 单一的chunk 文件,之后这个可执行文件在数百台机器上同时启动。存放这个可执行文件的几个 Chunk 服务器被数百个客户端的并发请求访问导致系统局部过载。我们通过:1)使用更大的复制参数来保存可执行文件;2)以及错开批处理队列系统程序的启动时间的方法解决了这个问题;3)一个可能的长效解决方案是,在这种的情况下允许客户端从其它客户端读取数据。
(4)元数据
Master 服务器存储 3 种主要类型的元数据,包括:文件和Chunk的命名空间、文件和Chunk的对应关系、每个Chunk 副本的存放地点。所有的元数据都保存在 Master 服务器的内存中。前两种类型的元数据同时也会以记录变更日志的方式记录在操作系统的系统日志文件中,日志文件存储在本地磁盘上,同时日志会被复制到其它的远程Master 服务器上。采用保存变更日志的方式,我们能够简单可靠的更新 Master 服务器的状态,并且不用担心 Master 服务器崩溃导致数据不一致的风险。Master 服务器不会持久保存 Chunk 位置信息。Master服务器在启动时,或者有新的 Chunk 服务器加入时,向各个Chunk 服务器轮询它们所存储的Chunk 的信息。
1)内存中的数据结构:
因为元数据保存在内存中,所以 Master 服务器的操作速度非常快。并且,Master 服务器可以在后台简单而高效的周期性扫描自己保存的全部状态信息。这种周期性的状态扫描也用于实现 Chunk 垃圾收集、在 Chunk服务器失效的时重新复制数据、通过 Chunk 的迁移实现跨 Chunk 服务器的负载均衡以及磁盘使用状况统计等功能。 将元数据全部保存在内存中的方法有潜在问题:Chunk 的数量以及整个系统的承载能力都受限于 Master服务器所拥有的内存大小。但是在实际应用中,这并不是一个严重的问题。Master 服务器只需要不到 64 个字节的元数据就能够管理一个 64MB 的 Chunk。由于大多数文件都包含多个 Chunk,因此绝大多数 Chunk 都是满的,除了文件的最后一个 Chunk 是部分填充的。同样的,每个文件的在命名空间中的数据大小通常在 64字节以下,因为保存的文件名是用前缀压缩算法压缩过的。 即便是需要支持更大的文件系统,为 Master 服务器增加额外内存的费用是很少的,而通过增加有限的费用,我们就能够把元数据全部保存在内存里,增强了系统的