GFS作为最著名的分布式文件系统,首先具备了大规模、可扩展、适配大文件、自动运维等高级特性。虽然是比较早期的分布式文件系统,但是它里面的设计思想还是值得现代分布式系统设计参考的,并且还有很多后期著名的分布式文件系统就是根据 GFS 来的。
一、设计预期
在论文前面,列举了设计预期,也就是 GFS 是一个怎么样的分布式文件文件系统:
- 失效是一种常态,因为集群系统由许多廉价的普通组件组成,组件失效是一种常态。系统必须持续监控自身的状态,它必须将组 件失效作为一种常态,能够迅速地侦测、冗余并恢复失效的组件。
- 系统存储的是大文件,而不是小文件对象存储,这个非常重要。系统存储一定数量的大文件。我们预期会有几百万文件,文件的大小通常在100MB或者以上。数个 GB大小的文件也是普遍存在,并且要能够被有效的管理。系统也必须支持小文件,但是不需要针对 小文件做专门的优化。
- 系统支持两种主要的读取方式:大规模的流式读取和小规模的随机读取。大规模的流式读取 通常一次读取数百KB的数据,更常见的是一次读取1MB甚至更多的数据。来自同一个客户机的连续 操作通常是读取同一个文件中连续的一个区域。小规模的随机读取通常是在文件某个随机的位置读取 几个KB数据。
- 系统支持高性能的写功能:追加写(append)和覆盖改写(oever-write)。
- 系统必须高效的、行为定义明确的(alex注:well-defined)实现多客户端并行追加数据到同一个文件里的语意,意思是读取的数据必须符合一致性原则。
- 高性能的稳定网络带宽远比低延迟重要。
GFS中提供文件系统的接口有如下:
- 创建(Create)
- 删除(Delete)
- 打开(Open)
- 关闭(Close)
- 读取(Read)
- 写入(Write)
- 生成快照(Snapshot)
- 修改(Update)
- 追加(Append)
二、整体架构
GFS中有三种节点:GFS client,GFS master,GFS chunkserver
- GFS client:维持专用接口,与应用交互。
GFS客户端代码以库的形式被链接到客户程序里。客户端代码实现了GFS文件系统的API接口函数、应用程 序与Master节点和Chunk服务器通讯、以及对数据进行读写操作。客户端和Master节点的通信只获取元数据,所有的数据操作都是由客户端直接和Chunk服务器进行交互的。
- GFS master:维持元数据,统一管理 chunk 位置和租约。
Master节点管理所有的文件系统元数据(元数据包括文件名,文件映射对应的chunk,chunkid对应的chunkserver)。这些元数据包括名字空间、访问控制信息、文件和Chunk的映射 信息、以及当前Chunk的位置信息。Master节点还管理着系统范围内的活动,比如,Chunk租用管理、孤儿Chunk(alex注:orphaned chunks)的回 收、以及Chunk在Chunk服务器之间的迁移。Master节点使用心跳信息周期地和每个Chunk服务器通 讯,发送指令到各个Chunk服务器并接收Chunk服务器的状态信息。
- GFS chunkserver:存储数据。
在Chunk创建的时候,Master服务器会给每个Chunk分 配一个不变的、全球唯一的64位的Chunk标识。Chunk服务器把Chunk以linux文件的形式保存在本地硬盘上,并且根据指定的Chunk标识和字节范围来读写块数据。出于可靠性的考虑,每个块都会复制到多个 块服务器上。缺省情况下,我们使用3个存储复制节点,不过用户可以为不同的文件命名空间设定不同的复制级别。
三、存储设计
因为GFS设计预期是存储大文件,有可能是几PB的巨大文件,并且大小不均。所以GFS没有选择直接以文件为单位进行存储,而是吧文件分为一个个chunk来存储。GFS 把每个 chunk 设置为64MB。
选择较大的chunksize的原因在论文中也做了详细的说明:
- 首先,它减少了客户端和Master节点通讯的需求,因为只需要 一次和Mater节点的通信就可以获取Chunk的位置信息,之后就可以对同一个Chunk进行多次的读写操作。这种方式对降低 GFS 的工作负载来说效果显著,因为应用程序通常是连续读写大文件。即使是小规模的随机读取,采用较大的Chunk尺寸也带来明显的好处,客户端可以轻松的缓存一个数TB的工作数据集所有的Chunk位置信息。
- 其次,采用较大的Chunk尺寸,客户端能够对一个块进行多次操作,这样就 可以通过与Chunk服务器保持较长时间的TCP连接来减少网络负载。
- 第三,选用较大的Chunk尺寸减少了 Master节点需要保存的元数据的数量。这就允许 client 把元数据全部放在内存中。比如64mb的chunk元数据只需要小于64byte的大小即可存储,几G的内存就可以缓存几个PB的数据元数据。
当然更大的chunk意味着多个线程同时操作一个chunk的可能性增加,容易产生热点问题,对于这个问题 GFS 在一致性设计上做出了对性能的妥协。
比如文件 FileA 中一共有三个 chunk,master节点只记录保存了文件命名空间对应的chunk映射关系,然后在 chunk 具体需要怎么存储完全是依靠chunkserver本地的物理存储的。比如其中 chunk1 是保存在 chunkserver1,2,4 中,其中两个是副本。
三、master节点设计
master节点并没有使用非中心共识算法来保证数据的可靠性,相反使用了单中心节点的设计,并且论文里对比了这两者优劣:
GFS设计了单个 master 节点,用来存储整个文件系统的三类元数据:
- 所有文件和chunk的命名空间namespace(持久化)
- 文件到chunk的映射(持久化)
- 每个chunk的位置(非持久化)通过master和chunkserver交互,由chunkserver告知master
不进行chunk位置的持久化数据的原因是,每个chunkserver才是真正保存chunk的地方,就算 master 进行数据持久化,最后还是需要去询问每个 chunkserver ,此chunk是否存在,所以与其做无用功,干脆就不持久化保存了。
在 master 中读取一个文件的过程是:
文件名 -> 获取文件名对应的所有chunk名 -> 获取所有chunk的位置 -> 依次到对应的chunkserver中读取chunk
所以 master 只需要保证元数据高可用性,就不必担心整个分布式文件系统的读写会收到影响。而元数据在 master 中其实也不是单份保存的,利用 checkpoint 和操作日志的方式,可以随时在备份节点上恢复元数据,拉起备份节点。有点类似 mysql 数据库的 redolog 和 binlog 的作用,checkpoint 是对定时进行数据的快照备份,操作日志来补全剩余的操作动作。
三、高可用设计
GFS诞生的时候共识算法并不像现在这成熟,所以GFS借鉴了主备的思想,为系统的元数据和文件数据都单独设计了高可用方案。因为 google 的文件量很大,所以 chunkserver 可能会非常非常多,所以发生宕机的几率可能会非常大,频繁宕机节点的问题GFS可以自动解决,即自动切换主备。而关于高可用的设计,主要是GFS的元数据和文件数据的高可用。
1. master的高可用设计:
- master的三类元数据中,namespace和文件与chunk的对应关系,因为只有在 master 中存在,所以必须要持久化,也自然是要保证其高可用的
- GFS除了正在使用的 master 节点(primary master)外,还维持了 shadow master 作为备份
- master 在正常运行时,对元数据做的所有修改操作,都要先记录日志 write ahead log(WAL),再真正去修改内存中的数据
- primary master会实时向 shadow master 同步 WAL。只有 shadow master 同步日志完成,元数据修改操作才算完完成
如果 master 宕机,GFS会使用 Chubby(本质上是共识算法)来识别并切换到 shadow master,这个切换是秒级的。master 的高可用机制就和 mysql 的主备机制非常相像。
2. chunk的高可用设计
- 文件是被拆为一个个chunk来进行存储,每个chunk都有三个副本。所以文件数据的高可用是以chunk为维度来保持的。
- 注意,chunk并不是依赖传统的主备关系,利用主chunk来进行数据的备份,GFS利用 master 来维持 chunk 的副本信息,也就是 master 来控制哪个 chunk 是主,并且在写过程中控制备份数据的落盘,具体可以看后面的读写过程在实现中,GFS对于一个 chunk 的写入,必须确保在三个副本中的写入都完成,才视为写入完成。并且一个 chunk 的所有副本都会有完整的数据,如果一个chunkserver宕机了,它上面的所有chunk都有另外两个副本依旧可以保存这个chunk数据。如果在这个宕机的副本在一段时间之后都没有回复,那么master就可以在另外一个chunkserver上重建一个副本,从而始终把chunk的副本数目维持在3个
- 每个 chunk 都会维持一个校验和,独缺的时候通过校验和检验,如果不匹配,chunksever会反馈给master,master会选择其他副本进行读取,并重建此 chunk 副本
- 为了减少对于master的压力,GFS采用一种租约的机制(Lease),把文件的读写权限下放给某个chunk副本,这样就不用频繁交互向 master 获取 chunkserver 中的 primary chunk
- 租约的主备只决定控制流走向,不影响数据流
chunk的副本放置也需要考虑,GFS 找那个有三种情况需要 master 发起创建 chunk 副本,分别是:创建新chunk、chunk副本复制、负载均衡
负载均衡是指因为某些chunkserver负载过高,比如chunk数据过于热点,那么就会触发把 chunk 副本搬到另外的 chunkserver 上,当然这里的搬迁操作,就是新建 chunk 和删除原来 chunk。
副本复制则是因为一个副本所在的 chunkserver 宕机,导致 chunk 副本数小于预期值,然后新增一个 chunk 副本。
在副本位置选择策略中,master节点遵循以下几点:
- 新副本所在的 chunkserver 负载资源利用率较低
- 新副本的 chunkserver 最近新建的 chunk 副本不多。这里是为了防止某个 chunkserver 新增大量的副本,成为热点
- chunk 的其他副本不能再同一个机架上,这个是为了保证机架或者机房级别的高可用
四、GFS 的读写流程
写操作
GFS 的写入需要三个副本都完成写入后才会返回写入结果。我觉得写入的流程至少现在看来,依然很惊艳。
- 流水线技术
- 数据流和控制流分离技术
在看论文的时候总是有这样的疑问,为什么 client 会先把相关变更的数据先发送到离当前最近的 chunkserver 备份数据上,然后再由 primary chunk 来控制写入的数据。
写入流程
- 第1和2步骤,client 向 master 询问要写入的 chunk 租约在哪个 chunkserver 上(primary replica),以及所有的副本(secondary replica)的位置,如果 client 上有缓存的话则直接缓存获取
- 第3步骤,client把数据都推送到离此最近的副本上,这一步会用到流水线技术,也就是写入过程中唯一的数据流操作,最近的副本会把数据扩散到其他的 replica,利用全双工网络传输,可以一边接收数据,一边发送给其他副本,最大化利用网络资源
- 第4步骤中。确认所有的副本上收到数据后,client 会发送正式写入的请求到 primary replica。primary replica 接收到这个请求后,会对这个chunk上所有的操作进行排序,然后按照顺序执行写入,这里比较关键的是,primary 唯一确定写入顺序,保证副本的一致性
- primary replica 把 chunk 的写入顺序同步给所有的 second replica
如果一部分的备份写入失败,那么就将从第三步重新写入。并且如果一个写入操作设计到多个chunk,client会把他们分成多个写入来执行。
以一个例子来说明,s1、s2、s3三个 chunkserver,client 在北京,s1 是主,在上海。s2、s3是备,在北京。
流水线同步写入的顺序是:Client -> s2/s3 -> s1,只有一次跨地域传输。如果是常规的主备的话,一定会先落入s1主,然后再同步到备用,那么在这个例子就会出现两次跨地域传输。
注意这里的第4步骤,和第3步骤的区别:
- 第3步骤:客户机把数据推送到所有的副本上。注意是变更的数据保存在这里,比如数据的变更:set v1 1、set v1 2、set v1 3,这三个操作如果不同的顺序执行会产生不同的结果,顺序只有 primary chunk 来决定的
- 第4步骤:确定控制流,也就是顺序执行的数据。当所有的副本都确认接收到了数据,客户机发送写请求到主Chunk服务器。这个请求标识了早前推 送到所有副本的数据。主Chunk为接收到的所有操作分配连续的序列号,这些操作可能来自不同的 客户机,序列号保证了操作顺序执行。它以序列号的顺序把操作应用到它自己的本地状态中。
这就是所谓的数据流和控制流的分离,保证GFS对一致性的保证可以不受数据同步的干扰。
写操作包括追加和改写,GFS 一般建议的是 append 追加的写入操作,因为如果改写的话,可能会涉及多个 chunk,而如果部分 chunk 成功,部分 chunk 失败,我们读到的文件就是不正确的。所以改写如果想要保持一致性,必须使用一个分布式锁的操作,这样会增加性能的开销,所以 GFS 建议追加的写入模式。
读取流程
- client 收到读取一个文件的请求后,首先会查看自身的缓存中有没有此文件的元数据信息,如果没有则请求 master 节点获取元数据并缓存
- client计算文件偏移量对应的 chunk
- 然后 client 向离自己最近的 chunkserver 发送读请求,如果读取失败则再向请求master元数据
- 读取会进行 chunk 校验和的确认,如果不通过,则读取其他的副本
五、GFS 的一致性模型
在使用中,因为有多个 client ,那么写入往往是并发的,所以会导致副本不一致的风险
并发改写:对于单个改写操作而言,成功就意味着副本间是一致的,但是并发操作可能会设计多个 chunk,不同 chunk 对改写的执行顺序不一定相同,而这有可能造成应用读取不到预期的结果。如下
如果在 client 角度,改写1-3是按顺序来执行的,但是对于跨 chunk 来说,primary chunk会根据 client 到达的时间来获取序列,那么有可能在 primary chunk2 中获取的顺序是改写3-1,那么会导致结果不一致。
追加写:追加写并不会如同改写一样顺序执行不同导致结果不一致。但是有可能重复执行会导致副本间的不一致。为了实现一致性,GFS 对追加写操作做了一些限制。
- 单次 append 大小不超过 64MB
- 如果文件最后一个 chunk 的大小不足以提供此次追加所需的空间,则把此空间用 padding 填满,然后新增 chunk 进行 append
这样可以保证每次 append 都限制在一个 chunk 上,从而保证追加操作的原子性,并且在论文中说明,追加操作中因为失败重试的机制会导致一个副本中会有重复执行追加的数据,比如文件原油的值为“ABC”,追加“DEF”。有的副本第一次失败再重复执行就是“ABCDEF”,而两次都正确执行的副本则为“ABCDEFEDF”。利用记录文件长度,和各个副本的定期校验来消除这个重复。
保证强一致性:
- 对于一个chunk所有副本写入顺序都是一致的,这是由控制流和数据流分离技术实现的,控制流都是由 primary 发出的,而副本的写入顺序也是由 primay 到 secodary
- 使用 chunk 版本号来检测 chunk 副本是否出现过宕机。失效的副本不会再写入操作,master 不会再记录这个副本的信息,gc 程序会自动回收这些副本
- master 会定期检查 chunk 副本的 checksum 来确认其是否正确
- GFS 推荐应用更多的使用追加来达到一致性
GFS的快照机制:
- 快照的机制是根据 Copy on write 写时复制来实现的,当需要生成快照时,master 会回收对应的 chunk 的租约,停止对应 chunk 的所有写入
- 拷贝一份文件的元数据并命名为快照文件,快照文件的元数据仍指向原文件的 chunk。例如,拷贝文件fileA的元数据,生成文件 fileA_backup 元数据,依然指向原 chunk
- 增加文件所有chunk的引用计数,例如本来 fileA 的所有 chunk 引用计数都是1,只有 fileA 引用,但是现在变成2,fileA 和 fileA_backup 都引用
- master 正常授权租约,允许对chunk写入,开始准备下次 fileA 的写入
- 当下次写入操作到来时,发现 chunk 的引用次数大于1,修改时会先拷贝一个新 chunk,再在新 chunk 上写入。例如 fileA 的三个 chunk ABC,chunk C 有写入,那么写入时拷贝 chunk C 为 C’,并写入 C‘。然后此时的 fileA 指向为 chunk ABC’,fileA_backup指向的是 chunk ABC。这时候才发生真正的快照