Google File System 论文解读

1,416 阅读11分钟
原文链接: icell.io

Google File System 设计目标

  1. GFS 是运行在许多廉价的硬件设备之上的,每个设备都可能面临各种各样的问题,设备出现问题应该被认为是常态,所以 GFS 应该是被设计为自动容错的;
  2. GFS 主要被设计用来存储一定数量的大文件(>=100 MB),但是也必须要支持小文件,只是无需针对小文件存储做专门的优化;
  3. GFS 上主要会出现两种读操作,大规模的 streaming read 和小规模的 random read,如果想针对随机读进行优化的话,可以对他们进行合并排序再按照顺序批量进行读取;
  4. GFS 上主要处理大规模、顺序、数据追加方式的写操作,极少有随机写入的需求,同时要求能够高效处理多个客户端并发的对同一个文件进行数据追加操作。

Google File System 架构

GFS 同传统的文件存储系统相比是很类似的,在传统文件存储系统中,会将文件分为许多个 block,每个 block 差不多是 4kb-64kb 级别的大小,同时会专门有一块区域存储文件的 metadata 信息,以及关于每个 block 存储信息的索引,这样有了索引信息就能够知道如果去相应的 block 查找相应的数据。

而在 GFS 中,无法再按照 block 这么小的单元进行划分,而是按照 64MB 为单元来分成许多个 chunk,每个 chunk 都有一个唯一的 id 进行标识,chunk 会有专门的 Chunk Server 来进行存储,另外会有一个 Master 节点,存储系统的 metadata,包括 namespace,chunk 的映射信息和位置信息等,以及负责类似于垃圾 chunk 的回收以及 chunk 在 Chunk Server 之间迁移等工作。

以下是官方给出的架构图:

架构图中给出了三种角色,Client、Master 和 Chunk Server,三者之间的交互关系是:

  1. Client 将文件名以及要访问的 chunk index 发送给 Master,其中 chunk index 是客户端自己根据要访问的文件大小和以 64MB 为一个 chunk 的基准计算出来的;
  2. Master 接收到之后,在 metadata 中查找到对应的 chunk 所在的 Chunk Server 以及 replica 的信息返回给 Client,Client 缓存这些信息;
  3. Client 之后请求数据,直接根据之前获取到的 Chunk Server 信息同 Chunk Server 直接通讯,而不再需要和 Master 进行通讯,除非缓存的数据过期。这里,Client 一般会同最近的 Chunk Server 进行通讯。
  4. Master 和 Chunk Server 之间是有心跳进行连接的,通过心跳让 Master 知晓如何对 Chunk Server 进行调度。

在这里,有几点细节部分需要额外说明:

chunk 的大小问题

GFS 中每一个 chunk 被设定为 64MB,远远大于传统文件系统中的 block 大小,这是一个 GFS 的关键设计参数之一。因为 GFS 主要面临的是大文件的处理,对于一个 TB 级别大小的文件,因为 chunk 比较大,所以分出来的所有 chunk 位置信息也就减少了,这样 Master 和 Client 需要保存的信息大小都会比较小。

另外,GFS 中采用了 Lazy Space Allocation 的策略,也就是说实际的空间分配被尽可能地推迟。在 GFS 中,最终的物理上的空间分配取决于写入的数据大小,数据会先写入到缓存中,然后再执行 chunk 的写入操作,这时会根据 chunk 的 64M 大小以及要写入的数据大小来决定是否进行物理空间的分配(这在下面的写入操作中会介绍)。

但是,较大的 chunk 也会出现问题,如果存储一个小文件包含了较少的 chunk 设置只有一个 chunk 的话,当出现许多的 Client 对这一个小文件进行多次访问,那么存储这个 chunk 的 Chunk Server 就很容易成为热点。但是 GFS 经常处理的是包含多个 chunk 的大文件,所以这个并不是最主要要解决的问题。

Master 节点数据存储问题

由于 Master 节点保存着所有 Chunk Server 的 metadata 数据,所以它很容易成为一个读写瓶颈。但实际上在 GFS 的设计上,尽量简化了 Master 同 Client 的交互,在一次读操作中,Client 和 Master 可以做到只有一次交互。而且,Master 所有的数据都是直接使用内存进行存储,且对于一个 64MB 的 chunk 只需要不到 64kb 的空间就能进行管理,因此数据读取速度也有保障。

另外,Master 节点不会持久化 Chunk Server 的位置信息,它只会在启动的时候轮询各个 Chunk Server 来获取这类信息,并在之后定期轮询更新这类信息,这样不仅简化了 Master 和 Chunk Server 的数据同步问题,还能保证 Master 所持有的信息始终是最新的。

Client 无需缓存 chunk 数据

Client 在同 Master 交互后,获取到 Chunk Server 的 metadata 数据,然后客户端会缓存这些数据,之后通过这些数据来直接同 Chunk Server 进行交互。这里要特别说明的是,Client 从 Chunk Server 获取数据之后不会对数据做缓存。如果要增加缓存逻辑,那就要处理 Client 间的数据一致性问题,无非要增加系统的处理逻辑。另外,Client 更多处理的是 streaming read 和 append write,这样的读写方式采用缓存数据并不会带来更多的优势。

Google File System 如何进行写入操作

为了保证数据不丢失,GFS 的每个 Chunk Server 都做了冗余,会有 3 个副本。当发生写入操作(包括 over-write 和 append 两种操作)的时候,Master 节点会同 Chunk Server 的其中一个副本之间建立 lease 操作,这个副本就成为了 Primary Chunk Server,而其他的副本是 Secondary Chunk Server。每个 lease 操作都会有超时时间,为 60 秒,但是 Primary Chunk Server 可以通过和 Master 之间的心跳来延长租约时间。每当 Master 同某个 Chunk Server 建立 lease 之后,Chunk Server 都会将自己所维护的 Version Number 加一,这样配合 Master 进行过期失效的副本检测。Master 赋予 lease 之后,同样也可以执行 revoke 操作。

知晓了 lease 机制后,来看一下 GFS 是如何进行写入操作的。下图是 Client 发起写入操作的流程图(图中序号和下文并无对应关系):

  1. Client 同 Master 发起写入操作,Master 进行 Primary Chunk Server 的选取(如果没有的话),将所有的 Chunk Server 信息返回给 Client;
  2. Client 并不是同 Primary Chunk Server 直接进行通信,而是采用一种叫 daisy chain 的网络拓扑方式,先找到所有 Chunk Server 副本中距离 Client 最近的一个,进行数据推送,然后由那个副本再选择下一个距离自己最近的副本进行数据推送,直到数据到达所有的副本。这里另外要注意的一点是,数据推送到 Chunk Server 之后,并没有进行写入操作,而只是进行了缓存操作;
  3. 当所有的副本都已经收到数据之后,告知 Client,此时,Client 会向 Primary Chunk Server 发送写入操作请求;
  4. Primary Chunk Server 接收到请求之后,为该操作分配序号,因为可能会有多个 Client 发起写入请求,通过序号的分配来保证操作按照顺序进行;
  5. Primary Chunk Server 先按照序号在本地执行写入操作,然后将写请求发送到所有的 Secondary Chunk Server 中,所有的 Secondary Chunk Server 按照序号进行顺序执行,执行完成后告知 Primary Chunk Server 操作完成;
  6. Primary Chunk Server 回复 Client,写入成功。

当发生 append 的操作时,因为每一个 Chunk 的体积都是 64M,所以肯定会出现要写入的 Chunk 大小不满足提交的数据大小。发生这种情况时,Primary Chunk Server 会将自己填充满至 64MB,然后通知别的 Secondary Chunk Server 进行填充(填充的数据是会提供方法进行辨识),然后再告诉 Client 此次写入操作不成功,需要重新请求。写入的数据大小同样会有要求,控制在 Chunk 大小的 1/4 之内。

另外,如果在 append 过程中,任意一个 Chunk Server 副本写入失败,都会以为着整个写入操作是失败的,这时会告知 Client 写入失败,Client 可以重新请求进行操作。此时,就会出现一个问题,就是各个 Chunk Server 副本中的数据是不同的,设置是同一个 Chunk Server 中的数据会有重复的。但是,GFS 是允许这种情况出现,它的设计原则只能保证每次写入的数据都会原子性地被写入至少一次(at least once)。这里很难确保 append 操作时 exactly once 的,因为这就要求 Primary Chunk Server 维护一套能够识别出重复数据的状态,并且需要将这个状态在多个副本之间保持一致,而这样对复杂性要求就大大提高了。

这样就要求数据读取方能够在应用级别容忍或者处理重复的数据,如果实在不能接受这样的情况出现,需要在追加的记录中增加类似于 unique ID 这样的辨识机制。

Google File System 的数据一致性

在 GFS 中,如果发生了类似于 metadata 的这种 namespace 级别的数据更改,是仅仅在 Master 中发生的,所有对 Master 中存储的 metadata 进行修改的操作都会有操作日志,GFS 会同步的将操作日志写入到磁盘中,成功后才会继续进行进一步操作。这样由单一的 Master 节点来保证 metadata 的修改操作是原子的。

当发生对 Chunk 中存储的数据进行修改(包含对数据 over-write 和 append 两种操作)时,就涉及到不同副本、并发等多种情况,所以最终的结果需要取决于操作类型、是否成功以及是否时同步修改。在 GFS 中定义了几种表征数据一致性的状态:

  • consistent:所有 Client 不管从哪个 Chunk Server 的副本读取数据,读到的都是一样的;
  • defined:Client 完全了解已写入的数据信息,包含了数据写入的偏移量、数据内容以及数据在所有副本中都写入成功;

当发生串行 over-write 操作时

Client 指定文件以及所要更新的偏移量,并进行串行操作时,且写入能够在所有副本之星成功,那么这次的写入操作在多个副本中时数据一致的,对 Client 来说结果也是明确的,这种操作为 defined;

当发生并行 over-write 操作时

多个 Client 由于并发操作,无法决定写入顺序,只能够由 Primary Chunk Server 来决定写入顺序,此时写入成功后,Client 无法确定最终写入结果的位置,对于 Client 来说是不明确的信息,但是因为更新数据成功所有的副本数据时一致的,因此这种操作为 consistent;

当发生 append 操作时

Primary Chunk Server 决定 append 操作的位置,在写入成功之后,将写入的偏移量告知 Client,因为 Client 明确写入的信息,因此是 defined,但是结合之前的写入操作看,各个副本之间的数据是不能保证完全一致的,所以是 inconsistent 的。

Master 故障恢复

GFS 的单一 Master 节点设计大大简化了整个系统的复杂度,但是这也带来了单点故障的风险,因为 Master 存储了所有的 metadata 信息,一旦 Master 整个没了,那就意味着整个系统就彻底崩溃了,所以,Master 的高可用是至关重要的。每当 Client 发起请求时,Master 都会在节点中记录日志,并有 checkpoint 操作,另外 Master 所有的操作日志和 checkpoint 文件都做了冗余操作,这样保证 Master 在故障之后能够快速恢复。

当 Master 发生故障后,会立即执行 reboot 操作:

  1. 从磁盘中读取日志进行 replay,包括 namespace 信息和 file-chunk 的映射信息;
  2. 轮询所有的 Chunk Server,获取信息,并根据 Chunk Server 持有的版本号进行操作;
  3. 如果 Chunk Server 版本号过低,将其标记为 stale 状态;如果 Chunk Server 版本号是最新的,则采用该版本号;

此外,GFS 中还存在着一些“影子” Master,它们会同主 Master 一样,不仅读取当前操作日志,也会从 Chunk Server 进行数据轮询,在主 Master 挂了之后提供文件系统的只读访问。

以上便是我对 Google File System 这篇文论的部分总结,论文中还涉及到了数据的 Checksum 机制,Master 节点 Snapshot 方式等,这里不再重点关注。

参考资料:

  1. The Google File System
  2. LEC 3, FAQ, MIT 6.824
  3. GFS: The Google File System, CS 6464