《Dynamo:Amazon`s Highly Available Key-value Sto...

这段时间学习了著名的Dynamo的论文,有些收获总结下来放在这里和大家分析下:)。由于英文水平实在太菜,存在很多理解不到位甚至是理解错了的地方,欢迎高手指正。

Dynamo简介

Dynamo是亚马逊的一个高可用、高性能的数据存储。Dynamo放弃了数据建模的能力,所有的数据对象采用最简单的Key-value模型存储,可以简单地将Dynamo视为一个巨大的Map,此外Dynamo牺牲了部分一致性来换取高可用性(availability)。

Dynamo并非建立在什么创新的伟大的技术上面,相反Dynamo最大的成就在于如何将现有的各种高新技术良好地融合在一起来满足可用性、可靠性、扩展性上的需求。我觉得Dynamo是基于对现有技术和场景的发问开始的,实现过程就是找到问题的解决方案,所以这篇总结也尽量尝试着用问题贯穿起来。

问题:Dynamo凭什么可以牺牲一致性而换取高可用性?

原因两个。

1,CAP理论:一个分布式的系统不可能同时为C(一致性,consistence)、A(可用性,avalibilty)、P(分区容忍,Partition tolerance)提供保证。也就是说鱼和熊掌和狗熊不可兼得(第三者叫什么好呢?所以就叫他狗熊吧),所以三者取其二,必须牺牲一个。

  •     C(consistence):分布式存储中各节点的数据保持一致,不会因更新导致数据不一致。
  •     A(avalibilty):保证每一个请求不论成功或失败都能收到一个响应。
  •     P(Partition tolerance):指当系统的任意信息丢失或者局部失败时,系统依然坚挺。


2,放弃实时一致性,追求最终一致性。考虑亚马逊的业务场景,Amazon运营的是一个电子商务(e-commerce platform)平台,峰值时同时服务数千万(tens million)用户。即使短暂的不可用(比如写拒绝)也会造成交易流失、用户体验差,这对于用户量极大的商务平台来说代价太高了,因此系统必须坚挺、要具备高可用性,可作出牺牲的就只剩下一致性了。在商用环境中已经证明了Dynamo牺牲的实时一致性保证最终一致性的策略是可行的。

问题:这样就合理了吗?上面只说了可用性和分区容忍性的重要,可没说一致性的不重要。

      一致性很重要,强调一下:Dynamo牺牲的是实时一致性,但是保证最终一致性。Dynamo并不是Amazon唯一的存储服务,Amazon开发了一系列的存储技术比如著名的Amazon S3(Amazon Simple Storage Service)。实际上针对Dynamo追求高可用、高可靠、高性能和保证最终一致性的特点,以其作为存储的服务包括:

  • Best seller list:畅销书列表,一个读取远远大于写的服务,不存在高并发的写。
  • Shopping Cart:购物车,这是个高并发写的服务,但是出现数据不一致的情况时可以简单的做数据融合消除不一致。
  • Session Mamagement:会话管理,会话状态的读写对网站性能影响很大,所以要优先保证高性能,发生数据冲突时可以简单地采用“最后一次写胜出(last-write-win)”的策略。
  • Dynamo还支撑了很多其它业务,这些业务的共同点都是对性能和可用性要求很高,能够在一定程度上容忍不一致性。


背景介绍

定位

    问题:数据库的进化史是从最初的Key-value数据库逐步发展出文档数据库,最终进化为现在的关系型数据库(Oracle、Mysql、DB2)的,那么正当关系型数据库广泛应用的时候,突然跑出来一个标榜Key-value的Dynamo是什么情况?算是返祖现象吗?

RMDB:我有强大的建模能力
Dynamo:我速度快、高可用
RMDB:我能提供强一致性保证
Dynamo:我速度快、高可用
RMDB:我支持事务管理
Dynamo:我速度快、高可用
RMDB:我可以集群、提供备份和性能扩展
Dynamo:我速度快、高可用,集群灵活
RMDB:我提供了复杂查询的能力,连表查询、嵌套查询
Dynamo:我速度快、高可用,集群灵活

    传统的关系型数据库提供了建模、一致性、事务管理和复杂查询等一系列的能力,但是这些复杂性也引入了一些问题导致在大数据应用中的水土不服,Dynamo等NoSQL的数据库(或者是存储方案)与关系型数据库之间是互补的关系,并非赤裸裸的竞争。
    大数据应中传统关系型数据库可能面对如下问题:

  • 需要昂贵的硬件支撑(指商用)。
  • 对程序员或运维人员个人技能的要求较高。
  • 侧重强一致性高于可用性,因此可用性的提升受到制约。
  • 性能扩展、集群都会受限于各种能力引入的复杂性。

    Dynamo是针对这些问题而设计的一套存储实现,Dynamo认为大多数业务对存储层的要求就是数据存储,数据建模能力、事务、强一致性、存储过程等特性的引入是以牺牲性能和可用性上为代价的。所以其放弃了数据建模、复杂查询、事务管理的能力,选择可用性高于一致性的策略。

需求、约束和能力

问题:Dynamo的定位是弥补RMDB所不能,那么Dynamo具备哪些优势和劣势?

使用Dynamo的应用会受到的约束:

  • Query Model(查询模型):Dynamo仅限于Key-value的查询,value可以保存任意的二进制对象。不支持跨多条数据的运算或关系操作,Dynamo定位于存储相对较小的数据对象(一般小于1MB)。
  • ACID:Atomic、Consistency、Isolution、Durablity(原子、一致性、隔离、稳定性)相对于传统的RMDB,Dynamo削弱了Consistency,并且不提供任何隔离性的保证。在RMDB中一系列的相关操作可以放在事务中执行,Dynamo只允许单条数据的操作。
  • Security:安全性。Dynamo预期运行环境是可信的,不提供任何安全机制(比如用户名、密码)。

Dynamo提供的优势包括:

  • 扩展性:Dynamo必须是容易扩展的,力求在增加节点时将对系统操作和系统本身的影响降到最低。
  • 高可用性:Dynamo极少拒绝读、写操作,对任何操作提供了较好的响应速度(成功或失败)。
  • 对称性:Dynamo中所有的节点都是同质的,不存在特殊节点(如master)。对称性降低了系统的部署和维护成本(至少你不用去区分master和slave了:>)。原文没有涉及的一个好处:对称性也消除了master节点成为系统性能瓶颈的隐患。
  • 去中心化:Dynamo是去中心化的系统,在中心化系统里中心节点的中断会导致整个系统“不坚挺”。去中心化的设计使系统更简单、易扩展和高可用。
  • 不均匀性:Dynamo可以利用其部署在的基础结构的不均匀性,也就是说组成Dynamo集群的机器性能是不完全一致的——地主家也不是每天都有余粮,服务器有新引够的,也有旧的(总不能每次都换新吧)。Dynamo可以利用Key Range的分配使每个Host承担不同程度的负担(后面会介绍)。

设计思想

Dynamo的最终一致性

问题:到底什么是最终一致性,Dynamo是如何权衡的?

    要回答这个问题先要说“不一致性”是哪里来的。Dynamo是一个分布式的存储服务,系统中每一份数据都会在若干个Host上复制这个过程可能会产生数据的不一致,另外高吞吐的情况下不同服务器同时接收到针对同一份数据的不同更新请求也可能造成不一致。
    这些不一致性是否可以避免?不能,这些不一致性的避免会使Dynamo退化成了类似RMDB的强一致系统,从而损失了高可用性。
    实际上所谓最终一致性是Dynamo将一致性处理推迟到了接受用户请求之后。简单的说就是不管用户写的是什么都先接受并保存下来,之后再去考虑一致性的处理。这里又引出了问题——什么时候处理(When)?谁来出来(Who)?
    
第一个问题比较容易回答,Dynamo要尽可能少的拒绝用户的写请求,所以选择了在读取的时候处理数据冲突问题。
    第二个问题,以往的经验在RMDB中一致性问题是由数据系统来处理的,可以采取数据取并集或者“最后一次胜出”的策略统一处理。然而不同的业务可能希望采取不同的处理方式,比如购物车的处理方式就倾向于取并集而非“最后一次胜出”(没有那个电子商务网站希望用户少买点吧?),而用户会话保持的业务就会反过来采取“最后一次胜出”的方法(用户的回话状态当然要去最近时间点的才靠谱啊)。因此Dynamo选择把数据冲突的协调逻辑交给业务逻辑处理,从而达到更高的灵活性。
    Dynamo对一致性的处理可以概括为不强调数据对象在更新过程中的一致性,改而强调在读取过程中发现冲突的数据对象并交给实际业务逻辑进行处理。

数据结构——Dynamo Ring

数据的分区和定位

问题:已经讲了很多Dynamo的优势、设计思路了,Dynamo到底长得什么样?是怎么实现这些特性的?

首先来看下Dynamo内部的数据结构:

图1:Dynamo 环上的分区和复制

    Dynamo对所有保存的数据Key做MD5哈希生成128位的特征码,将所有128位的数值首尾相接就形成了图1的环(Dynamo Ring),Dynamo Ring被节点(Node)切割形成了若干数据段(Key Range)。根据特征码将保存于Dynamo的数据对象分布到不同的Key-Range上,集群中的服务器(Host)各持有一个Node,负责存储当前Node与上一个Node划分出的Key-Range上的数据对象。

问题:这个数据结构是如何支撑扩展的?

    伴随Dynamo存储的数据不断膨胀,必然需要添加更多的Node来把Dynamo Ring切分成更小范围以扩张系统的性能。添加的Node会从其它相邻的节点上“偷取”一部分Key Range。这样在添加Node时受影响的就只有与之直接毗邻的那个Node了。比如我们在A、B之间添加Node A’,则只有Node 会受到影响将自己的一部分数据转移给A’。
    同样的在一个Node发生故障或者移除Node的时候,受影响的也只有它直接毗邻的那个Node。

问题:这样就平衡了吗?如果某个业务的数据都集中在Dynamo Ring的某一段区间呢?不就造成了严重的失衡?

    是的,Node没有办法保证平均负载。Dynamo实际使用的是虚拟节点(Virtual Node),Virtual Node拥有和Node一样的功能和责任,一个Host上可以存在多个Virtual Node并承担所有Virtual Node的责任,以下Node都是指Virtual Node。下面使用N代表Node的个数,在Dynamo系统中要求N >> Host数,这样带来的好处包括:

  • 平均了Dynamo中各个Host的负载。
  • 通过调整Host负责的Node个数达到调整Node负载的目的,利用组成集群的Host的不均匀性(Heterogeneous)。
  • Virtual Node的引入导致了Host增加或移除的影响扩展到多个Host上,这可能不是好事情。不过由于Virtual Node引入Dynamo Ring被切割成了N份而非与Host数相等,Key Range变得很小,所以虽然有很多Host要承担Host变动带来的影响,但是平均到每个Host上的工作量不大。

Node间关系维系

问题:新增加的Node是怎么知道其它Node的,既然是去中心化的系统,总不会又冒出个“Node Membership Server”吧?

    在Dynamo中节点的中断通常是暂时的很少发生节点永久不可达的情况,因此节点的短暂中断不应该导致Membership的重算和数据分区的调整,同样类似因人为错误导致一台Host的启动而自动加入Dynamo Ring中的事情也不应该发生。
    因此Dynamo设立了一个严格的机制控制Node的加入和移除。在Dynamo中不存在一个所谓的“Node Membership Server”,Dynamo通过Gossip-Protocol(留言传播协议)来实现节点间通信。当管理员通过命令行或者控制台添加一个Node时,某个已服役的Node接受到添加请求更新membership信息并本地持久化,每次添加和删除Node的动作都会形成修改记录(history)。以一秒为周期,每个Node会随机的选择另一个Node交换修改记录,最终每个Node都会形成一致的membership视图(就如同人与人之间留言的传播那样,最后大家都会知道)。
    留言传播是需要时间的,基于留言传播协议的关系维系机制可能会造成暂时的局部Ring。比如管理员通知Node A加入了Ring,接着又通知Node B加入Ring,这是Node A、Node B都认为自己是Ring中的成员,但是他们俩没办法马上认识彼此(各有各的小Ring)。Dynamo引入了Seed机制避免这个问题,有一些Node在Ring充当Seed Node,它们是可以通过外部机制获知的(静态文件或者配置服务),在Dynamo Ring上的所有Node都认识这些Seed Node,这样通过与Seed Node的信息交换就可以避免局部Ring的形成。

数据复制

Preference List(首选列表)

问题:Dynamo数据的备份是如何处理的?在讨论不一致性时提到了Dynamo没有强制所有Node同步更新,那么实际的数据复制是如何实现的?

    在这里需要引入三个字母N、W和R,N是Dynamo中一个数据要复制的份数,W是保证一个写请求成功的最少复制数,R是保证一个读请求最少要读取的份数。N、W、R是可配置的,当一个Dynamo系统中的配置为W+R > N时,Dynamo就是一个粗犷意义上的Quorum系统(保证了每次读都能读到最新的内容)。
    在Dynamo Ring上当前Node及其顺时针顺序尾随的若干个节点构成一个Preference List,当写请求到达时与其Key Range对应的Node负责接受写请求并复制到Preference List中的其它Node上,这个Node称之为Coordinator,考虑到Node中断或失败的情况Preference List实际包含的Node数是大于N的。由于引入了Virtual Node,Dynamo采取了Skipping Position的算法(我也不知道这是个啥算法)确保每个Host上的Node是间隔的,避免Preference List中的多个Node存在于同一个Host上。
    通常情况下Preference List的复制任务是由首个Node来承担的,并复制到前N个Node,但是在发生Node中断时这两项工作都可以有Preference List中后续的Node承担,同样的在高吞吐的情况下后续Node也可以分担首个Node的任务。

错误的处理

问题:Dynamo在出现Node中断的情况是如何处理读写请求的?如何达到高可用性?

    Dynamo使用的是“松散的法定人数”机制(Sloppy Quorum)来确保可用性的,在Quorum系统中当一个Node发生中断或失败时,请求是要报失败的。Dynamo中允许请求只要在N个正常的Node上响应即可,不限制必须是特定的(前N个)Node。
    比如在图1中Node A暂时失效或不可达了,本该在Node A上的复制请求移交(Handoff)给Node D承担,并且在数据对象的元数据里会添加信息指明应该保存在Node A上。Node D接收了请求后会把数据对象保存在独立的分区,一旦Node A恢复便尝试把数据转交给Node A保存,并在Node A接管数据对象后删除自身的备份,确保数据的复制数N不会增加。这个机制成为Hinted Handoff(标记移交?),通过Hinted Handoff机制Dynamo确保了系统不会因为Node暂时性中断而失败。
    除了单个Node的失败,Dynamo的数据是分布在多个DataCenter的,也要考虑到DataCenter跳票的情况。可能存在供电中断、制冷故障或是网络原因导致一个DataCenter暂时不可访问的情况,Dynamo可以通过配置使数据跨Data Center复制,说白了就是Preference List构建算法会考虑跨Data Center的Node。
    在系统摆动较少或者Node只是临时失效的情况下Hinted Handoff机制是最佳的方案,但是如果出现Hinted Handoff的数据没能正确返回原始节点的情况,这个机制就无能为力了(比如A还没好D也挂了……)。为了避免类似或其它的情况对系统造成威胁,Dynamo采用了anti-entropy protocol(反熵协议),用来确保各个Node中数据对象复制的同步和一致。
    Dynamo的Anti-entropy protocol使用Merkle Tree来降低数据同步比对和网络的开销,Merkle Tree是一个Hash树,树的叶子节点为各个数据的Hash值,上层节点是有下级节点的Hash生成的,这样Merkle Tree只需要比较Root的值就可以判断两棵树是否完全一致。在发生不一致的情况下,逐级递归向下比对即可迅速查找到不一致的数据。Dynamo中Host为其上的每个Key-Range建立一棵Merkle Tree,并定期的相互比较判断数据是否同步。Merkle Tree的好处是降低了数据比对的成本,缺点是一旦发生数据重新分区(Key Range变化),需要重新计算整棵树。

冲突解决

Vector clock和删除

问题:继续纠结Dynamo的最终一致性,复制过程中可能出现的不一致是如何被业务逻辑发现的呢?毕竟没办法发现就没有所谓的解决。

图二 vector clock示意图

    就像代码库有版本信息一样,Dynamo为每次写操作也加入了版本信息,这套机制称之为Vector clock。在此之前需要先明确一点,Dynamo的实现中的数据对象其实是可写但不可更新的(immutable),每一次写操作都会建立一个新版本的数据对象。当Dynamo发现新版本的数据对象和之前的版本不冲突时就会干掉旧版本的数据来实现更新(Dynamo并没有更原始新数据对象,而是删除了它)。Vector clock的构造很简单就是在数据对象中增加一个([Sx,n])的元数据,其中Sx是指Node,n是递增的版本号。
    如图二所示,数据K1第一个写请求被Node X接收产生数据对象D1([Sx,1])并复制到Y、Z上(N = 3情况下),然后第二个请求到达Node X更新了数据对象K1,这时Node X发现K1的Vector clock是[Sx, 1]没有冲突,所以生成新数据对象D2([Sx, 2])并干掉D1(注意Vector lock更新为[Sx,2])。这个时候Node X发生中断,之后的两个更新请求分别到达了Node Y 和 Node Z,Node Y在接收到请求后追加了自己的信息产生数据对象D3([Sx, 2][Sy,1]),由于数据还没有复制到Node Z,所以Node Z产生了数据对象D4([Sx,2][Sz,1])。    当业务再次读取数据时,Dynamo发现存在数据D3、D4,这两个版本的数据虽然都是基于[Sx,2]版本的但是没办法确定[Sy,1]和[Sz,1]的关系,数据冲突产生了。Dynamo将两个版本的数据同时返回给客户端,交由业务逻辑来处理数据冲突。业务逻辑解决冲突后,再次更新数据K1,这时Node X恢复正常并接收了这个请求产生数据对象D5([Sx,3][Sy,1][Sz,1]),当数据对象复制到Node Y和Node Z时发现是基于旧数据对象的更新,继而Node Y、Node Z更新数据为D5。

问题:Vector clock的数据是怎么传给客户端的,更新数据时Dynamo怎么知道客户端的数据版本?

    Vector clock的信息是存放在Context中的,在Dynamo返回数据时会返回Context信息,这个信息交由Client Library处理通常对业务逻辑是透明的。
    值得注意的是,Vector clock的信息是不断增长的,如果过长就会影响到传输和比较的性能开销。Dynamo中Vector clock值(即[Sx,n])是含有时间戳的,当Vector clock的长度超出临界值时(通常是10)最旧的Vector clock信息将被删除。

Get和Put的执行

问题:能否描述下Dynamo读写的流程呢?作为业务逻辑如何和Dynamo交互呢,是否需要知道所有的节点?

    客户端与Dynamo的通信有两种可行的方案。第一种是在Dynamo和Client之间引入负载均衡服务器,客户端只需要知道服务器的地址,消息的转发是根据Node的忙碌状态转发的。Node在接收的请求后判断请求是否可以由自己处理(即Key是否落在自己负责的Key Range上),如果不是的话就会转发给对应Preference List中的Top N Node。另一种方案是Client维持Dynamo的分区信息,根据Key的信息能够直接定位到负责的Node。这两种方案中,第一种方式带来的好处是实现简单,不需要考虑客户端更新Membership的问题,而第二种方式省略了负载均衡服务器分发和Node跳转带来的延迟。
    首先简单介绍Get请求的过程,Node接收到Get请求后向Preference List中所有活跃的Node发出读指令,在接收到前R个响应后Node将数据对象返回给客户端(如果发现R个请求之间存在冲突则返回所有的版本)。在返回数据后并不等于Get操作结束,Dynamo为Get请求创建一个状态机,除了向其它Node索取数据和处理冲突外,当返回数据对象后这个状态机还会维持一段时间继续接受Node的响应,试图发现保存了旧数据对象的Node并组织其更新,这个机制在Dynamo中称为“Read Repair”。
    接下来说相对简单的Put动作。Node在接收到写请求后,根据Context信息为数据对象产生一个新的Vector clock的信息,更新本地数据。这之后Node会向可达性最佳的(highest-ranked reachable nodes)N个Node发送更新的请求,如果超过W - 1个Node及时响应,就认为写请求是成功的。

W、R、N的调节策略

问题:全文中一直都充斥着W、R、N几个字母,这些是可调的吗?到底有什么用?

    个人认为W、R、N才是Dynamo最精髓的地方。这里在回顾一下W、R、N的含义:

  • W:一次成功的写请求至少要写入的Node数。在Dynamo中put的操作必须等到W个Node都完成写动作才会成功返回,否则失败。
  • R:一次读请求至少要读取的副本数。Dynamo接收到get的操作后向所有Preference List中的Node发送读请求,直到获取的R个Node的响应后才会返回数据,否则失败。
  • N:数据对象被复制的次数。N大于W,Dynamo的put操作最终会尝试将数据复制到N个Node上,但是为了提高响应速度在复制了W个Node时写请求就认为是成功的。在成功返回后Dynamo仍然会尝试其余的N - W个节点的数据复制。

    前文中已经介绍了Dynamo被用于Amazon不同的业务中,各个业务对一致性、可靠性、读性能、写性能都有不同的要求。Dynamo通过调整W、R、N这三个因子实现满足不同业务场景的目的。简单举例:
    追求写性能:比如我的业务需要频繁写但是读请求却很少,甚至读写之间有一个数量级的差距(如交易日志,每一笔交易都会产生若干日志,而只有交易出问题的时候才会考虑查看交易日志)。这种情况下可以设置一个足够大的N保证数据对象有可靠的复制数,同时设置较小的W和大的R(极端情况下W = 1)。较小的W保证了写的性能,即有少量的节点回应就认为成功写入了;大的R值保证了读取时数据一致性,即使由频繁更新产生了数据不一致也能在读的时候最大范围的发现冲突并通知业务逻辑解决冲突。这种方案的代价是读请求需要等待较多的Node响应,读性能差。
    追求读性能:这种方案适合读 >> 写的业务场景,比如畅销书列表。可能一个月更新一次列表就可以了,但是每次页面刷新都涉及到读取数据。同样选择合适N值保证健壮性,选择较大的W(或者干脆设置W = N,这样系统将是强一致性的, R可以等于1)和较小的R。达到的结果是牺牲写性能,追求更高的读性能和一致性。
    除此以外还可能涉及到一致性和高可用性之间的权衡,在适当的降低W和R值后(特别是W)能够达到牺牲一致性追求可用性的目的,此时Dynamo对读写操作Node响应的要求降低,提升了读写的响应速度和成功机会。反之则是牺牲可用性追求一致性的系统。还有几点要注意的:1,商用系统中通常不会设置W为1,这会威胁到系统的可靠性。2,N的值一般为3,过小的N值就存在无法实现灾难恢复的风险了。3,一般需要设置为W + R > N,这是一个可靠的Quorum系统,保证了每次读操作都能够读到最新的数据。如果W + R < N则有可能读到旧的数据增加系统而不一致性。

附件:Dynamo总结脑图

转载于:https://my.oschina.net/pangyangyang/blog/151093

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值