典型分布式系统分析: GFS
本文是典型分布式系统分析系列的第二篇,关注的是GFS,一个分布式文件存储系统。在前面介绍MapReduce的时候也提到,MapReduce的原始输入文件和最终输出都是存放在GFS上的,GFS保证了数据的可用性与可靠性,那么本文具体看看GFS是怎么做到的。
GFS(Google File System)是Google研发的可伸缩、高可用、高可靠的分布式文件系统,提供了类似POSIX的API,按层级目录来组织文件。在网络上,有很多对该轮文的翻译和解读,尤其是经典论文翻译导读之《Google File System》这篇文章,除了对论文的翻译,还有很多作者的思考、分析。而在本文中,还是首先介绍GFS系统设计中的一些要点,然后从伸缩性、可用性、一致性等方面进行探讨。
本文地址:http://www.cnblogs.com/xybaby/p/8967424.html
GFS系统简介
任何系统都是有自己适用的场景的,所以我们讨论一个系统的时候,首先得明确的是,这个系统是在什么样的环境下产生的,是为了什么目标而产生的,做了哪些假设或者限制。
系统是构建在普通的、廉价的机器上,因此故障是常态而不是意外
系统希望存储的是大量的大型文件(单个文件size很大)
系统支持两种类型读操作:大量的顺序读取以及小规模的随机读取(large streaming reads and small random reads.)
系统的写操作主要是顺序的追加写,而不是覆盖写
系统对于大量客户端并发的追加写有大量的优化,以保证写入的高效性与一致性,主要归功于原子操作record append
系统更看重的是持续稳定的带宽而不是单次读写的延迟
GFS架构
一份文件被分为多个固定大小的chunk(默认64M),每个chunk有全局唯一的文件句柄 -- 一个64位的chunk ID,每一份chunk会被复制到多个chunkserver(默认值是3),以此保证可用性与可靠性。chunkserver将chunk当做普通的Linux文件存储在本地磁盘上。
GFS master是系统的元数据服务器,维护的元数据包括:命令空间(GFS按层级目录管理文件)、文件到chunk的映射,chunk的位置。其中,前两者是会持久化的,而chunk的位置信息来自于Chunkserver的汇报。
GFS client是给应用使用的API,这些API接口与POSIX API类似。GFS Client会缓存从GFS master读取的chunk信息(即元数据),尽量减少与GFS master的交互。
应用程序调用GFS client提供的接口,表明要读取的文件名、偏移、长度。
GFS Client将偏移按照规则翻译成chunk序号,发送给master
master将chunk id与chunk的副本位置告诉GFS client
GFS client向最近的持有副本的Chunkserver发出读请求,请求中包含chunk id与范围
ChunkServer读取相应的文件,然后将文件内容发给GFS client。
GFS副本控制协议
在《带着问题学习分布式系统之中心化复制集》一文中,介绍过分布式系统中常用的副本控制协议。GFS为了可用性与可靠性,而且使用的都是普通廉价的机器,因此也采用了冗余副本机制,即将同一份数据(chunk)复制到在个物理机上(chunkserver)。
中心化副本控制协议
数据冗余的粒度
以机器为单位,即若干机器互为副本,副本机器之间的数据完全相同。有点是非常简单,元数据更少。缺点是回复数据时效率不高、伸缩性不好,不能充分利用资源。
在GFS中,数据段即为Chunk,上面提到,这样元数据会多一些,且GFS master本身又是单点,这个有没有问题呢。GFS说,问题不大,因为GFS中,一个Chunk的信息,64byte就够了,且Chunk本身的粒度又是很大的(64M),所以数据量不会太大,而且,在GFS master中,chunk的位置信息是不持久化的。
在MongoDB中,则是以机器为粒度进行副本冗余的。
数据写入过程
在GFS中,数据流与控制流是分开的,如图所示
step1 Client向master请求Chunk的副本信息,以及哪个副本(Replica)是Primary
step2 maste回复client,client缓存这些信息在本地
step2 client将数据(Data)链式推送到所有副本
step4 Client通知Primary提交
step5 primary在自己成功提交后,通知所有Secondary提交
step6 Secondary向Primary回复提交结果
step7 primary回复client提交结果
首先,为什么将数据流与控制消息分开,且采用链式推送方法呢,目标是最大化利用每个机器的网络带宽,避免网络瓶颈和高延迟连接,最小化推送延迟。
另外一种推送方式是主从模式:
Client首先将数据推送到Primary,再由Primary推送到所有secodnary。很明显,Primary的压力会很大,在GFS中,既然是为了最大化均衡利用网络带宽,那么就不希望有瓶颈。而且,不管是Client还是replica都知道哪个节点离自己更近,所以能选出最优的路径。
而且,GFS使用TCP流式传输数据,以最小化延迟。一旦chunkserver收到数据,即立刻开始推送,即一个replica不用收到完整的数据再发往下一个replica。
同步的数据写入
上述流程中第3三步,只是将数据写到了临时缓存,真正生效还需要控制消息(第4 5步)。在GFS中,控制消息的写入是同步的,即Primary需要等到所有的Secondary的回复才返回客户端。这就是write all, 保证了副本间数据的一致性,因此可以读取的时候就可以从任意副本读数据。关于同步写入、异步写入,可参考《Distributed systems for fun and profit》。
副本一致性保证
副本冗余的最大问题就是副本一致性问题:从不同的副本读到的数据不一致。
这里有两个术语:consistent, defined
consistent:对于文件区域A,如果所有客户端从任何副本上读到的数据都是相同的,那A就是一致的。
defined:如果A是一致的,并且客户端可以看到变异(mutation)写入的完整数据,那A就是defined,即结果是可预期的。
显然,defined是基于consistent的,且有更高的要求。
表1中,对于写操作(write,在用户指定的文件偏移处写入),如果是顺序写,那么一定是defined;如果是并发写,那么各个副本之间一定是一致的,但结果是undefined的,可能会出现相互覆盖的情况。而使用GFS提供的record append这个原子操作(关于append,可以操作linux 的O_APPEND选项,即声明是在文件的末尾写入),内容也一定是defined。但是在表1中,写的是“interspersed with inconsistent”,这是因为如果某个chunkserver写入数据失败,都会从写入流程的step3开始重试,这就导致chunk中有一部分数据在不同的副本中是不一致的。
record append保证了原子性写入,而且是at least once,但不保证只写入了一次,有可能写入了一部分(padding)就异常了,然后需要重试;也有可能是由于其他副本写入失败,即使自己写入成功了,也要再重新写入一份。
GFS提供的一致性保证称之为“relaxed consistency”,relaxed是指,系统在某些情况下是不保证一致性,比如读取到尚未完全写完的数据(数据库中的Dirty Read);比如上面提到的padding(可以使用checksum机制解决);比如上面提到的重复的append数据(读取数据的应用自行保证幂等性)。在这些异常情况下,GFS是不保证一致性的,需要应用程序来处理。
个人觉得,多个副本的写入其实也是一个分布式事务事务,要么都写入,要么都不写入,如果采用类似2PC的方法,那么就不会出现padding或者重复,但是2PC代价是昂贵的,非常影响性能,所以GFS采取重试的方法来应对异常,将问题抛给应用程序。
高性能、高可用的GFS master
在GFS中,master是单点,任意时刻,只有一个master处于active状态。单点简化了设计,集中式调度方便很多,也不用考虑糟心的“脑裂”问题。但是单点对系统的吞吐能力、可用性提出了挑战。那么如何避免单点成为瓶颈?两个可行的办法:减少交互,快速的failover。
master需要在内存中维护元数据,同时与GFS client,chunkserver交互。至于内存,问题并不大,因为GFS系统通常处理的是大文件(GB为单位)、大分块(默认64M)。每个64M的chunk,对应的元数据信息不超过64byte。而对于文件,使用了文件命令空间,使用前缀压缩的话,单个文件的元数据信息也少于64byte。
GFS client尽量较少与GFS master的交互:缓存与批量读取(预读取)。首先,允许Chunk的size比较大,这就减少了客户端想master请求数据的概率。另外,client会将chunk信息缓存在本地一段时间,直到缓存过期或者应用重新打开文件,而且,GFS为chunk分配有递增的版本号(version),client访问chunk的时候会携带自己缓存的version,解决了缓存不一致的问题。
In fact, the client typically asks for multiple chunks in the same request and the master can also include the informationfor chunks immediately following those requested. This extra information sidesteps several future client-master interactionsat practically no extra cost.
master的高可用是通过操作日志的冗余 + 快速failover来实现的。
master需要持久化的数据(文件命令空间、文件到chunk的映射)会通过操作日志与checkpoint的方式存储到多台机器,只有当元数据操作的日志已经成功flush到本地磁盘和所有master副本上才会认为其成功。这是一个write all的操作,理论上会对写操作的性能有一定的影响,因此GFS会合并一些写操作,一起flush,尽量减少对系统吞吐量的影响。
对于chunk的位置信息,master是不持久化的,而是在启动的时候从chunkserver查询,并在与chunkserver的常规心跳消息中获取。虽然chunk创建在哪一个chunkserver上是master指定的,但只有chunkserver对chunk的位置信息负责,chunkserver上的信息才是实时准确的,比如说当chunkserver宕掉的时候。如果在master上也维护chunk的位置信息,那么为了维护一致性视图就得增加很多消息和机制。
a chunkserver has the final word over what chunks it does or does not have on its own disks.
There is no point in trying to maintain a consistent view of this information on the master because errors on a chunkserver may cause chunks to vanish spontaneously (e.g., a disk may go bad and be disabled) or an operator may rename a chunkserver.
如果master故障,几乎是可以瞬时重启,如果master机器故障,那么会在另一台冗余机器上启动新的master进程,当然,这个新的机器是持有所有的操作日志与checkpoint信息的。
master 重新启动之后(不管是原来的物理机重启,还是新的物理机),都需要恢复内存状态,一部分来之checkpoint与操作日志,另一部分(即chunk的副本位置信息)则来自chunkserver的汇报。
系统的伸缩性、可用性、可靠性
作为一个分布式存储系统,需要良好的伸缩性来面对存储业务的增长;需要在故障成为常态的时候保证高可用;最为重要的,需要保证数据的可靠性,这样应用才放心将数据存放在系统中。
伸缩性
GFS具有良好的伸缩性,直接往系统中添加Chunkserver即可,而且前面提到,由于是以chunk为粒度进行副本冗余,允许每次增加一个ChunkServer。系统理论上的瓶颈在于master,因此master是单点,需要在内存中维护诸多元数据,需要与GFS client、GFS chunkserver交互,但基于上面的分析,master也很难成为事实上的瓶颈。系统以chunk为粒度进行副本冗余,这样当往系统中添加、删除机器的时候,也不会某个chunkserver、某个文件有较大影响。
可用性
元数据服务器(GFS master)的可用性保证在上面已经提到了,这里再来看看用户数据(文件)的可用性。
数据以chunk为单位冗余在多个chunkserver上,而且,默认是跨机架(rack)的冗余,这样及时出现了影响整个机架的故障(如交换机故障、电力故障)也不会对可用性有影响。而且,跨机架也能更好的均摊对数据的读操作,更充分利用网络带宽,让应用程序更可能地找到最近的副本位置。
当Master发现了某个chunk的冗余副本数目达不到要求时(比如某个chunkserver宕掉),会为这个chunk补充新的副本;当有新的chunkserver添加到系统中时,也会进行副本迁移--将chunk为负载较高的chunkserver迁移到负载低的chunkserver上,达到动态负载均衡的效果。
当需要选择一个chunkserver来持久化某个chunk时,会考虑以下因素:
- 选择磁盘利用率降低的chunkserver;
- 不希望一个chunkserver在短时间创建大量chunk;
- chunk要跨机架
可靠性
可靠性指数据不丢失、不损坏(data corruption)。副本冗余机制保证了数据不会丢失;而GFS还提供了checksum机制,保证数据不会因为磁盘原因损坏。
关于checksum,一个chunk被分解为多个64KB的块,每个块有对应32位的checksum。checksum被保存在内存中,并用利用日志持久化保存,与用户数据是隔离的,当然,这里的内存和持久化都是在chunkserver上。当chunkserver收到读数据请求的时候,会比对文件数据与对应的checksum,如果发现不匹配,会告知client,client从其他的读取;同时,也会告知master,master选择新的chunkserver来restore这个损坏的chunk
其他
第一:chunk惰性分配存储空间
第二:使用copy on write来创建快照(snapshot)
第三:解决问题的最好方法就是不解决,交给使用者来解决:
第一点,GFS对于文件的并发读写并不保证一致性,一来标准文件API就没有保证,二来把这种问题交给用户自己处理也大大简化了系统的设计
第二点,由于Chunk size较大,那么当文件较小时就只有一个chunk,如果文件读取频繁,对应的chunkserver就可能成为压力。解决办法就是用户提高冗余级别,然后不要集中在一个时间读取文件,分摊chunkserver的压力。
第四:防止文件命名空间死锁的方法:
一个操作必须按特定的顺序来申请锁:首先按命名空间树的层级排序,在相同层级再按字典序。
they are first ordered by level in the namespace tree and lexicographically within the same level