1、假设
- 组件失效被认为是常态事件,而不是意外事件。
- 以通常的标准来衡量的话,要存储的文件是巨大的。GB的文件非常普遍。
- 绝大部分文件的修改是采用在文件尾部追加数据而不是覆盖原有数据的方式。
- 应用程序和文件系统API的需要协同设计,这提高了整个系统的灵活性。
2、设计概述
2.1 设计预期
- 系统会存储一定数量的大文件,可能由几百万文件。文件的大小通常在100MB或者以上,数GB大小的文件也普遍存在。
- 系统也要支持小文件,但是不需要针对小文件做专门的优化。
- 系统工作负载主要有两种读操作:大规模的流式读取(数百kb甚至1MB,且读取连续)和小规模的随机读取(文件某个随机位置读取几kb)。多次随机读取的合并可以提升性能。
- 工作负载还包括许多大规模、顺序的、数据追加方式的写操作(写入规模和大规模读类似),数据一旦被写入后,就很少被修改了。系统也支持小规模的写入操作,但效率不高。
- 系统还要支持多个客户端同时向一个文件中并行的写入的操作。这是有同步来实现原子操作的多路追加数据。读取操作可以在写入完成后进行,也可以同时进行。
2.2 接口
- GFS提供了一套类似于传统文件系统的API接口函数。文件以分层目录的形式组织,用路径名来标识。
- GFS支持常用的操作,如创建文件、删除文件、打开文件、关闭文件、读和写文件。
- GFS提供了snapshot(快照)和record append(记录追加)的操作。snapshot操作用很低的成本创建一个文件或者目录树的拷贝;记录追加允许多个客户端对同一个文件进行数据追加操作,同时保证每个客户端的追加操作是原子性的。
2.3 系统架构
- 一个GFS集群包含一个单独的master节点、多台chunk服务器,并且同时被多个客户端访问。
- GFS存储的文件都被分割成固定大小的chunk,在chunk创建之初,master服务器会给每个chunk分配一个不变的唯一的64位的chunk handle。chunk服务器把chunk以Linux文件的形式保存在本地磁盘上,并根据指定的chunk handle和byte range读写块数据。(每个chunk都会被复制到多个块服务器上以保证可靠性)
- master节点管理所有的文件系统metadata(包括namespace、访问控制权限、文件到chunk的映射、chunk的当前位置信息 );同时还管理着系统范围内的活动,如:chunk lease、孤儿chunk回收、chunk在chunk服务器间的迁移;master还周期性的利用HeartBeat messages和chunk服务器进行交流:发送指令和接受chunk服务器的信息。
- GFS客户端代码以库的形式链接到客户程序中。客户端代码实现了GFS文件系统的API接口函数、应用程序与master节点和chunk服务器的通讯、对数据的读写操作。客户端和master节点的通信只获取metadata,所有数据操作都由客户端直接和chunk服务器进行交互。
- 无论客户端还是chunk服务器都不必缓存文件数据。客户端因为缓存数据太大收益太小。chunk服务器因为Linux操作系统的文件系统会将经常访问的数据缓存在内存中。
2.4 单一master节点
单一master节点可以利用全局的信息精确定位chunk位置以及进行复制决策。同时必须减少对master的读写避免其成为系统瓶颈。客户向master访问到元数据后将其缓存一段时间,后续的操作将直接和chunk服务器进行数据读写。
读取流程如下(结合上图):
- 客户端把文件名和程序指定的字节偏移根据固定的chunk大小换算成文件的chunk索引。然后把文件名和chunk索引发送到master节点。master节点将对应的chunk handle和副本的位置信息发送给客户端。客户端利用文件名和chunk handle作为key缓存这些信息。
- 然后客户端发送请求到其中一个副本处,一般选择最近的。请求信息包含了chunk handle和byte range。在对这个chunk后续的读取操作中,客户端不必再和master节点通讯了(除非缓存的元数据信息过期或者文件被重新打开)。
- 实际上,客户端通常会在一次请求中查询多个chunk信息,master节点的回应也可能包含了紧跟着这些被请求的chunk后面的chunk的信息。这些额外信息在没有任何代价的情况下避免了客户端和master节点未来坑你发生的几次通信。
2.5 chunk size
chunk size的大小是设计的关键性问题之一。GFS选择了64MB,这远远大于一般文件系统的block size。每个chunk副本都以普通文件形式保存在chunk服务器上,只有在需要的时候才扩大。这种lazy space allocation策略避免了因内部碎片造成的空间浪费。
选择较大size的chunk的优点如下:
- 减少客户端和master的通讯需求。
- 采用较大的chunk尺寸,客户端能够对一个块进行多次操作,这样通过与chunk服务器保持较长的tcp连接来减少网络负载。
- 减少了master节点保存的metadata数量,这允许把所有的metadata放入内存中。
采用较大size的chunk也有如下缺点:
- 小文件包含较少的chunk,甚至只有一个chunk。当许多客户端对同一个小文件进行多次访问时,存储这些chunk的chunk服务器就会变成hot spots(实际中大多是连续读取包含多个chunk的大文件,这并不是主要问题)。
- 当把GFS用于批处理队列系统的时候,hot spots问题还是产生了:一个可执行文件在chunk服务器上保存为single-chunk文件,之后这个可执行文件在数百台机器同时启动。存放这个可执行文件几个的chunk服务器会被数百个客户端的并发请求导致系统局部过载。(可以采用更多的副本数量和错开批处理队列系统程序的启动时间来解决)
2.6 metadata
master服务器主要保存三种类型的元数据:文件和chunk的命名空间、文件和chunk的对应关系、每个chunk副本的存放地点。所有元数据都保存在master服务器的内存中。前两种数据也会以记录变更日志的方式记录在os的系统日志文件中,日志存放在本地磁盘,同时会复制到其他远程master服务器上。master服务器不会持久保存chunk位置信息,其会在启动时或者有新的chunk加入时,向各个chunk服务器轮训它们存储的chunk信息。
2.6.1 内存中的数据结构
因为数据保存在内存中,所以master可以周期扫描自己保存的全部状态信息。这也有利于实现:chunk垃圾收集、在chunk服务器失效时重新复制数据、通过chunk的迁移实现跨chunk服务器的负载均衡、磁盘使用状况统计。(4.3 4.4)
将元数据全部保存在内存中有潜在的问题:
- chunk的数量和整个系统的承载能力受限于master服务器所拥有的的内存大小(实际这并不是一个严重问题。master只需要不到64byte的metadata就能管理一个64MB的chunk:大多数文件包含多个chunk,因此大多数chunk是满的;每个文件在命名空间中的数据大小通常是64byte以下,因为保存的文件名是用前缀压缩算法压缩过的)。
2.6.2 chunk位置信息
-
master服务器并不持久化保存哪个chunk服务器存有哪些chunk副本的信息。master服务器只是启动的时候轮询chunk服务器获取这些位置信息。
-
master服务器能够保证它持有的chunk位置信息始终是最新的,因为它控制了所有chunk位置的分配。而且通过周期性的HeartBeat来控制chunk服务器的状态。
2.6.3 操作日志
日志的重要性:
- 日志操作包含了关键的metadata的变更历史记录,对GFS很重要
- 日志是metadata唯一的持久化记录
- 它也是判断同步操作顺序的逻辑时间线: 文件和chunk连同版本号都由他们创建的逻辑时间唯一、永久的标识(4.5)
进行日志操作需要保证日志文件的完整,确保只有在元数据被持久化以后,日志才对客户端是可见的。否则即使chunk本身没有任何问题,依旧可能丢失整个文件系统,或丢失客户端最近的操作。故需要把日志复制到多台远程服务器,并只有把相应的日志记录写入到本地以及远程机器的硬盘后,才会相应客户操作请求。master服务器会收集多个日志记录后批量处理减少写入磁盘次数。
master服务器进行灾难恢复时,通过重演操作日志文件把文件系统恢复到最近的状态。为了缩短master启动的时间,需要让日志足够小。master服务器在日志增长到一定大小时对系统做一次checkpoint,将所有状态数据写入checkpoint文件。checkpoint文件以压缩B树形式的数据结构存储。
创建一个checkpoint需要一定时间,故master服务器的内部状态被组织为一种格式,来保证在checkpoint过程中不会阻塞当前进行的修改操作。master服务器采用独立的线程切换到新的日志文件和创建checkpoint文件。新的checkpoint需要包含之前的所有更改。对于一个包含几百万个文件的集群,创建一个checkpoint文件需要1分钟左右。
2.7 一致性模型
2.7.1 GFS提供的一致性保障
- 文件命名空间的修改是原子性的,仅有master节点控制。命名空间锁提供了原子性和正确性(4.1)的保障,master节点的操作日志定义了这些操作在全局的顺序(2.6.3)
- 数据修改后file region的状态取决于操作的类型、成功与否、是否同步修改。下表是各种操作的结果。
- 对上表的解释:
- 如果所有客户端的数据都一致(即同步修改)那么认为文件是consistent(一致的)
- 如果对文件数据修改后,region是一致的,并且客户端能够看到写入操作全部的内容,那么这个region是defined(已定义的)
- 并行修改操作完成以后,region处于consistent but undefined
2.7.2 程序实现
- 对于记录写追加方式,如果出现了错误(3.3),是可以进行正确处理的。这需要在每个记录中包含额外的写入信息如:checksum。
- readers来进行chunk的读取:(包含了处理偶然性的填充数据和重复内容)readers根据checksum识别和抛弃额外的填充数据和记录片段。
3、系统交互
- 此系统一个重要的原则:最小化所有操作与master节点的交互
3.1 租约(Leases) and 变更顺序(Mutation Order)
mutation是会改变chunk内容或者元数据的操作,比如:写入操作或者记录追加。Mutation会在chunk所有的副本上执行。我们使用leases来保证多个副本mutation order的一致性。分为以下步骤:
建立租约:master节点为一个chunk副本建立一个lease,这个副本被叫做主chunk。主chunk对chunk所有的更改操作进行序列化。所有副本都按照这个顺序进行修改操作。其他chunk的修改操作顺序由主chunk通知他们的序列号决定。
lease的目的:最小化master几点的管理负担。
下图是一次写操作的控制流和数据流:(弄懂每个数据流和控制流的作用)
3.2 数据流
- 控制流和数据流分开来提高网络效率。
- 控制流从client到主chunk,再到所有的二级副本。
- 数据以管道的方式沿着一个精心选择的chunk服务器链推送。(链式推送而不是拓扑推送是为了充分利用每台机器的带宽)
- 如何选择目的机器推送目标:每台机器尽量在拓普网络中选择一台没有接收到数据的、离自己最近的目标(举例可以通过ip地址”计算“)推送数据。
- 利用基于TCP连接的、管道式数据推送方式来最小化延迟。
3.3 原子的记录追加
-
GFS提供了一种原子的数据追加操作–记录追加。传统的并行写入操作不是串行的,这会导致写入出错。GFS保证至少有一次的原子写入操作成功执行。写入的数据追加到GFS指定的偏移位置上,然后GFS返回这个偏移量给客户端。
-
记录追加非常频繁,多个客户端会同时对一个文件进行写入。采用分布式锁管理器来进行同步消耗的代价太大。
-
追加记录在某些副本失败会出现什么结果?如何在不一致的chunk上读取?(2.7.2)
3.4 快照(snapshot)
- snapshot可以几乎瞬间完成对一个文件或者目录树的拷贝操作,并且几乎不会对其他正进行的操作造成任何干扰。
- snapshot是用标准的copy-on-write技术实现的。
- snapshot过程:
- 当master收到一个snapshot请求时,首先取消作为快照的文件的所有chunk租约。
- 租约取消或过期后,master节点把这个操作以日志的方式记录到硬盘上。然后master节点通过复制源文件或目录的元数据的方式,把这条日志记录的变化反映到保存的内存状态中,新创建的快照文件和源文件指向完全相同的chunk
- 在snapshot之后,客户机第一次写入chunk C时,会先向master查chunk当前租约持有者。master注意到chunk C引用计数超过1,就选择一个新的句柄C‘要求每个拥有chunk C当前副本的服务器创建一个叫C’的新chunk。(本地复制比网络复制快得多)
4、master节点的操作
4.1 命名空间管理和锁
- GFS对所涉及的操作都进行加锁控制。对当前操作对象加读写锁,对上层所有对象加读取锁。
- 命名空间中可能由很多节点,读写锁采用惰性分配策略,在不再使用的时候立刻被删除。
- 锁的获取也有一个全局的一致的顺序避免死锁:首先按照命名空间层次排序,同一层次中按照字典排序
4.2 副本的位置
当master需要分配新的chunk handle时,需要考虑chunk分配的位置。进行这一决策需要考虑:最大化数据可靠性和可用性,最大化网络带宽利用率。故需要在多台chunk服务器上分布式存储chunk副本。
4.3 创建、重新复制、重新负载均衡
chunk副本的三个用途:chunk创建、重新复制、重新负载均衡
master创建一个新的chunk时,要考虑最初放置副本的位置:
- 希望在低于平均磁盘使用率的chunk服务器上存储新的副本。
- 希望限制在每个chunk服务器上最近的chunk创建操作次数。
当chunk有效副本低于用户指定的复制因数时,master节点会重新复制它。这可能是由以下原因导致:
- 一个chunk服务器不可用了。
- chunk副本的复制因数提高了。
每个需要被重新复制的chunk都会根据几个因数排序优先级:
- 丢失两个chunk副本比丢失一个有更高的优先级
- 优先复制活跃的文件(live file)相对于刚刚被删除的文件的chunk
master服务器会周期性的对副本进行重新负载均衡:检查副本分布情况,然后移动副本以便更好的利用硬盘空间、更有效的进行负载均衡。(这里移动副本应该是指的将chunk多的服务器上的一部分chunk移到chunk少的服务器上,而不是用新的chunk填充chunk少的服务器)
4.4 垃圾回收
GFS在文件删除后不会立刻回物理空间,采用惰性策略,只在文件和chunk级的常规垃圾收集时进行。
4.4.1 垃圾回收机制
删除文件分以下过程完成:
- 将删除操作写入日志
- 将文件名改为一个包含删除时间戳、隐藏的名字
- master节点对文件系统命名空间做常规扫描时再删除所有三天前的隐藏文件(包括删除chunk和metadata)(可设置)
直至文件被真正删除都可以通过将隐藏文件名改为正常显示的文件名来“反删除”。
4.4.2 讨论
垃圾回收的优点:
- 极有可能存在孤儿chunk(创建失败、删除失败等)垃圾回收提供了一致的、可靠的清除无用副本的方法
- 垃圾回收把存储空间的回收操作合并到master节点规律性的后台操作,操作批量执行、开销分散。并且在master节点相对空闲的时候完成。
- 延缓存储空间回收为意外的、不可逆转的删除操作提供了保障。
采用惰性策略延迟回收的缺点:
- 阻碍用户调优存储空间的使用,特别是空间紧缺时。
- 重复创建和删除临时文件时,释放的存储空间不可立刻被使用。
针对缺点,可以进行以下改进:
- 可以通过显示的再次删除一个已经被删除的文件加速回收速度。
- 可以为命名空间的不同部分设定不同的复制和回收策略。(如立刻删除或者垃圾回收)
4.5 过期失效的副本检测
Master 节点保存了每个 Chunk 的版本号,用来区分当前的副本和过期副本。
版本号的更新:master与chunk签订一个新的租约时(同时也会被master和chunk服务器同时保存到持久化存储的状态信息中)
失效情况:
- master正常工作,版本号正常。chunk服务器由于失效有一次版本号未正常更新
- chunk服务器正常,master出错
失效检查:
- 在chunk服务器启动时会向马沙特人报告所有chunk的版本号,此时进行检查
- 客户机从master得到的chunk位置信息中包含版本号,可以验证(通过客户机或chunk服务器)
失效处理:在进行垃圾回收时移除所有失效副本
5、容错和诊断
GFS如何处理频繁发生的组件失效,这需要GFS自带工具诊断系统故障。
5.1 高可用性
在任何给定的时间内都会有写服务器不可用。我们使用两条简单有效策略保证系统的高可用性:快速回复和复制。
5.1.1 快速回复
不管master服务器和chunk服务器如何关闭的,都被设计为可以在数秒内恢复状态并重新启动。(并不区分正常和异常关闭)
5.1.2 chunk复制
chunk服务器离线时,或者通过checksum(5.2)发现了已经损坏的数据,master节点通过克隆已有的副本保证每个chunk都被完整的复制。
5.1.3 master服务器的复制
maser服务器也会备份,当master进程所在的机器或磁盘失效了,GFS系统外部的监控进程会在其他有完整操作日志的机器上启动一个新的master进程。
5.2 数据完整性
每个副本需要独立的维护checksum来校验自己的副本完整性(可能由于硬盘损坏带来完整性问题,也可能是有未定义问题)(虽然有副本,但修改操作的语义就使得副本不一定完全相同)。
每个chunk分成64kb大小的块。每个块对应一个32bit的checksum,与其他用户数据分开保存在内存和硬盘。
对于读:数据返回给客户端或者其他chunk服务器之前,会检验数据正确性。出错就会返回一个错误信息,并通知master这个错误。请求者应该从其他副本读取数据,master也会从其他副本克隆数据进行恢复。恢复成功后删除错误副本。
5.3 诊断工具
日志来帮助