YoloKokura

原论文:The Google File System

设计架构

GFS的设计建立在如下几个观察到的现象或假设上:

  • 不论什么原因,组件崩溃必然会出现,所以,监控、容错、自愈是系统必备的能力;
  • 存储几个集中的大文件比存储许多个小文件的情况更加常见,两者都要支持,但前者要额外优化;
  • 大批量的流式读比小规模的随机读更常见,关注性能的应用基本都会把小规模的读操作组织到一起,减少来回读取的开销;
  • 追加写比覆写更加常见,一旦写入,这个位置的数据就很少再被修改了;
  • 需要支持并发追加写的使用场景;
  • 高带宽的优先级高于低时延,如果要做选择,大多数应用倾向于能更快地处理数据,而非快速响应某一个读/写请求。

如图所示,GFS集群是一个单主(master)多从(chunkserver)的结构,支持多用户同时接入。 chunk指的是文件中固定大小的一段,master在其初始化阶段会给它一个全局唯一ID(chunk handle)。 chunkserver会在本地Linux文件系统中存储chunk,当然每个chunk会在多台chunkserver上留有备份。 master保存文件系统的元数据,包括命名空间、准入信息、文件和chunk的映射关系,以及chunk的存储情况。

master不负责文件读写,只是给client分配负责的chunkserver,后者把该信息缓存下来方便后续操作。

举例来说,读操作可以拆解为如下步骤:

  1. client根据固定的chunk大小,将文件名、读取位置翻译为该文件内的chunk索引;
  2. client向master发送请求,询问该文件名和chunk索引所在的chunkserver;
  3. master返回chunk句柄和对应的chunkserver(含冗余副本位置);
  4. client缓存该信息,和其中的一个chunkserver进行交互,不再需要master介入。

为了减轻master压力,client也可以一次性向master查询多个chunk的位置,后续操作便可以绕过master。

master在启动时、心跳时会从chunkserver处取得chunk位置。另外,集群元数据的变动会被记录在一个日志中,类似于Redis或之前Raft的机制,master在checkpoint后能够重放日志以恢复自身状态。而每当日志增长到指定规模,master都将更新一次checkpoint。此时,为了避免阻塞,master会新建一个日志文件,新开线程保存旧日志的checkpoint。

一致性模型

原论文中关于数据一致性有这样几个概念:

  • consistent:所有的client都能观察到一样的数据,不论连接的是哪台chunkserver;
  • defined:数据首先能保证consistent,且client能看到完整的数据变化(而不是并发写造成的片段,这种情况下,多个写操作的片段会混杂在一起)。

为了保证并发写时的数据行为是defined,GFS要确保至少一次原子写,它定义的offset会被返回给client,而非使用后者认为的offset。因此,GFS中chunk并非是紧凑的,中间可能会有padding或者重复记录。

GFS确保在每个chunkserver上,chunk的变更顺序都是一致的,并且使用版本号来识别脏chunk,这种chunk会被垃圾回收掉,不会交给client或参与后续变更。

系统交互

一次数据变更会发生在所有与之相关的chunkserver上,为了保证这些chunkserver上的变更数据顺序一致,GFS还引入了primary的概念。我们可以把拥有同一个chunk的chunkserver视为一个小集群。master会给其中一个chunkserver颁发租约,使其成为primary。primary会决定并发数据变更发生的顺序,其它chunkserver会按照这个顺序记录变更。

以上图为例,一次写操作的控制流是这样的:

  1. client咨询master,某特定的chunk在哪些chunkserver上。master会返回primary(目前拥有租约的chunkserver)和其他从chunkserver。如果目前没有chunkserver拿到租约,master会在此刻发放租约;
  2. client缓存master返回的信息,只有primary失去联系后它才会再次访问master;
  3. client将写操作的数据以任何顺序发送到所有的chunkserver,chunkserver会把数据缓存到LRU中;
  4. 所有的chunkserver都确认收到了数据后,client再向primary发送和先前数据相对应的写操作请求。primary会给它收到的并发写请求排序,并在本机实施写操作;
  5. primary将写操作转发给所有的从chunkserver,以确保大家的顺序都是一致的;
  6. 从chunkserver执行完了写操作,向primary确认;
  7. primary告知client操作的执行结果。如果出现错误,client会重试3-7步,因为错误大概率发生在从chunkserver处,如果实在不行,会从头开始重试。

GFS的数据流和控制流是解耦的,即第三步数据的发送。为了最大程度利用网络带宽,每台机器都会向网络拓扑中最近的,还没有收到数据的机器转发数据。

Record Append

GFS提供一种record append的原子化追加操作,即client只提供数据,GFS决定offset位置,就像Unix中的O_APPEND模式。 否则,并发的writer就需要额外的锁机制来避免竞态条件。

要实现record append,前文描述的控制流中需要做一些额外改动:在第5步中,primary会检查追加数据是否会导致chunk超出限制,如果会,primary干脆将chunk pad到最大大小,并令其它chunkserver也这么做。在此之后,它通知client,写操作必须在下一个chunk上重试。

在从chunkserver上出现写失败,需要重试时,会继续在原数据基础上追加重试数据。因此,一个chunk中很可能出现多端重复数据,而GFS并不保证所有的chunkserver能够在字节级别做到完全一致,只保证至少有一次原子写。

master涉及的操作

命名空间管理

为了支持多种操作并发进行,GFS采用命名空间和锁来确保同一位置的操作序列进行。 GFS并不具备文件系统的概念,它使用简单的lookup table将路径名和元数据进行映射。 master的每次操作都需要获取对应的锁,如:操作对象是d1/d2/d3/obj,则需要获取d1、d1/d2、d1/d2/d3的读锁,并根据操作类型获取d1/d2/d3/obj的读锁或者写锁。

副本管理

master在创建一个chunk,并决定分配它时会考虑如下几个因素:

  1. 要确保chunkserver的磁盘利用率大致均等;
  2. 要限制每台chunkserver上最近新建chunk的数量,因为新的chunk往往意味着大量的写操作;
  3. 要尽量将chunk的副本分散到不同机架上。

当一个chunk的副本低于HA阈值,master就需要重新复制一个副本并分配,要考虑的因素和前面新建chunk时一致。

最后,master会周期性地再分配chunk,以平衡磁盘利用和负载均衡。

垃圾回收

GFS的GC策略很简单:master不知道的chunk都要被回收。

当一个文件被删除时,master仅仅记录一条日志,将文件名更改为带有删除操作时间戳的隐藏名,而非马上回收对应的资源。 master在周期性扫描命名空间时,会将超过三天的这类文件清除。另外,在类似的扫描过程中,master会清理所有孤儿chunk(没有与之对应的文件的chunk)和与之对应的元数据。在心跳机制中,chunkserver会被指示删除没有元数据的chunk。

Tags: