使用暗示移交,Dynamo确保读取和写入操作不会因为节点临时或网络故障而失败。需要最高级别的可用性的应用程序可以设置W为1,这确保了只要系统中有一个节点将key已经持久化到本地存储 , 一个写是可以接受(即一个写操作完成即意味着成功)。因此,只有系统中的所有节点都无法使用时写操作才会被拒绝。然而,在实践中,大多数Amazon生产服务设置了更高的W来满足耐久性极别的要求。对N, R和W的更详细的配置讨论在后续的第6节。
一个高度可用的存储系统具备处理整个数据中心故障的能力是非常重要的。数据中心由于断电,冷却装置故障,网络故障和自然灾害发生故障。Dynamo可以配置成跨多个数据中心地对每个对象进行复制。从本质上讲,一个key的首选列表的构造是基于跨多个数据中心的节点的。这些数据中心通过高速网络连接。这种跨多个数据中心的复制方案使我们能够处理整个数据中心故障。
4.7处理永久性故障:副本同步
Hinted Handoff在系统成员流动性(churn)低,节点短暂的失效的情况下工作良好。有些情况下,在hinted副本移交回原来的副本节点之前,暗示副本是不可用的。为了处理这样的以及其他威胁的耐久性问题,Dynamo实现了反熵(anti-entropy,或叫副本同步)协议来保持副本同步。
为了更快地检测副本之间的不一致性,并且减少传输的数据量,Dynamo采用MerkleTree[13]。MerkleTree是一个哈希树(Hash Tree),其叶子是各个key的哈希值。树中较高的父节点均为其各自孩子节点的哈希。该merkleTree的主要优点是树的每个分支可以独立地检查,而不需要下载整个树或整个数据集。此外,MerkleTree有助于减少为检查副本间不一致而传输的数据的大小。例如,如果两树的根哈希值相等,且树的叶节点值也相等,那么节点不需要同步。如果不相等,它意味着,一些副本的值是不同的。在这种情况下,节点可以交换children的哈希值,处理直到它到达了树的叶子,此时主机可以识别出“不同步”的key。MerkleTree减少为同步而需要转移的数据量,减少在反熵过程中磁盘执行读取的次数。
Dynamo在反熵中这样使用MerkleTree:每个节点为它承载的每个key范围(由一个虚拟节点覆盖 key 集合)维护一个单独的MerkleTree。这使得节点可以比较key range中的key是否是最新。在这个方案中,两个节点交换MerkleTree的根,对应于它们承载的共同的键范围。其后,使用上面所述树遍历方法,节点确定他们是否有任何差异和执行适当的同步行动。方案的缺点是,当节点加入或离开系统时有许多key rangee变化,从而需要重新对树进行计算。通过由6.2节所述的更精炼partitioning方案,这个问题得到解决。
4.8会员和故障检测4.8.1环会员(Ring Membership)
Amazon环境中,节点中断(由于故障和维护任务)常常是暂时的,但持续的时间间隔可能会延长。一个节点故障很少意味着一个节点永久离开,因此应该不会导致对已分配的分区重新平衡(rebalancing)和修复无法访问的副本。同样,人工错误可能导致意外启动新的Dynamo节点。基于这些原因,应当适当使用一个明确的机制来发起节点的增加和从环中移除节点。管理员使用命令行工具或浏览器连接到一个节点,并发出成员改变(membership change)指令指示一个节点加入到一个环或从环中删除一个节点。接收这一请求的节点写入成员变化以及适时写入持久性存储。该成员的变化形成了历史,因为节点可以被删除,重新添加多次。一个基于Gossip的协议传播成员变动,并维持成员的最终一致性。每个节点每间隔一秒随机选择随机的对等节点,两个节点有效地协调他们持久化的成员变动历史。
当一个节点第一次启动时,它选择它的Token(在虚拟空间的一致哈希节点) 并将节点映射到各自的Token集(Token set)。该映射被持久到磁盘上,最初只包含本地节点和Token集。在不同的节点中存储的映射(节点到token set 的映射)将在协调成员的变化历史的通信过程中一同被协调。因此,划分和布局信息也是基于Gossip协议传播的,因此每个存储节点都了解对等节点所处理的标记范围。这使得每个节点可以直接转发一个key的读/写操作到正确的数据集节点。
4.8.2外部发现
上述机制可能会暂时导致逻辑分裂的Dynamo环。例如,管理员可以将节点A加入到环,然后将节点B加入环。在这种情况下,节点A和B各自都将认为自己是环的一员,但都不会立即了解到其他的节点(也就是A不知道B的存在,B也不知道A的存在,这叫逻辑分裂)。为了防止逻辑分裂,有些Dynamo节点扮演种子节点的角色。种子的发现(discovered)是通过外部机制来实现的并且所有其他节点都知道(实现中可能直接在配置文件中指定seed node的IP,或者实现一个动态配置服务,seed register)。因为所有的节点,最终都会和种子节点协调成员关系,逻辑分裂是极不可能的。种子可从静态配置或配置服务获得。通常情况下,种子在Dynamo环中是一个全功能节点。
4.8.3故障检测
Dynamo中,故障检测是用来避免在进行get()和put()操作时尝试联系无法访问节点,同样还用于分区转移(transferring partition)和暗示副本的移交。为了避免在通信失败的尝试,一个纯本地概念的失效检测完全足够了:如果节点B不对节点A的信息进行响应(即使B响应节点C的消息),节点A可能会认为节点B失败。在一个客户端请求速率相对稳定并产生节点间通信的Dynamo环中,一个节点A可以快速发现另一个节点B不响应时,节点A则使用映射到B的分区的备用节点服务请求,并定期检查节点B后来是否后来被复苏。在没有客户端请求推动两个节点之间流量的情况下,节点双方并不真正需要知道对方是否可以访问或可以响应。
去中心化的故障检测协议使用一个简单的Gossip式的协议,使系统中的每个节点可以了解其他节点到达(或离开)。有关去中心化的故障探测器和影响其准确性的参数的详细信息,感兴趣的读者可以参考[8]。早期Dynamo的设计使用去中心化的故障检测器以维持一个失败状态的全局性的视图。后来认为,显式的节点加入和离开的方法排除了对一个失败状态的全局性视图的需要。这是因为节点是是可以通过节点的显式加入和离开的方法知道节点永久性(permanent)增加和删除,而短暂的(temporary)节点失效是由独立的节点在他们不能与其他节点通信时发现的(当转发请求时)。
4.9添加/删除存储节点
当一个新的节点(例如X)添加到系统中时,它被分配一些随机散落在环上的Token。对于每一个分配给节点X的key range,当前负责处理落在其key range中的key的节点数可能有好几个(小于或等于N)。由于key range的分配指向X,一些现有的节点不再需要存储他们的一部分key,这些节点将这些key传给X,让我们考虑一个简单的引导(bootstrapping)场景,节点X被添加到图2所示的环中A和B之间,当X添加到系统,它负责的key范围为(F,G],(G,A]和(A,X]。因此,节点B,C和D都各自有一部分不再需要储存key范围(在X加入前,B负责(F,G], (G,A], (A,B]; C负责(G,A], (A,B], (B,C]; D负责(A,B], (B,C], (C,D]。而在X加入后,B负责(G,A], (A,X], (X,B]; C负责(A,X], (X,B], (B,C]; D负责(X,B], (B,C], (C,D])。因此,节点B,C和D,当收到从X来的确认信号时将供出(offer)适当的key。当一个节点从系统中删除,key的重新分配情况按一个相反的过程进行。
实际经验表明,这种方法可以将负载均匀地分布到存储节点,其重要的是满足了延时要求,且可以确保快速引导。最后,在源和目标间增加一轮确认(confirmation round)以确保目标节点不会重复收到任何一个给定的key range转移。
5实现
在dynamo中,每个存储节点有三个主要的软件组件:请求协调,成员(membership)和故障检测,以及本地持久化引擎。所有这些组件都由Java实现。
Dynamo的本地持久化组件允许插入不同的存储引擎,如:Berkeley数据库(BDB版本)交易数据存储,BDB Java版,MySQL,以及一个具有持久化后备存储的内存缓冲。设计一个可插拔的持久化组件的主要理由是要按照应用程序的访问模式选择最适合的存储引擎。例如,BDB可以处理的对象通常为几十千字节的数量级,而MySQL能够处理更大尺寸的对象。应用根据其对象的大小分布选择相应的本地持久性引擎。生产中,Dynamo多数使用BDB事务处理数据存储。
请求协调组成部分是建立在事件驱动通讯基础上的,其中消息处理管道分为多个阶段类似SEDA的结构[24]。所有的通信都使用Java NIO Channels。协调员执行读取和写入:通过收集从一个或多个节点数据(在读的情况下),或在一个或多个节点存储的数据(写入)。每个客户的请求中都将导致在收到客户端请求的节点上一个状态机的创建。每一个状态机包含以下逻辑:标识负责一个key的节点,发送请求,等待回应,可能的重试处理,加工和包装返回客户端响应。每个状态机实例只处理一个客户端请求。例如,一个读操作实现了以下状态机:(i)发送读请求到相应节点,(ii)等待所需的最低数量的响应,(iii)如果在给定的时间内收到的响应太少,那么请求失败,(iv)否则,收集所有数据的版本,并确定要返回的版本 (v)如果启用了版本控制,执行语法协调,并产生一个对客户端不透明写上下文,其包括一个涵括所有剩余的版本的矢量时钟。为了简洁起见,没有包含故障处理和重试逻辑。
在读取响应返回给调用方后,状态机等待一小段时间以接受任何悬而未决的响应。如果任何响应返回了过时了的(stale)的版本,协调员将用最新的版本更新这些节点(当然是在后台了)。这个过程被称为读修复(read repair),因为它是用来修复一个在某个时间曾经错过更新操作的副本,同时read repair可以消除不必的反熵操作。
如前所述,写请求是由首选列表中某个排名前N的节点来协调的。虽然总是选择前N节点中的第一个节点来协调是可以的,但在单一地点序列化所有的写的做法会导致负荷分配不均,进而导致违反SLA。为了解决这个问题,首选列表中的前N的任何节点都允许协调。特别是,由于写通常跟随在一个读操作之后,写操作的协调员将由节点上最快答复之前那个读操作的节点来担任,这是因为这些信息存储在请求的上下文中(指的是write操作的请求)。这种优化使我们能够选择那个存有同样被之前读操作使用过的数据的节点,从而提高“读你的写”(read-your-writes)一致性(译:我不认为这个描述是有道理的,因为作者这里描述明明是write-follows-read,要了解read-your-writes一致性的读者参见作者另一篇文章:eventually consistent)。它也减少了为了将处理请求的性能提高到99.9百分位时性能表现的差异。
6经验与教训
Dynamo由几个不同的配置的服务使用。这些实例有着不同的版本协调逻辑和读/写仲裁(quorum)的特性。以下是Dynamo的主要使用模式:
业务逻辑特定的协调:这是一个普遍使用的Dynamo案例。每个数据对象被复制到多个节点。在版本发生分岔时,客户端应用程序执行自己的协调逻辑。前面讨论的购物车服务是这一类的典型例子。其业务逻辑是通过合并不同版本的客户的购物车来协调不同的对象。
基于时间戳的协调:此案例不同于前一个在于协调机制。在出现不同版本的情况下,Dynamo执行简单的基于时间戳的协调逻辑:“最后的写获胜”,也就是说,具有最大时间戳的对象被选为正确的版本。一些维护客户的会话信息的服务是使用这种模式的很好的例子。
高性能读取引擎:虽然Dynamo被构建成一个“永远可写”数据存储,一些服务通过调整其仲裁的特性把它作为一个高性能读取引擎来使用。通常,这些服务有很高的读取请求速率但只有少量的更新操作。在此配置中,通常R是设置为1,且W为N。对于这些服务,Dynamo提供了划分和跨多个节点的复制能力,从而提供增量可扩展性(incremental scalability)。一些这样的实例被当成权威数据缓存用来缓存重量级后台存储的数据。那些保持产品目录及促销项目的服务适合此种类别。
Dynamo的主要优点是它的客户端应用程序可以调的N,R和W的值,以实现其期待的性能, 可用性和耐用性的水平。例如,N的值决定了每个对象的耐久性。Dynamo用户使用的一个典型的N值是3。
W和R影响对象的可用性,耐用性和一致性。举例来说,如果W设置为1,只要系统中至少有一个节点活就可以成功地处理一个写请求,那么系统将永远不会拒绝写请求。不过,低的W和R值会增加不一致性的风险,因为写请求被视为成功并返回到客户端,即使他们还未被大多数副本处理。