前言
背景意义
分布式存储相关概念
分布式存储系统的数据可以分为以下三类
CAP理论
复制副本
一致性
GFS架构
租约(lease)和变更顺序
容错机制
因为我有一门分布式的课,老师要求我们选择一个课题做汇报,有GFS、Hadoop、Bigtable、MapReduce、Chubby、OpenStack等。内容不限,可以只讲某一个重要的点,也可以讲综述。因为我原先就看过一些分布式文件的书,所以就选择了GFS(因为看着眼熟)综述。虽然之前看的书中提及到了GFS,但是其原理并没有介绍的很清楚,所以在网上下载了GFS的论文。这篇论文介绍了开发和设计GFS时的思想,以及遇到问题后的解决思路。从存储的数据的特点出发去设计系统,以及论文中的一些设计思路,都能让我了解分布式文件系统底层的工作原理。本篇博文主要靠介绍GFS论文和HDFS漫画,让大家了解分布式文件系统。
Hadoop分布式文件系统(HDFS)被设计成适合运行在通用硬件上的分布式文件系统。它和现有的分布式文件系统有很多共同点。但同时,它和其他的分布式文件系统的区别也是很明显的。HDFS是一个高度容错性的系统,适合部署在廉价的机器上。HDFS能提供高吞吐量的数据访问,非常适合大规模数据集上的应用。HDFS可以看作是GFS的开源实现,它借鉴了许多GFS的设计思想以及实现方式。为了更了解HDFS的原理,阅读GFS的论文是必不可少的学习过程。如果你之前就已经看过分布式相关的书籍,或者已经花了很多时间学习HDFS,那么在看GFS这篇论文并不会感觉有什么难度。本篇博文搜集了HDFS的漫画,在学习相关知识时看这些漫画是非常有趣的。先了解下漫画中的角色信息(NameNode代表的是GFS master,即元数据服务器。DataNode代表的是GFS chunkserver,即块服务器):
为了满足Google迅速增长的数据处理需求,Google开发人员设计并实现了Google文件系统(Google File System)。GFS是一个面向大规模数据密集型应用的、可扩展的分布式文件系统。GFS与传统的分布式文件系统有着很多相同的设计目标,比如,性能、可扩展性、可靠性以及可用性。GFS的设计还考虑了Google应用的负载情况和环境的影响。GFS是一个分布式存储系统,即是大量普通PC服务器通过Internet互联,对外提供一个整体的存储服务。
开发人员重新审视了传统文件系统在设计上的选择,衍生出了完全不同的设计思路:
(1)结构化数据:一般存储在关系数据库中,可以用二维关系表结构来表示。结构化数据的模式和内容是分开的,数据模式需要预先定义。 国内开源的分布式数据库——阿里巴巴的OceanBase。
(2)非结构化数据:包括所有格式的办公文档、文本、图片、音频和视频信息等。这类数据以对象的形式组织,对象之间没有关联,这样的数据一般称为Blob(Binary Large Object)数据。分布式文件系统用于存储Blob数据,典型的系统有Fackbook Haystack、GFS以及HDFS。
(3)半结构化数据:介于非结构化数据和结构化数据之间,HTML文档就属于半结构化数据。它的模式结构和内容混在一起,没有明显的区分,也不需要预先定义数据的模式结构。典型的系统是Google Bigtable。
由于非结构化数据存储的数据是文档、文本、图片、音频和视频等信息。这类数据的特点是读多写少。所以才有前面提及的——绝大部分文件的修改是采用在文件尾部追加数据,而不是覆盖原有数据的方式。其实想一想,在使用百度云盘时,我们可以将数据上传、下载和删除。但不能修改上传的数据。一方面是因为类似的系统是读多写少的应用,另一方面是修改操作的一致性模型比较复杂。
Bigtable的论文标题: A Distributed Storage System for Structured Data。但在其简介中说了Bigtable不支持完整的关系数据模型;与之相反,Bigtable为客户提供了简单的数据模型。所以有的书籍将其存储的数据归为半结构化数据。
分布式系统的CAP理论:理论首先把分布式系统中的三个特性进行了如下归纳:
● 可用性(Availability):读写操作在单台机器发生故障的情况下仍然能够正常执行(例:程序bug),而不需要等待发生故障的机器重启,即保证其服务一直可用。
● 分区容错性(Partition tolerance):分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供完整的服务。
● 一致性(Consistency):即更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致,这就是分布式的一致性。
分区容错性和可用性给人的感觉很相似,它们的区别在于节点是不是宕掉了。作为一个分布式存储系统,即使某一节点宕机了,仍然要求能为用户提供完整的存储服务。所以分区可容忍性总是需要满足的,因此,一致性和可用性不可兼得。
为了保证分布式存储系统的高可靠和高可用,数据在系统中一般存储多个副本。当某个副本所在的存储节点出现故障时,分布式系统能够自动将服务切换到其他的副本,从而实现自动容错。由于多个副本的存在,带来了数据一致性问题。
如上图读操作,用户在读取数据时,可以从主副本中获取信息,也可以从两个备副本中获取信息。假如有3000个人在同时读取同一数据,理想状态下,系统可以这么多读请求分摊给三个副本所在的服务器上。这样的处理速度明显要比一台服务器处理3000个请求要快。另外一点,由于某些原因,第一个备副本损坏了,那么客户端仍然能从主副本和另外一个备副本中获取数据。
通过复制副本,使得系统变得更可靠了。但由于多个副本的存在,客户端在写或追加某一数据的时候,需要保证三个副本数据的一致性。如上图写操作,由于2个备副本存在,客户端在写入数据时就要将数据同步的写入到备副本中。这一过程中,不仅要加入同步机制保证数据都写到三个副本中,而且此时读该副本的数据(服务)是不可用的。这就是一致性和可用性不可兼得的原因。一致性和可用性不可兼得不是时时刻刻都存在的问题,只有系统中追加数据,为了保证数据一致性时,相应的读服务才不可用。
从客户端的角度来看,一致性包含如下三种情况:
(1)强一致性:假如A先写入了一个值到存储系统,存储系统保证后续A、B、C的读取操作都将返回最新值。
(2)弱一致性:假如A先写入了一个值到存储系统,存储系统不能保证后续A、B、C的读取操作是否能够读取到最新值。
(3)最终一致性:假如A先写入了一个值到存储系统,存储系统保证如果后续没有写操作更新同样的值,A、B、C的读取操作”最终”都会读取到A写入的最新值。”最终”一致性有一个”不一致窗口”的概念,它特指从A写入值,到后续A、B、C读取到最新值的这段时间。”不一致窗口”的大小依赖于以下几个因素:交互延迟,系统的负载,以及复制协议要求同步的副本数。
角色任务:Master(单一)用来存储元数据,即描述数据的数据。Chunkserver(多个)用来存储用户存放的数据。Client是服务的请求方。
Master节点执行所有的名称空间操作。此外,它还管理着整个系统里所有Chunk的副本:它决定Chunk的存储位置,创建新Chunk和它的副本,协调各种各样的系统活动以保证Chunk被完全复制,在所有的块服务器之间的进行负载均衡,回收不再使用的存储空间。
Master上保存了三种元数据信息:
(1)命名空间,也就是整个文件系统的目录结构以及chunk基本信息;
(2)文件到chunk之间的映射;
(3)chunk副本的位置信息,每个chunk通常有三个副本。
单一的Master节点的策略大大简化了我们的设计。单一的Master节点可以通过全局的信息精确定位Chunk的位置以及进行复制决策。另外,我们必须减少对Master节点的读写,避免Master节点成为系统的瓶颈。客户端并不通过Master节点读写文件数据。反之,客户端向Master节点询问应与它联系的块服务器。客户端将这些元数据信息缓存一段时间,后续的操作将直接和块服务器进行数据读写操作。
由上述架构图学习GFS的读流程。首先,客户端把文件名和程序指定的字节偏移,根据固定的Chunk大小,转换成文件的Chunk索引。然后,它把文件名和Chunk索引发送给Master节点。Master节点将相应的Chunk标识和副本的位置信息发还给客户端。客户端用文件名和Chunk索引作为Key缓存这些信息。
之后客户端发送请求到其中的一个副本,一般会选择最近的。请求信息包含了Chunk的标识和字节范围。在对这个Chunk的后续读取操作中,客户端不必再和Master节点通讯了,除非缓存的元数据信息过期或者文件被重新打开。实际上,客户端通常会在一次请求中查询多个Chunk信息,Master节点的回应也可能包含了紧跟着这些被请求的Chunk后面的Chunk的信息。在实际应用中,这些额外的信息在没有任何代价的情况下,避免了客户端和Master节点未来可能会多次发生通讯。
Master服务器并不保存持久化保存哪个块服务器存有指定Chunk的副本的信息。Master服务器只是在启动的时候轮询块服务器以获取这些信息。Master服务器能够保证它持有的信息始终是最新的,因为它控制了所有的Chunk位置的分配,而且通过周期性的心跳信息监控块服务器的状态。
最初设计时,我们试图把Chunk的位置信息持久的保存在Master服务器上,但是后来我们发现在启动的时候轮询块服务器,之后定期轮询更新的方式更简单。这种设计简化了在块服务器加入集群、离开集群、更名、失效、以及重启的时候,Master服务器和块服务器数据同步的问题。在一个拥有数百台服务器的集群中,这类事件会频繁的发生。
读取数据流程如下:
写入数据流程如下:
设计者在设计这个系统时,一个重要的原则是最小化所有操作与Master节点的交互。带着这样的设计理念,现在描述一下客户端、Master服务器和块服务器如何进行交互,以实现数据修改操作、原子的记录追加操作以及快照功能。
变更是一个会改变Chunk内容或者元数据的操作,比如写入操作或者记录追加操作。变更操作会在Chunk的所有副本上执行。设计者使用租约(lease)机制来保持多个副本间变更顺序的一致性。Master节点为Chunk的一个副本建立一个租约,把这个副本叫做主Chunk。主Chunk对Chunk的所有更改操作进行序列化。所有的副本都遵从这个序列进行修改操作。因此,修改操作全局的顺序首先由Master节点选择的租约的顺序决定,然后由租约中主Chunk分配的***决定。
设计租约机制的目的是为了最小化Master节点的管理负担。租约的初始超时设置为60秒。不过,只要Chunk被修改了,主Chunk就可以申请更长的租期,通常会得到Master节点的确认并收到租约延长的时间。这些租约延长请求和批准的信息通常都是附加在Master节点和块服务器之间的心跳消息中来传递。有时Master节点会试图提前取消租约(例如,Master节点想取消在一个已经被改名的文件上的修改操作)。即使Master节点和主Chunk失去联系,它仍然可以安全地在旧的租约到期后和另外一个Chunk副本签订新的租约。
GFS提供了一种原子的数据追加操作-记录追加(Record append)。传统方式的写入操作,客户程序会指定数据写入的偏移量。对同一个region的并行写入操作不是串行的:region尾部可能会包含多个不同客户机写入的数据片段。使用记录追加,客户机只需要指定要写入的数据。GFS保证至少有一次原子的写入操作成功执行(即写入一个顺序的byte流),写入的数据追加到GFS指定的偏移位置上,之后GFS返回这个偏移量给客户机。这类似于在Unix操作系统编程环境中,对以O_APPEND模式打开的文件,多个并发写操作在没有竞态条件时的行为。
记录追加在我们的分布式应用中非常频繁的使用,在这些分布式应用中,通常有很多的客户机并行地对同一个文件追加写入数据。如果我们采用传统方式的文件写入操作,客户机需要额外的复杂、耗时的同步机制,例如使用一个分布式的锁管理器。在我们的工作中,这样的文件通常用于多个生产者/单一消费者的队列系统,或者是合并了来自多个客户机的数据结果的文件。
记录追加是一种修改操作,除了在主Chunk有些额外的控制逻辑。客户机把数据推送给文件最后一个Chunk的所有副本,之后发送请求给主Chunk。主Chunk会检查这次记录追加操作是否会使Chunk超过最大容量(64MB)。如果超过了最大size,主Chunk首先将当前Chunk填充到最大容量,之后通知所有二级副本做同样的操作,然后回复客户机要求其对下一个Chunk重新进行记录追加操作。(记录追加的数据大小严格控制在Chunk size的1/4,这样即使在最坏情况下,数据碎片的数量仍然在可控的范围。)通常情况下追加的记录不超过Chunk的最大size,主Chunk把数据追加到自己的副本内,然后通知二级副本把数据写在跟主Chunk一样的位置上,最后回复客户机操作成功。
如果记录追加操作在任何一个副本上失败了,客户端就需要重新进行操作。重新进行记录追加的结果是,同一个Chunk的不同副本可能包含不同的数据-重复包含一个记录全部或者部分的数据。GFS并不保证Chunk的所有副本在字节级别是完全一致的。它只保证数据作为一个整体,原子的被至少写入一次。这个特
性可以通过简单观察推导出来:如果操作成功执行,数据一定已经写入到Chunk的所有副本的相同偏移位置上。这之后,所有的副本至少都到了记录尾部,任何后续的记录都会追加到更大的偏移地址,或者是不同的Chunk上,即使其它的Chunk副本被Master节点选为了主Chunk。就我们的一致性保障模型而言,记录追加操作成功写入数据的region是已定义的(因此也是一致的),反之则是不一致的(因此也就是未定义的)。
追加数据的流程图:
为了提高网络效率,我们采取了把数据流和控制流分开的措施。在控制流从客户机到主Chunk、然后再到所有二级副本的同时,数据以管道的方式,顺序的沿着一个精心选择的块服务器链推送。我们的目标是充分利用每台机器的带宽,避免网络瓶颈和高延时的连接,最小化推送所有数据的延时。
为了充分利用每台机器的带宽,数据沿着一个块服务器链顺序的推送,而不是以其它拓扑形式分散推送(例如,树型拓扑结构)。线性推送模式下,每台机器所有的出口带宽都用于以最快的速度传输数据,而不是在多个接收者之间分配带宽。