写在最前:笔者初学gfs有什么理解不当的地方,欢迎在评论区指出,不胜感激。
一.GFS简介
gfs全称Google File System,Google文件系统。是谷歌自主研发的可扩展的分布式文件系统,用于大型的、分布式的、对大量数据进行访问的应用。它运行于廉价的普通硬件上,并提供容错功能。它可以给大量的用户提供总体性能较高的服务。
二.组织架构
架构
既然gfs是分布式文件系统,必然有分工合作的划分:
GFS包括
- 一个master结点(元数据服务器,管理所有chunkserver的节点,只管理,不存放数据)
- 多个chunkserver(数据服务器,真正存放数据的节点)
- 多个client(运行各种应用的客户端,可以发出写入/读出某个文件的请求)。
简要画图可以理解为:
注意的点:
- 在可靠性要求不高的场景,client和chunkserver可以位于一个结点。
- 一般一个逻辑的Master节点包括两台物理主机,即两台Master服务器(以防有一个master主机down掉了,导致整个系统瘫痪)
- 每一结点都可以是普通的Linux服务器,GFS的工作就是协调成百上千的服务器为各种应用提供服务
gfs的数据存储方式
那么,gfs的数据存储方式究竟是怎样的呢?
GFS存储的文件都被分割成固定大小的Chunk(64MB)。
在Chunk创建的时候,Master服务器会给每个Chunk分配一个不变的、全球唯一的 64位的Chunk标识。Chunkserver把Chunk以linux文件的形式保存在本地硬盘上,并且根据指定的Chunk标识和字节范围来读写块数据。
首先我们先了解一个定义:元数据
命名空间(文件和chunk的名称)、文件和Chunk的映射关系(一个文件包含哪些Chunk)、每个Chunk副本的位置信息。这三类信息都被统称为元数据。
master会持久化存储一部分元数据(存在内存中)。而文件被分割成为多个chunk之后,则被分配存储到相应的chunkserver中,写入chunkserver的磁盘。
而为了保证数据的可读性,在没有特殊指定(缺省)的情况下,我们把每个文件的chunk创建两个副本,加上原来有的,一共写入三次,只要没有三个写入都丢失,那么我们就能找到这个文件的数据。
我稍微丰富了一下架构图:
黄底的为持久化存储的数据,橙底的是只保存在内存的数据(没有回退之类的保障)。
刚刚我们说到master只会保存一部分元数据:
master保存每个Chunk副本的位置,和其他两种元数据不同的是: Master服务器不会持久保存Chunk位置信息。Master服务器在启动时,或者有新的Chunk服务器加入时,向各个Chunk服务器轮询它们所存储的Chunk的信息。后续也会定期轮询各个chunkserver获取信息。
而前两种类型的元数据(命名空间、文件和Chunk的对应关系)会被持久化存储:以记录变更日志的方式记录在操作系统的系统日志文件中,日志文件存储在本地磁盘上,同时日志会被复制到其它的远程Master服务器上。
采用保存变更日志的方式,我们能够简单可靠的更新Master服务器 的状态,并且不用担心Master服务器崩溃导致数据不一致的风险。如果mstaer服务器产生错误,我们能通过重复日志文件操作的方式来恢复master服务器。
chunk副本位置不持久化的原因和好处:
-
因为chunkserver可能加入或者离开集群,改变路径名,崩溃,重启等.一个集群中有几百个server,这些情况可能经常发生,所以这样做就排除了master和chunkserver之间的同步问题.
-
只有chunkserver才能确定它自己到底有哪些块,可能由于各种错误,chunkserver中的一些块会自然消失,这样master就没必要为此保存一个不变的记录.
-
因为chunkserver才能确定它自己到底有哪些chunk
持久化的元数据->操作日志的处理方式:
操作日志记录对metadata的所有修改,它作为逻辑时间线定义了并发操作的执行顺序.文件,块以及它们的版本号都是由它们被创建时的逻辑时间而唯一地,永久的被标示.
为了恢复时间短,日志必须小,每当日志增长到一定规模后,master就要检查它的状态,可以从本地磁盘装入最近的checkpoint来恢复状态.
checkpoint的实现内容???如何实现的??
三.读取数据的过程
了解了gfs系统是如何存储数据的,那么很容易的我们可以理解读数据的大致过程:
1.client要查询某个文件,首先先以chunk的固定大小(64MB),client将应用指定的文件名称和byte offset转换成文件系统的一个chunk索引(filename 和byte offset可以最终确定为某个file中的特定chunk信息),然后向master询问,获取这个文件的chunk地址,客户端将缓存这个地址。
2.获取文件地址后,找到地址对应的chunkserver,直接与chunkserver交互,拿到这个文件的数据。
之前提到,gfs系统为了保证数据的可读性,默认把每个文件的chunk创建两个副本了,加上原来有的,一共写入三次。因此读文件操作就还要复杂一点:因为master会告诉client所有副本所在的位置。那么我们的客户端面对三个副本的位置,则会选择离它最近的一个副本所在的chunkserver进行交互,获取file的数据。
我们的网络拓扑非常简单,通过IP地址就可以计算出节点的“距离”。比如如果client的ip是192.0.0.3 而从master获取的chunk位置在chunkserver1(192.0.0.4),chunkserver2(192.0.0.8) ,chunkserver3(192.0.0.9) 那么离client最近的就应该是192.0.0.4 client在读取chunk的时候就会选择最近的chunkserver1来交互读取数据了。
所以,以图来说:
client先向master问,我要找file,它根据64MB的大小分成了两个chunk。master回答:file分成 了chunk-10ef和chunk-20ee,它们在chunkserver1,chunkserver2,chunkserver3上。client知道了之后,离我最近的就是chunkserver1,于是client就找到了chunkserver1,向chunkserver1获取到chunk-10ef和chunk-20ee
四.写数据的过程
要了解gfs系统存数据的过程,我们需要先了解租约的概念。
Master节点为Chunk的一个副本建立一个租约,我们把这个副本所在的server叫做主Chunkserver。主 Chunkserver对Chunk的所有更改操作进行序列化。所有的副本都遵从这个序列进行修改操作。因此,修改操作全局的顺序首先由Master节点选择的租约的顺序决定,然后由租约中主Chunkserver分配的连续序列号决定。
租约时长为60秒,且在租约结束后如果不出问题,master尽量将租约付给原持有租约的chunkserver。
设置租约的目的:
我们使用租约 (lease)机制来保持多个副本间变更顺序的一致性,目的是为了最小化Master节点的管理负担。
1)客户端向Master请求chunk每个副本所在的ChunkServer,其中持有租约的chunk所在的ChunkServer叫主ChunkServer。如果没有ChunkServer持有租约,说明该chunk最近没有写操作,Master会发起一个任务,按照一定的策略将chunk的租约授权给其中一台ChunkServer。
2)Master返回客户端主副本和备副本所在的ChunkServer的位置信息,客户端将缓存这些信息供以后使用。如果不出现故障,客户端以后读写该chunk都不需要再次请求Master。如果出现chunkserver有错误,不可访问的时候,client会再次问询master。
3)客户端将要写入/追加的记录发送到每一个副本,每一个ChunkServer会在内部的LRU结构中缓存这些数据。GFS中采用数据流和控制流分离的方法,从而能够基于网络拓扑结构更好地调度数据流的传输。
4)当所有副本都确认收到了数据,客户端发起一个写请求控制命令给主副本。由于主副本可能收到多个客户端对同一个chunk的并发追加操作,主副本将确定这些操作的顺序并(分配连续的序列号)写入本地。
5)主副本把写请求提交给所有的备副本。每一个备副本会根据主副本确定的顺序执行写操作。
6)备副本成功完成后应答主副本。
7)主副本应答客户端,如果有副本发生错误,将出现主副本写成功但是某些备副本不成功的情况,客户端将重试写入,从第3步开始。
之前说过一个chunk固定为64MB,那么为什么那么定义呢?
1.它减少了客户端和Master节点通讯的需求,因为只需要一次和Mater节点的通信就可以 获取Chunk的位置信息,之后就可以对同一个Chunk进行多次的读写操作。这种方式对降低我们的工作负载来说效果显著,因为我们的应用程序通常是连续读写大文件。即使是小规模的随机读取,采用较大的Chunk尺寸也带来明显的好处,客户端可以轻松的缓存一个数TB的工作数据集所有的Chunk位置信息。
2.采用较大的Chunk尺寸,客户端能够对一个块进行多次操作,这样就可以通过与Chunk服务器保持较长时间的TCP连接来减少网络负载。
3.选用较大的Chunk尺寸减少了Master节点需要保存的元数据的数量。这就允许我们把元数据全部放在内存中,减少了master的负担。
但是采用64MB的大尺寸的也有一定的缺陷:
小文件包含少量的chunk,或许只有一个。在多个客户端访问同一个文件时,存储这些chunk的chunkserver可能成为热点(hot spots)。如果一个可执行文件作为一个single-chunk的文件被写入到GFS,然后同时在多个机器上启动。存储这个可执行文件(executeable)的几个chunkservers在数个请求同时访问时可能会过载。
所以gfs系统更适用于大文件存储
五.删除文件的过程
是一种惰性删除
当要删除文件时:
1.master在元数据中将文件改为一个隐藏的名字,并且包含一个删除时间。master定期检查,如果发现文件删除超过一定时间,就会从内存中将元数据删除。
2.在chunkserver通过心跳汇报chunk信息时,master会回复master元数据中已经不存在的的chunk信息。
3.chunkserver会释放这些chunk副本。
删除操作即完成。 chunk的删除定期检查可以批处理需要删除的文件chunk。
六.master的更多作用
快照
快照(可以瞬间完成,几乎不会对正在进行其它操作造成任何干扰)
写时复制生成快照。步骤如下:
1)通过租约机制收回对文件的每个chunk的写操作权限(master把这个操作以日志的方式记录到硬盘上),停止对文件的写操作
2)master拷贝文件名等元数据生成一个新的快照文件
3)对执行快照的文件的所有chunk增加引用计数
在快照操作之后,客户端向快照中涉及到的chunk写数据步骤:
1)询问master当前持有租约者
2)master发现当前chunk的引用大于1,通知chunkserver复制该chunk,客户端的所有追加操作转向新复制出来的chunk
容错机制
** 1)master容错机制:**
i、操作日志和checkpoint。master服务器的所有操作日志和checkpoint文件都被复制到多台机器上。
ii、“影子”master服务器,非master的镜像,它们比主服务器的更新要慢,通常不到1秒
iii、持久化命名空间和文件到chunk映射的元数据
** 2)chunserver容错机制 **
i、存储多个chunk副本
ii、对存储数据进行checksum。gfs的每个chunk都分成一定大小的block(64KB),每个block都有一个32bit的checksum。当读取一个chunk副本时,chunkserver会将读取的数据和校验和进行比较。如果比匹配,则返回客户端错误,并报告master服务器。客户端收到错误后会请求其它副本读取数据。同时master服务器也会从其它副本克隆数据进行恢复。当一个新副本就绪后,master服务器通知副本错误的chunkserver删除错误的副本
master通过checksum监控各个chunk,如果副本的checksum和master内存里的checksum不匹配,master也会删除这个副本并且创造一个新的副本取代它。如同其他元信息,checksum和用户数据分开,checksum保存在内存并且同时持久化到硬盘中。读操作时,chunkserver计算读的区域的checksum,如果出错则通知client错误并且报告错误给master,为了降低计算checksum开销,client将读操作按照checksum块对齐。master也会周期性地扫描非活跃chunk的checksum以保证数据的正确性。
过期失效的副本检测
当 Chunk 服务器失效时, Chunk 的副本有可能因错失了一些修改操作而过期失效。 Master 节点保存了每个 Chunk 的版本号,用来区分当前的副本和过期副本。
无论何时,只要 Master 节点和 Chunk 签订一个新的租约,它就增加 Chunk 的版本号,然后通知最新的副本
当一个chunkserver宕机错过了chunk的升版本,这意味着它所持有的chunk数据是过期的。
一致性
一致性:gfs保证至少追加成功一次
1)出现异常:客户端会重新请求追加,因此可能出现记录在某些副本中被追加了多次,即重复记录;也可能出现一些可识别的填充记录。
2)gfs客户端支持并发追加。多个客户端之间的顺序无法保证,同一客户端连续追加成功的多个记录也可能被打断
为了保证各副本内容一致,除了让主副本规定执行顺序,还需要version的保障,如果在写入的时候发现某个副本的版本号不一致,则不会在该副本上执行任何操作因为这个副本可能已经丢失了数据,master之后也不会再给client回复该chunk副本的位置。垃圾回收之后会回收掉这个chunk副本。
但是由于client缓存的存在,client仍然可能读到一个过期副本的位置,论文里面说是通过控制缓存的超时时间以及下一次打开文件的时间来控制,但是这里具体的做法论文没有提到(谁来帮我理解一下这里??)。
由于client大量的操作是append,所以即使是过期副本client也只会读到数据偏少,而不会读取到错误的数据,当client重试去master获取chunk位置时将不会拿到这个过期chunk的位置了。
(这里在加入一些我个人的理解,因为一个副本因为宕机重启,而它又刚好被master选为主chunk。这时它报告master的chunk版本没有升,而实际上在它宕机期间又确实发生了数据的新写入,这个错误是检测不到的,所以我们必须要保证租约的过期时间不能比宕机重启的时间更长)
重复追加,读数据的时候不会读错误数据吗??
总结
GFS的命名空间操作是原子性的,并且用日志记录下操作顺序。
虽然GFS没有目录结构,但是仍然有一颗逻辑的目录树,树的每个结点都有自己的读写锁,每个元数据操作都需要获得一系列的锁,应该是写锁会阻塞其它的锁,而读锁只阻塞写锁而不阻塞读锁。
比如/home/user “目录” 正在创建快照,需要获得/home的读锁和/home/user的写锁,这时如果想创建文件/home/user/foo会被阻塞,因为需要获得/home、/home/user的读锁以及/home/user/foo的写锁,快照会阻塞创建操作获取/home/user的读锁。
如果是在一个有传统目录树结构的文件系统里,创建一个文件需要修改父目录的数据,因此需要获得父目录的写锁。这种锁机制允许在一个目录里并发修改数据(如并发创建文件等),这在传统文件系统里是不允许的。
在逻辑上,GFS 的namespace就是一个全路径和元数据映射关系的查找表。利用前缀压缩,这个表可以高效的存储在内存中。在存储名称空间的树型结构上,每个节点(绝对路径的文件名或绝对路径的目录名)都有一个关联的读写锁。采用这种锁方案的优点是支持对同一目录的并行操作。
master的作用:
Master机器保存了文件系统的所有metadata(元数据),包括namespace(命名空间),访问权限控制,文件与chunks的影射关系,还有当前chunks的位置。
它(master)也控制系统范围内的activities(活动),例如chunk lease(契约)管理,孤立(失效)chunk的回收,chunkserver间 chunk的迁移(migration)。master会间歇性的与每个chunkserver通讯,在heartbeat messages(心跳消息)中发送指令和收集chunkserver的状态。
chunkserver的作用:
1.主chunkserver:负责控制写入数据写入的顺序,并且同步给其他副本。保证数据的一致性
2.chunk的数据写入本地磁盘。保存checksum,维护数据的正确性。
3.和master交互,上报各自所拥有的chunk。
client:
发出读写请求的客户端,可以缓存一定时间内曾经从master获取的chunk的元信息,以便下次使用。
gfs系统的致命缺点是一致性:
当写入不能正常写入的时候,将会多次写入,gfs系统保证所有副本都至少有一次正确写入。但是这样就导致有些副本写入多次,副本就会不一致。
其他漏洞例子:
如果chunk有63mb,有client1,client2同时追加2mb的数据。首先假定client1先写入,那么chunkserver向client1返回错误,因为剩下的1mb不够写了,于是client向master申请多加一个新的chunk,剩余的1mb写入新的chunk,而在client1申请完成之后,client2也向chunkserver发出写入请求,这时如果顺序是先写入client2的数据后再写入client1的数据,就会造成数据没有连续写,原子性遭到破坏。
给出了这样几条使用GFS的建议:依赖追加(append)而不是依赖覆盖(overwrite),设立检查点(checkpointing),写入自校验(write self-validating),自记录标识(self-identifying records)。
使用方式1:只有单个写入的情况下,按从头到尾的方式生成文件。
方法1.1:先临时写入一个文件,再全部数据写入成功后,将文件改名成一个永久的名字,文件的读取方只能通过永久的文件名访问到这个文件。
方法1.2:写入方按一定的周期,写入数据,写入成功后,记录一个写入进度检查点(checkpoint ),这个检查点包括应用级的校验数(checksum)。读取方只去校验、处理检查点之前的数据。即便写入方出现宕机的情况,重启后的写入方或者新的写入方,会从检查点开始,继续重新写入数据,这样就修复了不一致的数据。
使用方式2:多个写入并发向一个文件尾部追加,这个使用方式就像是一个生产消费型的消息队列,多个生产者向一个文件尾部追加消息,消费者从文件读取消息
方法2.1:使用record append接口,保证数据至少被成功的写入一次。但是应用需要应对不一致数据,和重复数据。
为了校验不一致数据,给每个记录添加校验数(checksum),读取方通过校验数识别出不一致的数据,并且丢弃不一致的数据。
如果应用读取数据没有幂等处理,那么应用就需要过滤掉重复数据。写入方写入记录时额外写入一个唯一的标识(ident ifier),读取方读取数据后通过标识辨别之前是否已经处理过这个标识的数据。