谷歌文件系统(The Google File System译)(第3章)

3. 系统交互

我们是出于最大限度地减少master在所有操作中的参与度来设计系统的。在此背景之下,现在我们再来描述客户端、master,以及chunkserver是如何进行交互以实现数据变更,原子性记录追加和快照的操作

3.1 租约与更新顺序

数据更新是指改变元数据或是块的内容的操作,例如写入或追加操作。每一个变更都会在块的所有副本上执行。我们使用租约来维护副本间更新顺序的一致性。master将块的租约授予副本的其中一个,我们可以称其为主副本。主副本会为所有在该块上的更新操作选择一个顺序,其余所有副本都会在执行时遵循这一顺序。因此,全局变更顺序首先是被master选择的租约授予顺序所确定的,然后才是在租约中被主副本所分配的序列号来确定

租赁机制被设计用来最大限度地减轻master的管理负担。一个租约的初始超时时间为60秒,然而,只要块被更新,主副本就可以无限制地向master请求延长租约,通常都会得到回应。这些额外的请求和授权是在master和所有的chunkserver之间的定期交换的心跳消息中捎带的。master有时可能会尝试在到期前撤销租约(例如,master想要禁止一个对文件重命名的更新操作)。即使master和主副本之间断开通信,也可以在租约到期后将新的租约授予另一个副本

在图2中,我们通过编号的写入控制流来说明这一步骤
图2

  1. 客户端询问master哪个chunkserver保存着块当前的租约和其他的副本位置。如果都没有拥有租约,则master将从副本中选择一个来授予租约(未在图中显示)
  2. master给客户端回复主副本和其他(次要)副本的位置。客户端会缓存这些数据用于近期的更新操作。只有当主副本无法访问或是不再拥有租约时,客户端才需要重新联系master
  3. 客户端将数据推送给所有副本,顺序并不固定。每一个chunkserver都会把数据保存在内部的LRU缓存中,直到数据被使用或是过期。通过将数据流与控制流分离,我们可以基于网络拓扑来调度昂贵的数据流,从而提高性能,也不用关心哪一个是主副本。3.2节中对这一点进行了更深入的讨论
  4. 一旦所有副本都确认收到了数据,客户端就会向主副本发送写请求,这个请求标识了之前推送给所有副本的数据。主副本会为收到的所有数据变更操作(可能来自于多个客户端)分配连续的序列号,这提供了必要的串行化机制,它会按照序列号的顺序将更新应用于本地
  5. 主副本将写请求发送给其他所有的次级副本,每一个次级副本都会按照主副本分配的同样的序列号顺序来执行数据变更
  6. 次级副本在完成操作后需要向主副本回复消息
  7. 主副本向客户端返回应答。在任何副本上遇到的错误都会汇报给客户端。出现错误时,该写操作可能已经在主副本或其余一部分次级副本上执行成功了(如果在主副本上发生错误,将不会进行序列号的分发),客户端请求仍会被认为是失败的,并且被修改的区域将会处于不一致的状态。我们的客户端代码会通过重试失败的数据变更操作来处理这样的错误,将会在完全退回重写之前,先在第(3)步到第(7)步之间进行一些尝试

如果应用程序的写入操作过于庞大,或是超过了块的边界,GFS客户端代码将会将其分解为多个写入操作。它们都会遵循上述的写入流程,但可能会被来自其他客户端的并发操作交错覆盖。因此,共享文件区域最终可能包含来自于不同客户端的片段,虽然这些副本是一致的,因为各个操作都以相同的顺序在所有副本上成功执行,这会让文件区域保持一致但不确定的状态,如2.7节所述

3.2 数据流

为了更高效地使用网络资源,我们将数据流从控制流中分离出来。控制流从客户端到达主副本,再到其他所有次级副本的这一过程,数据是在认真挑选的chunkserver链上以流水线形式被线性推送的。我们的目的是充分利用每台机器的网络带宽,来避免网络瓶颈和高延迟的链路,同时最小化推送所有数据的延迟

为了充分利用每台机器的网络带宽,数据是沿着一条chunkserver链被线性推送的,而不是像其他拓扑结构的分布那样(例如树型结构)。因此,每一台机器全部的出站带宽都被用来尽快地传送数据,而不是用于为多个接收者进行切分

为了尽可能的避免网络瓶颈和高延迟链路(例如,通常情况下的内部交换链路),每一台机器都将数据转发到网络拓扑结构中尚未接收到数据的离他“最近”的机器。假设客户端将数据发送给chunkserver S1到S4,首先会将数据发送到离它最近的机器,比如S1。S1接下来就将数据转发到S2~S4中离它最近的机器,比如S2。类似地,S2再转发给S3或S4,看哪一个离S2更近,等等。我们的网络拓扑可以简单到能够从ip地址准确地估算“距离”

最后,我们通过在TCP连接上使用流水线来传送数据以最小化延迟。一旦chunkserver接收到一部分数据,就会立刻开始转发。流水线这里对我们来说尤其有用,因为我们使用了全双工链路的交换网络。立即发送数据并不会降低接收速率,在没有网络拥塞的情况下,将B个字节发送到R个副本上经过的时间理想情况下是B/R + RL,其中T是网络的吞吐量, L是两台机器间传送字节的延迟。我们的网络链路一般是100Mbps(T),L远低于1ms。因此,理想情况下,1MB的数据可以在80ms内完成分发

3.3 原子性的记录追加

GFS提供了一个名为record append的原子性追加操作。在传统的写操作中,客户端指定数据写入的位置。在同一区域的并发写操作是不可串行化的:该区域最终可能包含来自多个客户端的数据段。然而在一个记录追加操作中,客户端仅仅需要指定数据,GFS至少会将其原子性的(即,作为一个连续的字节序列)追加到文件中至少一次,追加的位置由GFS选择,并会向客户端返回这个偏移量。这很像Unix中O_APPEND的文件打开模式,当多个写入者并发操作时不会产生竞争条件

我们的分布式应用程序大量使用了记录追加操作,不同机器上的多个客户端并发地向同一个文件中进行数据的追加。如果使用传统的写入操作,客户端将会额外需要复杂且昂贵的同步开销,例如分布式锁管理器。在我们的工作负载中,这样的文件通常会作为多生产者/单消费者队列,或是不同客户端的归并结果

记录追加是一种数据变更,除了一些主副本上的额外逻辑操作之外,依然会遵循3.1节中所述的控制流。客户端将数据推送给文件最后一个块的所有副本后,会向主副本发送请求,主副本会检查当前块记录的追加操作是否导致块超过了最大大小(64MB),如果超过了,就将块填充到最大大小,并通知其余副本也这么做,然后告诉客户端应在下一个块上进行重试(追加的记录被限制为最大块大小的1/4,以将最坏情况下的碎片控制在一个可接受的水平上)。一般情况下,记录的大小都不会超过最大块大小,如果在这种情况下,主副本所在节点会将数据追加到它的副本上,并让其余的副本节点将数据写在它们控制的副本中确定的偏移量处,最后返回给客户端一个成功应答

如果任何副本上的记录追加操作失败,客户端会对操作进行重试。因此,同一个块的副本上的数据可能包含了不同的数据,这些数据可能包含了一份同样记录的全部或部分的重复值。GFS不保证所有副本是字节层面一致的,它仅能保证数据能被作为原子单位写入至少一次。这个特性很容易从对那些成功响应的报告中简单地观察出来:数据一定被写入到某些块所有副本的相同偏移位置处,此外,所有副本至少和记录结束的长度相同。因此,之后的任何记录都会被分配到更大的偏移位置处,或是不同的块上,即使有另一个不同的副本成为了主副本。就我们的一致性保证而言,成功执行写入数据的记录追加操作的区域是已定义的(所以是一致的),而介于其间是不一致的(所以是未定义的)。我们的应用程序可以如2.7.2节所述中来处理这种不一致的区域

3.4 快照

快照操作几乎可以在瞬间生成文件或目录树(“源文件”)的一份副本,同时会尽可能地避免中断任何正在进行的数据变更。我们的用户使用快照来迅速创建大型数据集的分支副本(通常是递归地拷贝这些副本),或是在调整实验前为当前状态建立检查点,以便可以在之后方便地进行提交或回滚

例如AFS[5],我们使用标准的copy-on-write(写时复制)技术来实现快照。当master收到一份快照的请求时,会首先撤销那些在即将进行快照的文件块上的未完成的租约。这种行为确保了这些块上任何连续的写操作都需要与主机交互,以找到租约的持有者,这首先给了master有一个创建块副本的机会

在租约被撤销或过期之后,master会将操作以日志形式记录到磁盘中。然后,master会通过拷贝源文件或目录树的元数据来将此日志记录应用到它的内存状态中。新创建的快照文件会指向与源文件相同的块

客户端在快照生效后首次对块C进行写入时,会向master发送请求来寻找当前租约的持有者。master注意到块C的引用计数大于1,会推迟对客户端请求的响应,并选择一个新的块句柄C’。然后会让拥有块C副本的每一个chunkserver都创建一个新的块,叫做C’。通过在与原块相同的chunkserver上创建新块,我们可以确保数据在本地进行拷贝,而不是通过网络(我们磁盘的速度是100MB以太网链路的3倍左右)。从这一点来看,处理任何块的请求都没有什么不同:master在新块C’上为其副本之一授予租约,并给客户端回复,使其可以正常地对块进行写入,而不知道这个块是从已存在的块刚创建出来的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章