GFS 的主要需求
在论文的一开始首先提出了系统的需求:
- 组件失效是正常的事情而不是异常 (exception),因为在 GFS 中使用了成百上千台普通的机器。所以需要持续的检测错误,并且能够自动恢复。
- 文件巨大,数个 GB 的文件是非常常见的。
- 大多数文件的修改 (mutation) 是以追加 (append) 为主而不是在覆盖 (overwrite)。文件一旦写入完成,之后只会有顺序的读操作。
- 支持原子追加操作,多个客户端可以同时对文件进行追加而不需要额外的同步。
- 高带宽比低延迟更加重要。
GFS 的设计
结构
GFS 集群包括了单个 Master 和多个 Chunkserver,他们会作为用户级程序运行在 Linux 系统上。
文件在存储时会划分成固定大小的 chunks,每一个 chunk 都是通过由 Master 分配的一个不可变且全局唯一的 64bitchunk handle 来标识。chunkserver 通过 Linux 文件读写的方式来处理 chunk。每个 chunk 在多个 chunkserver 中储存来提高可靠性。
master 维护整个文件系统的元数据,包括了 namespace (文件名)、access control (权限)、从文件到 chunk 的映射表以及每个 chunk 的位置。master 还负责了系统级任务比如 chunk lease 管理 (后文会涉及)、垃圾回收、chunkserver 之间的 chunk 迁移。master 通过心跳包 (Heartbeat) 与 chunkserver 进行通信来下达指令或收集状态。
client 通过向 master 通信来获得文件的原数据,但是所有的数据都是直接传输到 chunkserver。
client 和 chunkserver 都不缓存数据,因此简化了设计难度。因为 client 需要的是顺序读取而 chunkserver 中 Linux 自身的缓存已经足够了。
Master
因为只有一个 master,所以我们需要在文件的读写中减少 master 的参与程度来避免瓶颈。client 并不从 master 中获取数据。
当 client 需要读取数据时,通过向 master 发送请求来获得 chunkserver 的位置,之后 client 将 chunkserver 的位置缓存以供以后使用。
当 client 获得 chunk 对应的位置后,client 直接向对应的 chunkserver 发送请求来获得数据。
元数据
master 存储的元数据有:文件和 chunk 的命名空间、从文件到 chunk 的映射以及每个 chunk 副本的位置。所有的元数据都是储存在内存中,前两个(命名空间以及映射)都是通过 log 来持久化储存的,而 chunk 副本的位置则在每次启动 master 时向每个 chunkserver 收集。
master 中不持久存储 chunk 的位置避免了两者的同步问题,master 通过与 chunkserver 的心跳包来监控 chunk。
log
log 包括了关键元数据的历史记录,同时 log 也作为并发操作的时间线。
在对文件元数据进行操作前首先写入 log (类似 WAL),同时定期保存检查点 (checkpoint) 来减少启动时间:只需要从最后一个没有损坏的检查点开始重做 log 即可。
在建立检查点的过程中转换到新的 log 文件并且在另一个线程中穿件检查点。
一致性模型
GFS 的一致性模型比较宽松以提高效率。
对文件元数据的操作 (如创建文件) 是原子的,通过在 master 中对元数据进行加锁来实现。
在 GFS 中提出了两个一致性概念:consistent 与 defined
- consitent 指 client 读取文件的任何一个副本 (replica) 都是一样的
- defined 指 client 可以看到上一次修改的完成内容
对文件修改后,会出现以下几种情况:
- 一个修改成功的执行并且没有与并行的操作发生干涉,则文件是 defined
- 并行的对文件修改是 consistent 而是 undefined:所有的 client 都能看到相同的数据,但是数据并不能反映任何一次修改
- 失败的写入是 unconsistent:不同的 client 看到的数据不同
对文件的修改包括了 overwrite 和追加 append,注意这里的追加与文件的追加不同:追加数据时 GFS 保证数据在文件中至少出现一次,而数据的偏移量是由 GFS 返回。GFS 可能在数据之间插入 padding 或者重复数据。
对应用的要求
因为会出现重复或 padding,所以文件的校验需要由上层应用通过添加额外的字段来完成。
GFS 的操作
租约 Lease 与文件修改顺序
mater 会把 lease 给一个 chunk 的副本,称这个 chunk 为 primary。primary 决定文件修改的执行顺序,其他的副本按照 primary 的顺序进行修改。
租约机制的用来减少 master 的开销,租约的默认时间为 60s,但是 primary 可以向 master 申请延长时间,延长请求通过心跳包与所有的 chunkserver 通信。所以即使是 chunkserver 失去了与 primary 的通信依然可以吊销其租约。
写文件流程
- client 向 master 请求哪一台 chunkserver 持有了要修改的 chunk 的租约,如果没有租约则 master 会分配一个
- master 返回 primary 与其他副本的位置,client 缓存位置信息供以后修改。当下次无法联系到 primary 或者租约过期时再向 master 请求
- client 向所有的副本推送数据,chunkserver 会把接收到的数据缓存下来 (注意这时候还没有对文件进行修改) <–这是数据流
- 在所有的副本都收到了数据后,client 发送写请求给 primary。写请求中标识了之前推送的数据,primary 为写请求安排序列号用来表示执行的顺序。之后按照顺序对本地文件进行修改
- primary 将写请求转发到其他的副本中,每一个副本按照相同顺序执行操作
- 副本向 primary 返回完成操作
- primary 向用户返回。如果操作失败 (数据在不同副本间不一致,此时为 inconsistent),client 会重新尝试执行操作。
如果对数据的操作超过了一个 chunk,client 会将其分为多个请求。切分后的多个请求可能会与其他的文件操作交织 (interleaved) 运行,导致在文件中可能交错出现不同的数据,此时文件为 undefined。
数据流
在以上过程中很重要的一点就是解藕 (decouple) 了控制流和数据流,由此可以有效地利用网络带宽。
在推送数据的过程中,按照拓扑顺序推送,由此推送数据可以并行执行。
原子文件追加
文件追加与写文件类似,不同的是 primary 收到追加的请求后会检查追加文件是否会使 chunk 超过预设大小,如果会,会将 chunk 的剩余部分填充空白,并要求 client 重新执行操作,将数据追加到新的 chunk 后。
如果追加操作在任意一个副本上失败,client 重新执行该操作,这时会出现同一个 chunk 但是不同副本间不一致的问题。GFS 并不保证所有副本间的文件完全一致,只保证数据至少写一次。所以当追加操作执行时可以保证数据必然写入到所有副本的相同偏移量上。下一次的追加操作会在更高的偏移量执行,
快照
快照可以为文件或者目录创建副本。
快照的创建使用了 CoW (copy-on-write) 技术:当 master 收到创建快照的请求后,master 吊销涉及 chunk 的租约,确保了下一次的对文件的写操作需要请求 master,此时 master 便有机会创建 chunk 的拷贝。
创建 chunk 的拷贝是在本地的,因为硬盘的读取速度相比网络要快。
Master 操作
Namespace 管理与锁
文件的元数据由 master 管理,所以 master 需要锁机制来确保正确的串行化。
所有的 master 操作在执行钱都需要获取锁,如果需要操作 /d1/d2/.../dn/leaf
文件,需要对 /d1
,/d1/d2/
, ...
, /d1/d2/.../dn
加 read lock,对 /d1/d2/.../dn/leaf
加上对应的 read 或 write lock。
由此设计的锁机制可以允许对同一目录的不同文件进行同时操作。
所有的锁只要在需要时才创建,在不需要时被销毁。同时所有的锁都是按照顺序获取以避免死锁。
Replica 管理
chunk 副本的放置为了实现两个目标:最大文件可靠性与最大化带宽利用率。所以单纯的将副本放置在不同的机器上是不够的,我们需要将 chunk 放置在不同的机架中,这不仅可以避免整个机架的损坏,也可以利用多个机架的带宽和进行文件读。但是在写文件时流量需要流过许多个机架,在 Google 看来这是个合理的 tradeoff。
当 master 创建 chunk 时,对于机器的选择有三个因素:
- 将 chunk 放置在硬盘使用率较低的机器上
- 限制每个机器中最近创建 chunk 的数量,如果一个机器中最近创建了很多 chunk,那么接下来将会有很多的写操作
- 同时根据上边说的原因,将 chunk 分布在不同机架中
当副本的数量少于用户设定的值时,进行副本的重建,需要重建的 chunk 按照如下顺序:
- 距离用户指定数量的差距
- 优先为未删除的文件进行重建
- 优先为阻塞的 client 操作进行重建
master 按照优先级对 chunk 进行重建,master 限制重建任务的数量与带宽使用避免对 client 操作影响。
最后,master 周期性对副本进行浮躁均衡
垃圾回收
在文件进行删除后,GFS 不会立刻回收空间。对文件和 chunk 进行 lazy 垃圾回收。
当文件删除后,master 记录 log,然后将文件名重命名为隐藏名字并且带有删除时间戳。在 master 的周期性扫描中会清理超过一定时间的标记为隐藏的文件。在此之前,仍然可以通过重命名来恢复文件。同时在 master 的扫描中,master 能够发现孤儿 chunk。在与 master 的心跳包中,chunkserver 会报告其拥有的 chunk,master 会回复不再需要的 chunk。
这种垃圾回收机制有几点好处:
- 在大型分布式系统中简单可靠。chunk 的创建在某些 chunkserver 可能成功了,但是在其他上可能失败了,导致留下了 master 不知道是否存在的 chunk。删除副本的请求可能会丢失,master 要记录并且重发。相比之下垃圾回收提供了一种统一的方式来清理无用的副本
- 将存储空间回收与日常后台任务合并,可以在 master 空闲时进行
- 删除请求的延迟提供了错误恢复
同时允许用户手动删除文件
容错
设计系统的最大挑战就是组件时常失效。
高可用性
在集群中提供影子 master,影子 master 只提供只读操作。影子 master 可能会相较 master 延迟一段时间。
数据完整性
chunkserver 使用校验来检测文件损坏。
每一个 chunk 分为 64K 的块,每个块计算 32bit 校验和,保存在 log 中。
在读操作时,chunkserver 会在返回数据前检查校验和。如果校验失败 chunkserver 会返回失败,client 会去读取另一份副本,同时 master 对副本进行重建。重建完成后 master 会通知 chunkserver 删除掉损坏的副本。
校验和的计算方式为文件追加进行优化,校验和可以进行增量更新。追加的最后一块在下一次读取时能够检测到损坏。
但是在覆写一段数据时需要先检测头和尾的校验之后进行写入。
在空闲时,chunkserver 可以扫描检测不活跃的 chunk 的校验和。