通过一个大型项目来学习分布式算法(3)


图2:Dynamo的划分和键的复制。

一个负责存储一个特定的键的节点列表被称为首选列表(preference list)。该系统的设计,如将4.8节中解释,让系统中每一个节点可以决定对于任意key哪些节点应该在这个清单中。出于对节点故障的考虑,首选清单可以包含起过N个节点。请注意,因使用虚拟节点,对于一个特定的key的第一个N个后继位置可能属于少于N个物理所节点(即节点可以持有多个第一个N个位置)。为了解决这个问题,一个key首选列表的构建将跳过环上的一些位置,以确保该列表只包含不同的物理节点。

4.4版本的数据 

Dynamo提供最终一致性,从而允许更新操作可以异步地传播到所有副本。put()调用可能在更新操作被所有的副本执行之前就返回给调用者,这可能会导致一个场景:在随后的get()操作可能会返回一个不是最新的对象。如果没有失败,那么更新操作的传播时间将有一个上限。但是,在某些故障情况下(如服务器故障或网络partitions),更新操作可能在一个较长时间内无法到达所有的副本。

在Amazon的平台,有一种类型的应用可以容忍这种不一致,并且可以建造并操作在这种条件下。例如,购物车应用程序要求一个“添加到购物车“动作从来没有被忘记或拒绝。如果购物车的最近的状态是不可用,并且用户对一个较旧版本的购物车做了更改,这种变化仍然是有意义的并且应该保留。但同时它不应取代当前不可用的状态,而这不可用的状态本身可能含有的变化也需要保留。请注意在Dynamo中“添加到购物车“和”从购物车删除项目“这两个操作被转成put请求。当客户希望增加一个项目到购物车(或从购物车删除)但最新的版本不可用时,该项目将被添加到旧版本(或从旧版本中删除)并且不同版本将在后来协调(reconciled)。

为了提供这种保证,Dynamo将每次数据修改的结果当作一个新的且不可改变的数据版本。它允许系统中同一时间出现多个版本的对象。大多数情况,新版本包括(subsume)老的版本,且系统自己可以决定权威版本(语法协调 syntactic reconciliation)。然而,版本分支可能发生在并发的更新操作与失败的同时出现的情况,由此产生冲突版本的对象。在这种情况下,系统无法协调同一对象的多个版本,那么客户端必须执行协调,将多个分支演化后的数据崩塌(collapse)成一个合并的版本(语义协调)。一个典型的崩塌的例子是“合并”客户的不同版本的购物车。使用这种协调机制,一个“添加到购物车”操作是永远不会丢失。但是,已删除的条目可能会”重新浮出水面”(resurface)

重要的是要了解某些故障模式有可能导致系统中相同的数据不止两个,而是好几个版本。在网络分裂和节点故障的情况下,可能会导致一个对象有不同的分历史,系统将需要在未来协调对象。这就要求我们在设计应用程序,明确意识到相同数据的多个版本的可能性(以便从来不会失去任何更新操作)。

Dynamo使用矢量时钟[12]来捕捉同一不同版本的对象的因果关系。矢量时钟实际上是一个(node,counter)对列表(即(节点,计数器)列表)。矢量时钟是与每个对象的每个版本相关联。通过审查其向量时钟,我们可以判断一个对象的两个版本是平行分枝或有因果顺序。如果第一个时钟对象上的计数器在第二个时钟对象上小于或等于其他所有节点的计数器,那么第一个是第二个的祖先,可以被人忽略。否则,这两个变化被认为是冲突,并要求协调。

在dynamo中,当客户端更新一个对象,它必须指定它正要更新哪个版本。这是通过传递它从早期的读操作中获得的上下文对象来指定的,它包含了向量时钟信息。当处理一个读请求,如果Dynamo访问到多个不能语法协调(syntactically reconciled)的分支,它将返回分支叶子处的所有对象,其包含与上下文相应的版本信息。使用这种上下文的更新操作被认为已经协调了更新操作的不同版本并且分支都被倒塌到一个新的版本。

 
图3:对象的版本随时间演变。

为了说明使用矢量时钟,让我们考虑图3所示的例子。

1)客户端写入一个新的对象。节点(比如说Sx),它处理对这个key的写:序列号递增,并用它来创建数据的向量时钟。该系统现在有对象D1和其相关的时钟[(Sx,1)]。

2)客户端更新该对象。假定也由同样的节点处理这个要求。现在该系统有对象D2和其相关的时钟[(Sx,2)]。D2继承自D1,因此覆写D1,但是节点中或许存在还没有看到D2的D1的副本。

3)让我们假设,同样的客户端更新这个对象但不同的服务器(比如Sy)处理了该请求。目前该系统具有数据D3及其相关的时钟[(Sx,2),(Sy,1)]。

4)接下来假设不同的客户端读取D2,然后尝试更新它,并且另一个服务器节点(如Sz)进行写操作。该系统现在具有D4(D2的子孙),其版本时钟[(Sx,2),(Sz,1)]。一个对D1或D2有所了解的节点可以决定,在收到D4和它的时钟时,新的数据将覆盖D1和D2,可以被垃圾收集。一个对D3有所了解的节点,在接收D4时将会发现,它们之间不存在因果关系。换句话说,D3和D4都有更新操作,但都未在对方的变化中反映出来。这两个版本的数据都必须保持并提交给客户端(在读时)进行语义协调。

5)现在假定一些客户端同时读取到D3和D4(上下文将反映这两个值是由read操作发现的)。读的上下文包含有D3和D4时钟的概要信息,即[(Sx,2),(Sy,1),(Sz,1)]的时钟总结。如果客户端执行协调,且由节点Sx来协调这个写操作,Sx将更新其时钟的序列号。D5的新数据将有以下时钟:[(Sx,3),(Sy,1),(Sz,1)]。

关于向量时钟一个可能的问题是,如果许多服务器协调对一个对象的写,向量时钟的大小可能会增长。实际上,这是不太可能的,因为写入通常是由首选列表中的前N个节点中的一个节点处理。在网络分裂或多个服务器故障时,写请求可能会被不是首选列表中的前N个节点中的一个处理的,因此会导致矢量时钟的大小增长。在这种情况下,值得限制向量时钟的大小。为此,Dynamo采用了以下时钟截断方案:伴随着每个(节点,计数器)对,Dynamo存储一个时间戳表示最后一次更新的时间。当向量时钟中(节点,计数器)对的数目达到一个阈值(如10),最早的一对将从时钟中删除。显然,这个截断方案会导至在协调时效率低下,因为后代关系不能准确得到。不过,这个问题还没有出现在生产环境,因此这个问题没有得到彻底研究。

4.5执行get()put()操作

Dynamo中的任何存储节点都有资格接收客户端的任何对key的get和put操作。在本节中,对简单起见,我们将描述如何在一个从不失败的(failure-free)环境中执行这些操作,并在随后的章节中,我们描述了在故障的情况下读取和写入操作是如何执行。

GET和PUT操作都使用基于Amazon基础设施的特定要求,通过HTTP的处理框架来调用。一个客户端可以用有两种策略之一来选择一个节点:(1)通过一个普通的负载平衡器路由请求,它将根据负载信息选择一个节点,或(2)使用一个分区(partition)敏感的客户端库直接路由请求到适当的协调程序节点。第一个方法的优点是,客户端没有链接(link)任何Dynamo特定的代码在到其应用中,而第二个策略,Dynamo可以实现较低的延时,因为它跳过一个潜在的转发步骤。

处理读或写操作的节点被称为协调员。通常,这是首选列表中跻身前N个节点中的第一个。如果请求是通过负载平衡器收到,访问key的请求可能被路由到环上任何随机节点。在这种情况下,如果接收到请求节点不是请求的key的首选列表中前N个节点之一,它不会协调处理请求。相反,该节点将请求转发到首选列表中第一个跻身前N个节点。

读取和写入操作涉及到首选清单中的前N个健康节点,跳过那些瘫痪的(down)或者不可达(inaccessible)的节点。当所有节点都健康,key的首选清单中的前N个节点都将被访问。当有节点故障或网络分裂,首选列表中排名较低的节点将被访问。

为了保持副本的一致性,Dynamo使用的一致性协议类似于仲裁(quorum)。该协议有两个关键配置值:R和W. R是必须参与一个成功的读取操作的最少数节点数目W是必须参加一个成功的写操作的最少节点数。设定R和W,使得R+W>N产生类似仲裁的系统。在此模型中,一个get(or out)操作延时是由最慢的R(或W)副本决定的。基于这个原因,R和W通常配置为小于N,为客户提供更好的延时。

当收到对key的put()请求时,协调员生成新版本向量时钟并在本地写入新版本。协调员然后将新版本(与新的向量时钟一起)发送给首选列表中的排名前N个的可达节点。如果至少W-1个节点返回了响应,那么这个写操作被认为是成功的。

同样,对于一个get()请求,协调员为key从首选列表中排名前N个可达节点处请求所有现有版本的数据,然后等待R个响应,然后返回结果给客户端。如果最终协调员收集的数据的多个版本,它返回所有它认为没有因果关系的版本。不同版本将被协调,并且取代当前的版本,最后写回。

4.6故障处理:暗示移交(Hinted Handoff) 

Dynamo如果使用传统的仲裁(quorum)方式,在服务器故障和网络分裂的情况下它将是不可用,即使在最简单的失效条件下也将降低耐久性。为了弥补这一点,它不严格执行仲裁,即使用了“马虎仲裁”(“sloppy quorum”),所有的读,写操作是由首选列表上的前N个健康的节点执行的,它们可能不总是在散列环上遇到的那前N个节点。

考虑在图2例子中Dynamo的配置,给定N=3。在这个例子中,如果写操作过程中节点A暂时Down或无法连接,然后通常本来在A上的一个副本现在将发送到节点D。这样做是为了保持期待的可用性和耐用性。发送到D的副本在其原数据中将有一个暗示,表明哪个节点才是在副本预期的接收者(在这种情况下A)。接收暗示副本的节点将数据保存在一个单独的本地存储中,他们被定期扫描。在检测到了A已经复苏,D会尝试发送副本到A。一旦传送成功,D可将数据从本地存储中删除而不会降低系统中的副本总数。

你可能感兴趣的:(算法,分布式事务)