目录
背景意义
分布式存储相关概念
分布式存储系统的分类
复制副本
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 互联,对外提供一个整体的存储服务。
开发人员重新审视了传统文件系统在设计上的选择,衍生出了完全不同的设计思路:
- 组件失效被认为是常态事件,而不是意外事件(系统运行在普通的 PC 上)。原因包括程序 bug、操作系统 bug、人为失误,甚至还有硬盘、内存、连接器、网络以及电源失效等。
- 文件非常巨大,I/O 操作和数据块的大小都需要重新考虑(块大小影响元数据表)。
- 绝大部分文件的修改是采用在文件尾部追加数据,而不是覆盖原有数据的方式(具体原因看存储的数据类型)。
- 引入了原子性的记录追加操作,从而保证多个客户端能够同时进行追加操作,不需要额外的同步操作来保证数据的一致性。
分布式存储相关概念
分布式存储系统的分类
(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 为客户提供了简单的数据模型。所以有的书籍将其存储的数据归为半结构化数据。
复制副本
为了保证分布式存储系统的高可靠和高可用,数据在系统中一般存储多个副本。当某个副本所在的存储节点出现故障时,分布式系统能够自动将服务切换到其他的副本,从而实现自动容错。由于多个副本的存在,带来了数据一致性问题,这个将在后面进行说明。
如上图读操作,用户在读取数据时,可以从主副本中获取信息,也可以从两个备副本中获取信息。假如有 3000 个人在同时读取同一数据,理想状态下,系统可以将这么多读请求分摊给三个副本所在的服务器上。这样的处理速度明显要比一台服务器处理 3000 个请求要快。另外一点,由于某些原因,第一个备副本损坏了,那么客户端仍然能从主副本和另外一个备副本中获取数据。
通过复制副本,使得系统变得更可靠了。但由于多个副本的存在,客户端在写或追加某一数据的时候,需要保证三个副本数据的一致性。如上图写操作,由于 2 个备副本存在,客户端在写入数据时就要将数据同步的写入到备副本中。这一过程中,不仅要加入同步机制保证数据都写到三个副本中,而且此时读该副本的数据(服务)是不可用的。这就是一致性和可用性不可兼得的原因。一致性和可用性不可兼得不是时时刻刻都存在的问题,只有系统中追加数据,为了保证数据一致性时,相应的读服务才不可用。
CAP理论
分布式系统的CAP理论:理论首先把分布式系统中的三个特性进行了如下归纳:
● 可用性(Availability):读写操作在单台机器发生故障的情况下仍然能够正常执行(例:程序bug),而不需要等待发生故障的机器重启,即保证其服务一直可用。
● 分区容错性(Partition tolerance):分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供完整的服务。
● 一致性(Consistency):即更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致,这就是分布式的一致性。
分区容错性和可用性给人的感觉很相似,它们的区别在于节点是不是宕掉了。作为一个分布式存储系统,即使某一节点宕机了,仍然要求能为用户提供完整的存储服务。所以分区可容忍性总是需要满足的,因此,一致性和可用性不可兼得。
一致性
从客户端的角度来看,一致性包含如下三种情况:
(1) 强一致性:假如 A 先写入了一个值到存储系统,存储系统保证后续 A、B、C 的读取操作都将返回最新值。
(2) 弱一致性:假如 A 先写入了一个值到存储系统,存储系统不能保证后续 A、B、C 的读取操作是否能够读取到最新值。
(3) 最终一致性:假如 A 先写入了一个值到存储系统,存储系统保证如果后续没有写操作更新同样的值,A、B、C 的读取操作 "最终" 都会读取到 A 写入的最新值。"最终" 一致性有一个 "不一致窗口" 的概念,它特指从 A 写入值,到后续 A、B、C 读取到最新值的这段时间。"不一致窗口" 的大小依赖于以下几个因素:交互延迟,系统的负载,以及复制协议要求同步的副本数。
GFS 架构
角色任务: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 服务器和块服务器数据同步的问题。在一个拥有数百台服务器的集群中,这类事件会频繁的发生。
读取数据流程如下:
写入数据流程如下:
租约(lease)和变更顺序
设计者在设计这个系统时,一个重要的原则是最小化所有操作与 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 是已定义的(因此也是一致的),反之则是不一致的(因此也就是未定义的)。
追加数据的流程图:
- 客户端向 Master 节点询问哪一个块服务器持有当前的租约,以及其它副本的位置。如果没有一个 Chunk 持有租约,Master 节点就选择其中一个副本建立一个租约(这个步骤在图上没有显示)。
- Master 节点将主 Chunk 的标识符以及其它副本(又称为 secondary 副本、二级副本)的位置返回给客户端。客户端缓存这些数据以便后续的操作。只有在主 Chunk 不可用,或者主 Chunk 回复信息表明它已不再持有租约的时候,客户端才需要重新跟 Master 节点联系。
- 客户端把数据推送到所有的副本上。客户端可以以任意的顺序推送数据。块服务器接收到数据并保存在它的内部 LRU 缓存中,一直到数据被使用或者过期交换出去。由于数据流的网络传输负载非常高,通过分离数据流和控制流,我们可以基于网络拓扑情况对数据流进行规划,提高系统性能,而不用去理会哪个块服务器保存了主 Chunk。
- 当所有的副本都确认接收到了数据,客户端发送写请求到主块服务器。这个请求标识了早前推送到所有副本的数据。主 Chunk 为接收到的所有操作分配连续的序列号,这些操作可能来自不同的客户端,序列号保证了操作顺序执行。它以序列号的顺序把操作应用到它自己的本地状态中(It applies the mutation to its own local state in serial number order. mutation 是改变的意思,这里根据上下文翻译成改变状态的操作)。
- 主 Chunk 把写请求传递到所有的二级副本。每个二级副本依照主 Chunk 分配的序列号以相同的顺序执行这些操作。
- 所有的二级副本回复主 Chunk,它们已经完成了操作。
- 主块服务器(主 Chunk 所在的块服务器)回复客户端。任何副本产生的任何错误都会返回给客户端。在出现错误的情况下,写入操作可能在主 Chunk 和一些二级副本执行成功。(如果操作在主 Chunk 上失败了,操作就不会被分配序列号,也不会被传递)客户端的请求被确认为失败,被修改的 region 处于不一致的状态。我们的客户端代码通过重复执行失败的操作来处理这样的错误。在从头开始重复执行之前,客户机会先从步骤(3)到步骤(7)做几次尝试。
- 如果应用程序一次写入的数据量很大,或者数据跨越了多个 Chunk,GFS 客户端代码会把它们分成多个写操作。这些操作都遵循前面描述的控制流程,但是可能会被其它客户端上同时进行的操作打断或者覆盖。因此,共享的文件 region 的尾部可能包含来自不同客户端的数据片段,尽管如此,由于这些分解后的写入操作在所有的副本上都以相同的顺序执行完成,Chunk 的所有副本都是一致的。
为了提高网络效率,我们采取了把数据流和控制流分开的措施。在控制流从客户机到主 Chunk、然后再到所有二级副本的同时,数据以管道的方式,顺序的沿着一个精心选择的块服务器链推送。我们的目标是充分利用每台机器的带宽,避免网络瓶颈和高延时的连接,最小化推送所有数据的延时。
为了充分利用每台机器的带宽,数据沿着一个块服务器链顺序的推送,而不是以其它拓扑形式分散推送(例如,树型拓扑结构)。线性推送模式下,每台机器所有的出口带宽都用于以最快的速度传输数据,而不是在多个接收者之间分配带宽。
容错机制