设计概述
设计预期
- 组件故障处于常态,系统需要能迅速侦测、冗余并灰度失效组件
- 系统能存储一定数量的大文件(通常是在100MB以上),同时也支持存储小文件,但是不需要对小文件进行优化
- 主要是2种读操作,分别是大规模的流式读取和小规模的随机读取(如果对性能有要求,可以将小规模的随机读取合并并排序,之后按照顺序批量读取)
- 主要是2种写操作,分别是大规模的顺序追加写和小规模的随机位置写入
- 系统必须高效并且行为是明确定义的
- 高性能的稳定网络带宽比低延迟重要
接口
提供一套类似传统文件系统的API接口函数,虽然并不是严格按照POSIX等标准API的形式定义。另外还提供了快照和append功能,快照可以以很低的成本创建一个文件或者目录树的拷贝;append功能允许多个客户端同时对一个文件进行数据追加操作,同时保证每个客户端的追加操作都是原子的
架构
- Master服务器会给每个Chunk分配一个全局不变的全球唯一的64位Chunk标识,同时会管理所有的元数据,包括名字空间、访问控制信息、文件和Chunk的映射关系以及当前的Chunk的位置信息,还管理着Chunk租用关系、孤儿Chunk的回收,以及Chunk的数据迁移
- Chunk服务器以linux文件的形式保存在本地硬盘上,并根据制定的chunk标识和字节范围来读写数据块,文件系统会缓存文件到内存中
- 客户端实现GFS文件系统的API接口函数、应用程序与Master节点和Chunk服务器的通讯、以及对数据进行读写操作。客户端只和Master节点获取元数据(会缓存),所有的数据操作都和Chunk服务进行交互。并且不缓存任何数据(Chunk服务器一样),因为大部分的程序要么以流的方式读取,要么数据太大无法被缓存
单一Master节点
优点:单一的Master节点可以通过全局的信息精确定位Chunk的位置以及进行复制决策 缺点:需要降低Master的读写操作避免Master节点成为系统的瓶颈 Master 节点管理着整个文件系统,主要涉及以下几个方面:
-
整个文件系统的元数据:包括命名空间(namespace)、访问控制信息(access control information)、文件与Chunk Server的映射关系以及每个 Chunk 的当前位置(the current locations of chunks)
-
文件系统的动态信息:Chunk 租用管理(chunk leases management)、孤儿 Chunk 的回收(garbage collection of orphaned chunks)以及Chunk 在 Chunk Server 之间的迁移
-
使用心跳周期性与每个Chunk Server通信,发送至指令到各个Chunk Server并接受Chunk Server的状态信息
-
接受并回应客户端的操作请求
数据结构
- 文件名到Chunk ID或者Chunk Handle数组的对应,表示文件对应了哪些Chunk,该数据会写入磁盘
- Chunk ID到Chunk数据的对应关系
- 每个Chunk存储在哪些服务器,该数据不用更新到磁盘
- 每个Chunk当前的版本号,该数据需要更新到磁盘
- 哪个Chunk服务器持有主Chunk,该数据不用更新到磁盘
- 主Chunk的租约过期时间,该数据不用更新到磁盘
需要新增一个Chunk或者由于指定了新的主Chunk而导致版本号更新了,Master节点需要向磁盘中的Log追加一条记录
简单读取的流程
- 客户端把文件名和程序指定的字节偏移,根据固定的 Chunk大小,转换成文件的Chunk索引
- 它把文件名和Chunk索引发送给Master节点
- Master节点将相应的Chunk标识和副本的位置信息发还给客户端
- 客户端用文件名和Chunk索引作为key缓存这些信息
- 客户端会发送请求(包含Chunk的标识和字节范围)到其中的一个副本处,一般会选择最近的
- 在后续对这个Chunk的后续读取操作中,客户端不会跟Master再通讯,除非缓存的元信息过期或者文件被重新打开 2.客户端通常会在一次请求中查询多个Chunk信息,Master节点的回应也可能包含了紧跟着这些请求的Chunk的后面的Chunk信息 3.客户端本身依赖了一个GFS的库,这个库会注意到读请求跨越了Chunk的边界 ,并会将读请求拆分,之后再将它们合并起来。所以这个库会与Master节点交互,Master节点会告诉这个库说Chunk7在这个服务器,Chunk8在那个服务器。之后这个库会说,我需要Chunk7的最后两个字节,Chunk8的头两个字节。GFS库获取到这些数据之后,会将它们放在一个buffer中,再返回给调用库的应用程序。Master节点会告诉库有关Chunk的信息,而GFS库可以根据这个信息找到应用程序想要的数据。应用程序只需要确定文件名和数据在整个文件中的偏移量,GFS库和Master节点共同协商将这些信息转换成Chunk
Chunk尺寸
选择较大的Chunk尺寸的有几个优点
- 减少客户端和Master的节点通讯,客户端可以缓存更多的Chunk的数据
- 客户端可以对一个Chunk进行多次操作,通过与Chunk服务器保存较长时间的TCP连接来减少网络负载
- 减少Master节点需要保存的元数据的数量
但大的Chunk尺寸也是有其缺陷的,即小文件包含较少的Chunk甚至只有一个Chunk,当有许多的客户端对同一个小文件进行多次访问时,存储这些Chunk的Chunk服务器就会变成热点。这个问题可以通过增大复制因子来解决。
这里还有一个值得思考的地方,即GFS对数据的冗余是以Chunk为基本单位而不是机器(或者说文件)。以机器为单位进行冗余的优点是简单方便,但是伸缩性不好,不能充分利用资源;而以Chunk为基本单位,虽然Master节点上需要存更多的元数据,但一个Chunk的信息也就64字节左右,而Chunk本身的粒度又有64M这么大,加之在Master中Chunk的位置信息是不持久化的,所以这并不会给Master带来太多的负担。
元数据
Master服务器存储3种主要类型的元数据:
- 文件和Chunk的命名空间
- 文件和Chunk的映射关系
- 每个Chunk的存放位置
前2种会以记录变更日志的方式记录到操作系统的系统日志中,日志文件存储在本地磁盘上,同时日志会被复制到其他的Master机器上;对于第3种不会持久化
内存中的数据结构
Master会周期性扫描自己保存的全部信息,用以实现Chunk垃圾回收、Chunk服务器数据重新复制、Chunk迁移实现的负载均衡以及磁盘使用状况的统计功能
Chunk位置信息
只有Chunk服务器才能最终确定一个Chunk是否在它的硬盘 上。我们从没有考虑过在Master服务器上维护一个这些信息的全局视图,因为Chunk服务器的错误可能会导致Chunk自动消失(比如,硬盘损坏了或者无法访问了),亦或者操作人员可能会重命名一个Chunk服务器。
操作日志
只有在元数据的变化日志被持久化之后,才对客户端是可见。为了缩短Master的启动时间,日志在增长到一定的数量的时候会对系统做一个Checkpoint(为了不阻塞正在进行的操作,会使用独立线程进行持久化),将所有的状态数据写入到一个Checkpoint文件,在恢复的时候Master服务器通过从磁盘上读取Checkpoint并回放Checkpoint之后的有限个日志文件就能恢复
一致性模型
随机写 | 追加写 | |
---|---|---|
串行成功 | 已定义 | 已定义,但部分不一致 |
并行成功 | 一致但是未定义 | 已定义,但部分不一致 |
失败 | 不一致 | 不一致 |
一致是指当客户端需要读取文件内某一个区域时,无论客户端是从哪个副本读的,看到的内容都是一样的 已定义的前提是一致的,同时需要满足当客户端写入了一块内容后再去读,可以读到之前写入的全部内容
下面通过几个例子详细解释一致和已定义的区别,以及随机写和追加写的一致性问题。这里有一个前提条件是客户端可能会有重试,即无论是顺序成功还是并行成功,都会有这种情况:即客户端某一次写入失败了,而后重试若干次最后写入成功。这种情况下也属于写入成功,而不属于写入失败,只有重试后依然失败的才算写入失败
随机写
随机写由客户端指定写入的位置和内容
并行成功
由于客户端知道写入的位置,所以无论是否存在重试写入位置和内容都是固定的,并且同一个chunk下所有副本写入的顺序又是一致的,所以只要最后全部写入都成功了那么文件最终一定是一致的。但并行成功是未定义的,因为客户端的写入范围可能会有交叉的情况,考虑下面这个例子:
初始状态下,文件内容为
位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
字节 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
此时存在两个客户端C1和C2,其中C1写入的位置和内容是[4, “11111”],C2写入的位置和内容是[7, “222”],由于C1和C2的写入顺序不是固定的(同一个chunk由主chunk决定顺序)
Case1:假如C1先于C2执行,且写入同时完成,文件内容最终为
位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
字节 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 2 | 2 | 2 | 0 |
Case2:假如C2先于C1执行,且写入同时完成,文件内容最终为
位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
字节 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 2 | 0 |
甚至还有一种极端的情况(Case3):假如chunk大小为8,那么C1和C2都需要写入chunk1和chunk2,而每个chunk又由自己的主chunk决定顺序,所以很可能发生chunk1中C1先于C2写入,而chunk2中C2先于C1写入,最后写入同时完成,那么最终文件内容为
位置 | 0(chunk1) | 1(chunk1) | 2(chunk1) | 3(chunk1) | 4(chunk1) | 5(chunk1) | 6(chunk1) | 7(chunk1) | 8(chunk2) | 9(chunk2) | 10(chunk2) |
---|---|---|---|---|---|---|---|---|---|---|---|
字节 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 2 | 1 | 2 | 0 |
那么如果站在客户端的角度,实际上在写入完成之后并不一定可以看到自己写入的全部内容,例如Case1中只有C2可以看到自己写入的全部内容,Case2中只有C1能看到自己写入的全部内容,Case3中每个客户端都看不到自己写入的全部内容
顺序成功
同并行成功,顺序成功无论是否有重试也不会影响最终的一致性。而顺序成功意味着只有一个写者在写入文件,显然写者在写入后可以看到自己写入的全部内容,所以是已定义的
写入失败
由于失败可能发生在任何时候,假如写chunk一半时失败、前面的chunks写入成功后面某个chunk写入失败等等,所以一旦失败就会发生不一致
追加写
在GFS中,追加写的写操作是原子的
并行成功/顺序成功
由于追加写是原子的,所以每个追加写的范围是两两不重叠的,即每个写者写入成功后都可以看到自己写入的全部内容,所以追加写的写入是已定义的,与是否并行无关
又因为追加写的特性是每次在文件结尾写,即每次写的位置都是文件末尾,所以客户端一旦发生重试那么之前写入的内容就会不一致了,考虑下面这种情况
初始状态下,文件内容为”000”,并且存在3个副本,此时文件是一致的
- 副本1(主chunk)
位置 | 0 | 1 | 2 |
---|---|---|---|
字节 | 0 | 0 | 0 |
- 副本2
位置 | 0 | 1 | 2 |
---|---|---|---|
字节 | 0 | 0 | 0 |
- 副本3
位置 | 0 | 1 | 2 |
---|---|---|---|
字节 | 0 | 0 | 0 |
此时发生追加写”aaa”,并且主chunk写入成功了,但在副本2写到一半时失败了,此时文件内容为
- 副本1(主chunk)
位置 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
字节 | 0 | 0 | 0 | a | a | a |
- 副本2
位置 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
字节 | 0 | 0 | 0 | a | a |
- 副本3
位置 | 0 | 1 | 2 |
---|---|---|---|
字节 | 0 | 0 | 0 |
接着客户端感知到错误,并且发起重试并且重试成功。由于每个副本的写入位置需要一致,所以当客户端重试时,副本2和副本3需要先填充特殊字符让长度和主chunk一致,接着再写入最新的内容,此时文件内容为
- 副本1(主chunk)
位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
字节 | 0 | 0 | 0 | a | a | a | a | a | a |
- 副本2
位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
字节 | 0 | 0 | 0 | a | a | x | a | a | a |
- 副本3
位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
字节 | 0 | 0 | 0 | x | x | x | a | a | a |
此时三个副本在[3, 5]范围内是不一致的。 所以GFS要求客户端在追加写时还需要额外做一些控制信息来保证读出的数据是正确的,详见论文的2.7.2 Implications for Applications小节
写入失败
和随机写一样,只要失败就会发生不一致
系统交互
租约和变更顺序
- 使用租约机制来保持多个副本间变更顺序的一致性,主chunk对chunk的所有更改操作进行序列化。
- 主chunk可以通过和Master节点之间的心跳来申请延长续约,有时Master节点会提前取消租约。
- 即使Master节点和主chunk失去联系,它仍然可以安全地在旧的租约到期后和另外一个chunk副本签订新的租约。
当客户端想要对文件进行追加,但是又不知道文件尾的Chunk对应的Primary在哪时,Master会等所有存储了最新Chunk版本的服务器集合完成,然后挑选一个作为Primary,其他的作为Secondary。之后,Master会增加版本号,并将版本号写入磁盘,接下来,Master节点会向Primary和Secondary副本对应的服务器发送消息并告诉它们,谁是Primary,谁是Secondary,Chunk的新版本是什么。Primary和Secondary服务器都会将版本号存储在本地的磁盘中。这样,当它们因为电源故障或者其他原因重启时,它们可以向Master报告本地保存的Chunk的实际版本号
- 客户机向Master节点询问哪一个chunk服务器持有当前的租约,以及其它副本的位置。如果没有一个Chunk持有租约,Master节点就选择其中一个副本建立一个租约(这个步骤在图上没有显示)
- Master节点将主chunk的标识符以及其它副本(又称为secondary副本、二级副本)的位置返回给客户机。客户机缓存这些数据以便后续的操作。只有在主chunk不可用,或者主Chunk回复信息表明它已不再持有租约的时候,客户机才需要重新跟Master节点联系
- 客户机把数据推送到所有的副本上,客户机可以以任意的顺序推送数据,chunk服务器接收到数据并保存在它的内部LRU缓存中,一直到数据被使用或者过期交换出去。由于数据流的网络传输负载非常高,通过分离数据流和控制流,我们可以基于网络拓扑情况对数据流进行规划,提高系统性能而不用去理会哪个chunk服务器保存了主chunk
- 当所有的副本都确认接收到了数据,客户机发送写请求到主chunk服务器,这个请求标识了早前推送到所有副本的数据。主chunk为接收到的所有操作分配连续的序列号,这些操作可能来自不同的客户机,序列号保证了操作顺序执行。它以序列号的顺序把操作应用到它自己的本地状态中
- 主Chunk把写请求传递到所有的二级副本。每个二级副本依照主Chunk分配的序列号以相同的顺序执行这些操作
- 所有的二级副本回复主chunk,它们已经完成了操作
- 主chunk服务器回复客户机,任何副本产生的任何错 误都会返回给客户机。在出现错误的情况下,写入操作可能在主chunk和一些二级副本执行成功。(如果操作在主Chunk上失败了,操作就不会被分配序列号,也不会被传递)客户端的请求被确认为失败,被修改的region处于不一致的状态。我们的客户机代码通过重复执行失败的操作来处理这样的错误。在从头开始重复执行之前,客户机会先从步骤(3)到步骤(7)做几次尝试。
数据流
- 为了机器带宽的利用率,通过链式结构的方式来复制数据
- 为了避免网络瓶颈和高延迟的链接,每台机器都选择网络拓扑中选择一台没有收到数据的、离自己最近的机器作为目标机器
- 为了降低延迟,基于tcp和管道式方式推送数据
原子的记录追加
客户机把数据推送给文件最后一个chunk的所有副本,之后发送请求给主chunk。主chunk会检查这次记录追加操作是否会超出checnk的最大尺寸。如果超过了最大尺寸,主chunk首先会将当前的chunk填充到最大的尺寸,之后通知所有的二级副本执行同样的操作,然后回复客户机要求对其在下一个chunk重新进行记录追加操作,主chunk把数据追加到自己的副本内部,然后通过二级副本把数据写在跟主chunk一样的位置上,最后回复客户机成功。如果记录追加操作在任何一个副本上失败了,客户端就需要重新进行操作。最终,同一个chunk的不同副本可能包含了不同的数据-重复包含一个记录或者部分数据,GFS不保证chunk所有的副本在字节级别上是完全一致的。
快照
当Master节点收到一个快照请求,它首先取消作快照的文件的所有chunk的租约。这个措施保证了后续对这些chunk的写操作都必须与Master交互交互以找到租约持有者。这就给Master节点一个率先创建chunk的新拷贝的机会,在租约取消或者过期之后,Master节点把这个操作以日志的方式记录到硬盘上,然后通过复制源文件或者目录的元数据的方式,把这条日志记录的变化更新到内存中。
在快照操作之后,当有客户机第一次想写入数据到chunk中,他会首先发送一个请求到Master节点查询当前的租约的持有者,Master节点发现chunk的引用计数超过1,Master要求chunk所在的机器克隆一个新的chunk,通过在源chunk所在的服务器上穿新的chunk,通过本地复制提高速度,同时确保新的chunk拥有租约
Master节点的操作
名称空间管理和锁
演示一下在/home/user被快照到/save/user的时候,锁机制如何防止创建文件/home/user/foo。快照操作获取/home和/save的读取锁,以及/home/user和/save/user的写入锁。文件创建操作获 得/home和/home/user的读取锁,以及/home/user/foo的写入锁。这两个操作要顺序执行,因为它们试图获取的/home/user的锁是相互冲突。
副本的位置
目标:最大化数据可靠性和可用性,最大化网络带宽利用率 方案:必须在多个机架间分布储存chunk的副本
创建、重新复制、重新负载均衡
创建chunk时考虑因素
- 均衡机器磁盘使用率
- 限制每台机器最近创建chunk的次数
- chunk分部在不同的机架上
垃圾回收
当一个文件被删除时的操作:
- Master节点把删除操作的日志记录下来
- 修改文件名为包含删除时间戳和隐藏的名字
- 当Master节点对文件命名空间进行常规扫描时,删除3天前的隐藏文件
- 当隐藏文件删除元数据之后,也就切断了和chunk的映射关系
- Master节点告诉chunk服务器那些chunk可以删除了
在文件删除前,依然可以通过隐藏文件来读取,甚至可以修改隐藏文件名来’反删除’
过期失效的副本检测
只要Master节点和chunk签订一个先的租约,它就增加chunk中的版本号,然后通知所有副本,Master节点和这些副本都把新的版本号记录在他们持久化存储的状态信息中。这个动作发生在任何的客户机得到通知之前,在对这个chunk开始写之前,如果某个副本所在的chunk服务器正在处于失效的状态下,则这个副本的版本号不会增加。
容错和诊断
高可用
快速恢复
并不区分正常关闭和异常关闭,当正在发出的请求超时时,需要重新连接到服务器,重试这个请求
chunk复制
当有服务器离线或者通过checksum校验发现损坏数据,Master节点通过克隆已有副本保证每个chunk都被完整复制
Master服务器的复制
Master服务器的所有的checkpoint和操作日志都被复制到多台机器上,对Master服务器状态的修改操作成功的前提是操作日志写到Master节点服务器的备节点和本机的磁盘。
数据完整性
可以通过别的chunk副本来解决数据损坏问题,但是跨越chunk服务器比较副本来检查数据是不是损坏是很不实际的,因此,每个chunk服务器必须通过独立维护checksum来校验自己的副本的完整性。把每个chunk都分成64KB大小的块,每个块都对应一个32位的checksum,和其它元数据一样,checksum与其他的用户数据分开并且保存在内存和硬盘上,同时也记录操作日志
对于读操作来说,在把数据返回给客户端或者其它的chunk服务器之前,chunk服务器会校验读取操作涉及的范围内的块的checksum,因此chunk服务器不会把错误数据传递到其它的机器上。如果发生某个块的checksum不正确,chunk服务器返回给请求者一个错误信息,并且通知Master服务器这个错误。作为回应,请求者应当从其它副本读取数据,Master服务器也会从其它副本克隆数据进行恢复。当一个新的副本就绪后,Master服务器通知副本错误的chunk服务器删掉错误的副本。
问题
写文件失败之后Primary和Secondary服务器上的状态如何恢复?
Primary告诉所有的副本去执行数据追加操作,某些成功了,某些没成功。如果某些副本没有成功执行,Primary会回复客户端说执行失败。之后客户端会认为数据没有追加成功。但是实际上,部分副本还是成功将数据追加了。所以现在,一个Chunk的部分副本成功完成了数据追加,而另一部分没有成功,这种状态是可接受的,没有什么需要恢复,这就是GFS的工作方式。
写文件失败之后,读Chunk数据会有什么不同?
如果写文件失败之后,一个客户端读取相同的Chunk,客户端可能可以读到追加的数据,也可能读不到,取决于客户端读的是Chunk的哪个副本。如果一个客户端发送写文件的请求,并且得到了表示成功的回复,那意味着所有的副本都在相同的位置追加了数据。如果客户端收到了表示失败的回复,那么意味着0到多个副本实际追加了数据,其他的副本没有追加上数据。所以这时,有些副本会有追加的数据,有些副本没有。这时,取决于你从哪个副本读数据,有可能读到追加的新数据,也有可能读不到。
可不可以通过版本号来判断副本是否有之前追加的数据?
所有的Secondary都有相同的版本号。版本号只会在Master指定一个新Primary时才会改变。通常只有在原Primary发生故障了,才会指定一个新的Primary。所以,副本(参与写操作的Primary和Secondary)都有相同的版本号,你没法通过版本号来判断它们是否一样,或许它们就是不一样的(取决于数据追加成功与否)。这么做的理由是,当Primary回复“no”给客户端时,客户端知道写入失败了,之后客户端的GFS库会重新发起追加数据的请求,直到最后成功追加数据。成功了之后,追加的数据会在所有的副本中相同位置存在。在那之前,追加的数据只会在部分副本中存在。
客户端将数据拷贝给多个副本会不会造成瓶颈?
考虑到底层网络,写入文件数据的具体传输路径可能会非常重要。当论文第一次提到这一点时,它说客户端会将数据发送给每个副本。实际上,之后,论文又改变了说法,说客户端只会将数据发送给离它最近的副本,之后那个副本会将数据转发到另一个副本,以此类推形成一条链,直到所有的副本都有了数据。这样一条数据传输链可以在数据中心内减少跨交换机传输(否则,所有的数据吞吐都在客户端所在的交换机上
什么时候版本号会增加?
版本号只在Master节点认为Chunk没有Primary时才会增加。在一个正常的流程中,如果对于一个Chunk来说,已经存在了Primary,那么Master节点会记住已经有一个Primary和一些Secondary,Master不会重新选择Primary,也不会增加版本号。它只会告诉客户端说这是Primary,并不会变更版本号
如果写入数据失败了,不是应该先找到问题在哪再重试吗?
当Primary向客户端返回写入失败时,你或许会认为一定是哪里出错了,在修复之前不应该重试。实际上,就我所知,论文里面在重试追加数据之前没有任何中间操作。因为,错误可能就是网络数据的丢失,这时就没什么好修复的,网络数据丢失了,我们应该重传这条网络数据。客户端重新尝试追加数据可以看做是一种复杂的重传数据的方法。或许对于大多数的错误来说,我们不需要修改任何东西,同样的Primary,同样的Secondary,客户端重试一次或许就能正常工作,因为这次网络没有丢包。 但是如果是某一个Secondary服务器出现严重的故障,那问题变得有意思了。我们希望的是,Master节点能够重新生成Chunk对应的服务器列表,将不工作的Secondary服务器剔除,再选择一个新的Primary,并增加版本号。如果这样的话,我们就有了一组新的Primary,Secondary和版本号,同时,我们还有一个不太健康的Secondary,它包含的是旧的副本和旧的版本号,正是因为版本号是旧的,Master永远也不会认为它拥有新的数据。但是,论文中没有证据证明这些会立即发生。论文里只是说,客户端重试,并且期望之后能正常工作。最终,Master节点会ping所有的Chunk服务器,如果Secondary服务器挂了,Master节点可以发现并更新Primary和Secondary的集合,之后再增加版本号。但是这些都是之后才会发生(而不是立即发生)。
如果Master节点发现Primary挂了会怎么办?
在某个时间点,Master指定了一个Primary,之后Master会一直通过定期的ping来检查它是否还存活。因为如果它挂了,Master需要选择一个新的Primary。Master发送了一些ping给Primary,并且Primary没有回应,你可能会认为Master会在那个时间立刻指定一个新的Primary。但事实是,这是一个错误的想法。为什么是一个错误的想法呢?因为可能是网络的原因导致ping没有成功,所以有可能Primary还活着,但是网络的原因导致ping失败了。但同时,Primary还可以与客户端交互,如果Master为Chunk指定了一个新的Primary,那么就会同时有两个Primary处理写请求,这两个Primary不知道彼此的存在,会分别处理不同的写请求,最终会导致有两个不同的数据拷贝。这被称为脑裂(split-brain)。脑裂是一种非常重要的概念,我们会在之后的课程中再次介绍它(详见6.1),它通常是由网络分区引起的。比如说,Master无法与Primary通信,但是Primary又可以与客户端通信,这就是一种网络分区问题。网络故障是这类分布式存储系统中最难处理的问题之一。所以,我们想要避免错误的为同一个Chunk指定两个Primary的可能性。Master采取的方式是,当指定一个Primary时,为它分配一个租约,Primary只在租约内有效。Master和Primary都会知道并记住租约有多长,当租约过期了,Primary会停止响应客户端请求,它会忽略或者拒绝客户端请求。因此,如果Master不能与Primary通信,并且想要指定一个新的Primary时,Master会等到前一个Primary的租约到期。这意味着,Master什么也不会做,只是等待租约到期。租约到期之后,可以确保旧的Primary停止了它的角色,这时Master可以安全的指定一个新的Primary而不用担心出现这种可怕的脑裂的情况。
作者:肖宏辉 链接:https://zhuanlan.zhihu.com/p/187542143 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。