Dynamo:高可靠的数据存储技术。标记服务分类、简单的key-value接口、清晰的一致性窗口定义、用简单的方案记录数据增长量和服务请求率。每个服务有自己的Dynamo实例。
第四节 系统架构
Dynamo使用的技术和优势
系统接口
get(),put()简单接口。
分区算法
Dynamo 采用一段连续变化的hash值(简单的使用[10, 20]的一个),而不是采用一个节点映射到环里的一个点,任何一个节点都会复制给其他多个节点。Dynamo采用“虚拟节点”的概念。一个虚拟节点像系统的一个节点,但是每个节点都可以由一个以上的虚拟节点的负责(获得响应)。当一个新的节点被加入系统,它就被赋予了环中的多个位置(“token”)。使用虚拟节点的优点:
- 如果一个节点不可用(由于故障或例行维护),原来由这个节点完成的负载就分发到剩余可用的节点上。
- 当一个节点又恢复可用,或者一个新的节点加入系统,新可用节点可以从其他节点接收相当数量的负载。
- 虚拟节点的数量是由它自己的容量决定的,根据物理基础架构的不同而不同。
复制
为了达到高可用和高持久性的目的,Dynamo复制它的数据到多个hosts,每个数据项被复制到N个hosts,N是一个“每个实例”的参数配置。每个key k被赋予一个协调者节点,协调者掌管数据项的复制,保证属于它(NODE)的范围。此外,本地保存每个key在它(协调者NODE)的范围内,协调者还复制这些key到N-1个环中顺时针方向的节点。具体见上图,节点B复制key k到节点C和D,并保存在本地。节点D将保存key将落在(A, B], (B, C], and (C,D]。
优先列表:负责保存特殊key的节点列表,至少包括N个节点,小于等于N个物理节点,所以优先列表忽略环中的一些位置来保证列表中包含不同的物理节点。
数据版本
为了保证写的可用性,Dynamo 把每次修改看成一个新的不便的数据版本(dataversion),它允许对象的多个数据版本同时出现在系统内。大多数情况下,新版本包含之前的版本,系统自己可以决定有效的版本(根据语意协调syntacticreconciliation)。但是可能出现版本分支,系统无法协对象调多版本冲突,那客户必须执行调解保证多分枝数据版本合并成一个。
Dynamo采用向量时钟vector clocks 来捕同一个对象不多版本的因果关系。一个 vectorclock是有效的(节点,计数器)的列表. 一个向量时钟vectorclock关联了所有对象的所有版本。通过检查向量时钟可以决定哪两个版本是并行分支或有一个因果顺序。 One candetermine
whether two versions of an object are on parallel branches orhave
a causal ordering, by examine their vector clocks.如果计数器在第一个对象的时钟小于等于第二个时钟所有节点的计数器,它就是第二个时钟的父亲,可以被忘记,否则,这2个变化被认为是冲突需要协调。
一个可能的问题:向量时钟长度由于多server同等的对同一个对象写而不断增加?在实践中,这个不可能,因为写通常被优先列表中的TOPN个节点处理,只有在网络分区或多服务器不可用的情况下,才会有优先列表中非TOPN的节点处理,这样才会使向量时钟增长。Dynamo最后采用下面截断方式处理clock truncationscheme:在每个(节点,计数器) 中, Dynamo保存一个时间戳表示更新数据项的最后时间。当(节点,计数器)的对的数量超过一定的阀值例如10,最老的对就会被从时钟移除。显而易见,这种截断的方式可能会导致在协调冲突是变得无效,例如后代关系不够精确。但是这个问题没有在生产环境出现,而且这个问题还没有调查研究。
执行get()和put()操作
这节为了简化,仅描述读写操作在无异常的环境的情况。根据Amazon的基础架构规范,通过http访问get和put操作。客户端选择节点有2种策略:(1)路由客户端请求通过总的负载均衡器基于负载信息来选择一个节点。好处:客户端不用将Dynamo硬编码在应用里。(2)使用分区敏感的客户端库/包请求直接到达适当的协调者节点。好处:能达到较低的延迟(latency),因为它跳过潜在的forward步骤。
为了维护复制的一致性,Dynamo采用一个一致性协议类似配额制quorum systems:这个协议有2个关键的配置项: R 和 W。R是必须参与成功读操作的最少节点数;W是必须参与成功写操作的最少节点数。根据配额制quorum-like system设置R和W满足R+ W >N(N为优先列表中的前N个节点)。在这个模型中,读写操作最差的延迟由R或W中最慢的复制决定。由于这个原因,所以读和写通常配置小于N来达到较好的延迟betterlatency。
一旦协调者收到put()请求,它将产生一个新版本的向量时钟,在本地写入新的版本,并将新版本(连同新向量时钟)发送给N个最好级别的可达节点。如果有至少W-1个节点响应写操作就认为是成功了。
类似,对应get()请求,协调者请求所有已存在的数据版本,针对key从优先列表中的N个最高级别的可达节点获取,在返回给客户端之前,要等待R个响应。如果协调者整理收集多个数据版本,它返回所有它认为这些没有关联的版本。有分歧的版本被协调,协调后的版本(取代当前版本的)被回写。
失败处理:提示移交HintedHandoff
如果Dynamo采用传统的配额制,在服务器宕机或网络分区异常,它将降低持久性(一个已提交事务的任何结果都必须是永久性的,即“在任何崩溃的情况的能保存下来”。),即使是最简单失败情况也是如此。
为了完善这个问题,采用“宽松的配额制”,所有的读写在优先列表不一定总是前N个健康的节点。
参考分区算法的图,N=3,这个例子,如果在写操作的过程中节点A临时宕机或不可达,复制将发送到节点D,这个是为了保证期望的可用性和持久性。发送达节点D的复制包括一个提示在它的元数据中:建议哪个节点是期望的接收节点(这个例子是A)。接收到暗示的节点将保存写的数据在独立的本地数据库,并定期扫描。
一旦侦测到节点A恢复,节点D将尝试发送的复制信息到节点A,一旦传输成功,节点D将删除本地的这些临时对象而不损失复制信息。
为了提升应用的高可用,可以将W设置为1,它表示写操作只要一个节点写到本地存储就被认为成功,这样只有所有的节点在不可用是写操作才被拒绝。但是,实际上,大多数Amazon应用在生产环境设置W为一个比较大的值,主要是为了满足期望的持久性程度。
处理永久性失败情况:同步复制
如果系统成员变化较低,提示移交方案较好。有一些场景暗示:在这些场景下,在返回源复制节点前提示移交方案不可用。Dynamo实现反熵(同步复制)协议保持复制同步。
为了侦测快速复制和最小化传输数据之间的矛盾,Dynamo采用Merkle trees方案。Merkletree是一个Hash树,叶节点是每个key的hash值,父节点的值是它子节点的值的hash(key的hash的hash)。好处:Merkletree的每个分支可以独立检查,而不需要整棵树所有数据。而且在复制检查不一致性时,Merkletree可以帮组减少所需传递的数据。例如两棵树的root节点的hash值相同,表示树的叶节点都相同,所以不需要同步。如果不同,两个node之间要叫号树上子节点的hash只,直到树的叶节点。主机host可以识别出不需要同步的keys。Merkletree最小化同步复制传输的数据,减少磁盘读操作的执行。
每个节点维护各自独立的Merkletree,在一定的key范围内。这使得节点可比较key是否在范围内。这个方案中,2个节点交换对应的Merkletree,那么它们对应的key范围是共有的。随后,采用树反转方案(treetraversal)描述上述节点,决定它们是否有一些不同而需要执行同步动作。这个方案缺点:但一个节点加入或退出系统,大量key范围变化要求tree重新计算。
成员资格和失败侦测
.环成员资格
由于Amazon环境内的节点的不可用通常是透明的,但可能持续较长时间。一个节点很少明确表示永久的离开,而且不应该导致重新均衡分区的分配,或重新修复不可达的复制。所以采用显式的机制初始化加入或去除环中的节点。自适应成员关系管理协议(gossip-basedprotocol)传递成员资格变化和维护最终的成员的一致性视图。每个节点每秒随机选择一个对等节点,这两个节点有效地协调它们之间持久的成员资格变更历史信息。
当一个节点首次开始,它选择自己token集合(在一段连续hash空间的多个虚拟节点),映射节点和token值。这些映射信息将持久化到本地磁盘,最初只包含本地节点和token集合信息。保存在Dynamo不同节点的映射信息在同一个通讯交换时候协调成员资格变更历史信息。而且分区信息和部署信息经一起gossip-basedprotocol传递,每个存储节点都知道它的对等节点能处理的token范围。这使得每个节点直接forward一个key的读/写操作到正确的节点集合。
.外部发现
为了避免逻辑隔离,一些Dynamo节点要作为种子节点的角色。种子节点经由外部机制发现,所有节点都知道。因为所有几点最后都和一个种子节点协调成员资格,逻辑隔离不太可能做到。种子节点可用从配置文件或配置服务中获取。
.
失败侦测
失败侦测为了避免在get和put操作,或传输分区信息和提示复制时,尝试与不可达的节点通讯。节点A人为节点B失败,如果节点B不能响应A的消息(即使节点B响应节点C的消息)。如果客户端请求在一定频率,产生Dynamo环中的内部节点通讯,节点A很快发现节点B没有响应。节点A使用备选的节点服务请求,即与B分区映射的节点。节点A定期尝试检查节点B是否恢复。如果没有客户请求的驱动两个节点之间的通讯,没有任何一个节点知道对方是否可达或有响应。
去中心化失败侦测协议使用简单的gossip-style协议,使得系统中的每个节点了解其他节点加入或推出。前期的设计Dynamo使用了一个去中心化的失败侦测机制维护一个全局一致的失败状态视图。后期决定采用
节点明确的加入或退出方法,避免采用全局失败状态视图。因为所有节点可以通过显式地调用加入和推出方法来得到通知持久节点的加入和退出通知;而临时节点的失败,通过各自节点与其他节点的通讯异常来侦测。
.
增加和删除存储节点
当一个新的节点X加入到系统,它将获得多个Token并随机散布到环里。对于每个赋予节点X的key范围,
有一些节点(<=N)目前是这些节点负责的,节点X的key范围中的key原则是落在这些节点的key范围内的。
为了分配key范围给节点X,一些已经存在的节点不再拥有一些key,而是交给节点X。
这种方案可以平均的分配存储节点的负载,对应满足延迟需求和保证快速加载比较重要。最后,需要在源和目的节点之间增加一个确认的回合,确认目的节点没有收到2次重复的key范围。
第五节 实现细节
在Dynamo,每个存储节点有3个软件组件:请求协调组件,成员资格和失败侦测组件,本地持久化引擎组件。所有组件均采用Java。
Dynamo的本地持久化组件支持不同存储引擎插件。引擎使用Berkely DB传统数据存储,BDBjava版本,MySQL和拥有持久化支持的内存缓冲。
可以插入式的持久化组件设计,主要原因是为了针对应用访问模式采用适当的存储引擎。例如,BDB能处理的对象主要是kb级别的,MySQL能处理更大的对象。应用选择Dynamo的本地持久化引擎取决于它们对象大小的分布。绝大多数Dynamo的生产实例主要使用BDB传统数据存储。
请求协调组件建立在事件驱动消息方式,底层消息处理管道被划分成多个步骤,类似SEDA架构。所有通讯均采用JavaNIO通道。每个客户端请求导致节点上创建一个状态机服务请求。状态机包含所有业务逻辑:识别可以响应某个key的节点,发送请求,等待响应,尝试重发,处理恢复和打包响应给客户端。每个状态机实例处理对应的一个客户端请求。
在读操作的响应返回到调用者后,状态机等待一个短暂的时间去接收任何重要的响应。如果在一些返回的响应中有旧的版本信息,协调这会用最新版本去更新这些节点。这个过程称为“读修复”,因为它修复这些节点的复制信息,这些节点在某个时间刚好缺失最近的一个更新,也能减轻对反熵协议(同步)的依赖。
在先前的章节,写请求被优先列表中的前N个节点之一协调处理。尽管它总是期望TopN的第一个节点协调写操作并在一个地点序列化所有的操作,但是这种方案会导致不均衡的分发负载,从而破坏SLA协议。这是因为请求没有根据对象做负载均衡。为了解决这个问题,优先列表中的任意一个节点都可以协调写操作。特别地,既然每个写操作通常紧跟在读操作之后,协调者会选择先前响应读操作最快的节点来处理写操作,这个信息是保存在请求的上下文信息中的。这个优化可以选择拥有这个数据的节点,而且可以提升获得"readyourwrites"的一致性。另外它也可以减少请求处理性能的变化,99.9%可以提升性能。
本文介绍了Dynamo,一个高度可用和可扩展的数据存储系统,被Amazon.com电子商务平台用来存储许多核心服务的状态。Dynamo已经提供了所需的可用性和性能水平,并已成功处理服务器故障,数据中心故障和网络分裂。Dynamo是增量扩展,并允许服务的拥有者根据请求负载按比例增加或减少。Dynamo让服务的所有者通过调整参数N,R和W来达到他们渴求的性能,耐用性和一致性的SLA。
在过去的一年生产系统使用Dynamo表明,分散技术可以结合起来提供一个单一的高可用性系统。其成功应用在最具挑战性的应用环境之一中表明,最终一致性的存储系统可以是一个高度可用的应用程序的构建块。