Amazon有些场景可以容忍这种不一致性,并且可以在这种场景下继续运行。例如,购物车应用要求所有的“Add to Cart”操作永远不能丢失或者被拒绝。如果购物车当前最新的状态不可用,那么用户可以在一个稍老的版本上做修改,并且这种修改也是有意义的、并且需要保留。但是他不能取代购物车最新的状态,因为最新的状态也有一些改变需要保留。在这里需要注意的是,“add to cart”和“delete iter from cart”在Dynamo中都是使用put请求。当一个顾客想要向购物车中添加或者删除一个商品时,如果购物车当前最新的状态不可用,那么该商品将会被添加到老版本的购物车中,并由随后的步骤来解决冲突。
https://levy5307.github.io/blog/Dynamo/
Dynamo是Amazon实现的一款KV存储。为了支持Amazon大规模且持续增长的用户,具有高可用、高扩展性的特性。Dynamo有一个重要的设计目标:允许应用自己控制自己的系统特性(例如持久性和一致性)让应用自己决定如何在功能、性能和成本效率之间取得折中。
Background
我们对我们的存储服务有如下几个要求:
Design Considerations
商业系统中的数据复制都是同步的,这样可以提供一个强一致的数据访问接口。为了达到这种级别的一致性,这些算法强制牺牲了在某种错误场景下的可用性。其实对于服务器和网络故障比较容易出现的场景,可以通过使用乐观复制技术来提高可用性。在这种复制技术中,数据变更在后台同步到其他节点,并发更新和网络失联也是可以容忍的,这种方式也叫做异步复制,其无法满足强一致性。Dynamo设计为最终一致性的存储服务,即所有的更新最终会到达所有的副本。但是这种模式的挑战在于如何检测和解决冲突。而解决冲突也会带来两个问题:何时解决冲突以及谁来解决冲突。
何时解决冲突
一个非常重要的设计考虑是决定何时执行解决更新冲突,例如,是在读的时候还是写的时候去解决冲突。一些传统的数据仓库是在写的时候解决冲突,这样可以保证读流程足够简单。在这种系统中,如果写入不能到达全部或者大多数的副本,写入将会被拒绝。与此相反,Dynamo被设计为永远可写的数据仓库。对于Amazon的许多服务来说,拒绝写入会导致非常差的客户体验。例如,尽管在发生网络或者服务器故障的时候,也应该允许用户向购物车中添加或者删除商品。这种需求导致我们将冲突解决放在了读取操作,以保证写入永远不会被拒绝。
谁来解决冲突
下一个问题在于,谁来执行冲突解决。存储和应用都可以解决这件事情。如果是由存储来做,那么选择将会相当受限,因为存储系统只能选用比较简单的策略,例如:最新写入有效。另外一方面,应用更加理解其data schema,其可以选择一个更适合自己的冲突解决方案。例如,购物车可以选择合并冲突的版本,返回一个合并后的购物车。不过尽管这样会带来很大的灵活性,但是很多应用并不想实现自己的一套冲突解决机制,因此这种情况下可以由存储系统来解决,采用一些简单的策略,例如前面所说的最新写入有效。
另外,还有一些其他的关键设计:
System Architecture
生产环境的存储系统的架构是很复杂的。除了数据持久化之外,系统还要为以下组件设计高扩展与健壮的解决方案,这些组件包括:负载均衡、成员管理、故障检测、故障恢复、副本同步、过载处理、状态转换、并发与任务调度、请求压缩、请求路由、系统监控和报警、以及配置管理。在这篇文章里主要关注Dynamo使用到的、分布式系统中的几个核心技术:分区、复制、版本化(versioning)、成员管理、错误处理、规模扩展(scaling)。
System Interface
Dynamo使用key通过一个非常简单的接口来访问对象,它提供两个操作:
其中context包含了对象相关的元数据,例如对象的版本。其对调用方是不透明的。context信息和对象时存储到一起的,这样可以很容易验证put请求的context是否是合法的。
Dynamo将调用方提供的key和对象都视为不透明的字节序列。它对key应用MD5 hash产生一个128bit的ID,并根据该ID计算应该存储到哪个节点。
Partitioning Algorithm
Dynamo的设计核心需求之一就必须支持增量扩展(scale incrementally)。这要求需要有一个机制将数据动态分散到系统中的不同节点上。Dynamo的数据分片机制依赖于一致性hash。在一致性hash中,hash函数的输出范围通常作为一个固定的环。每个节点会随机分配一个落在该环上的值,其代表了该节点所在环上的位置。对一个数据项,通过如下步骤找到对应的存储节点:
因此,每个节点都负责环上它的前继节点与自己范围内的区域。一致性hash的好处在于,添加或者删除节点只会影响相邻的节点,其他节点不受影响。
但是基础的一致性hash有如下缺点:
为了克服这些问题,Dynamo使用了一致性hash的变种,即:每个节点并不是映射到环上的一个点、而是多个点(Cassandra采用了一致性hash的另一个变种,具体可以参考)。Dynamo使用虚拟节点的概念。一个虚拟节点看上去和一个普通节点一样,但是一个实际节点对应的不止一个虚拟节点。具体来说货,当一个新的节点添加到系统后,它将会被分配环上的多个位置。
虚拟节点会带来如下好处:
Replication
为了实现高可用和持久性,Dynamo将数据复制到多个节点上。每个数据被复制到N个节点上,这里的N是由每个Dynamo集群自己配置的。每一个key都会被分配到一个coordinator节点,coordinator节点负责落到它管理范围内数据的复制。它除了自己复制一份之外,还会向环上沿着顺时针方向的N-1个节点上存储一个副本。因此每个节点都要负责从自己开始向前N个前继节点的数据范围。
如上图所示,当N=2时,Key的coordinator是节点D,但是其也会被复制到E和F。所以对于节点F,其负责的key范围是(A, F]。
存储某个特定key的所有节点组成一个列表,成为preference list。另外需要注意,由于Dynamo引入了虚拟节点,所以存储一个key的N个preference list,其物理节点可能少于N个,为了避免这个问题,preference list在选择节点的时候会跳过一些位置,以保证preference list里面的节点都在不同的物理节点上。
Data Versioning
Dynamo提供了最终一致性,所有更新都会异步的复制到所有副本。put()操作可以在应用到所有副本之前返回给调用者,因此将会导致紧接着的get()操作可能获取不到最新的数据。在没有故障的情况下,更新传播都是有一个时间上限的。然而在某些特定的故障场景下(服务器故障或者网络分区),更新可能在限定的时间内无法传递到所有副本。
Amazon有些场景可以容忍这种不一致性,并且可以在这种场景下继续运行。例如,购物车应用要求所有的“Add to Cart”操作永远不能丢失或者被拒绝。如果购物车当前最新的状态不可用,那么用户可以在一个稍老的版本上做修改,并且这种修改也是有意义的、并且需要保留。但是他不能取代购物车最新的状态,因为最新的状态也有一些改变需要保留。在这里需要注意的是,“add to cart”和“delete iter from cart”在Dynamo中都是使用put请求。当一个顾客想要向购物车中添加或者删除一个商品时,如果购物车当前最新的状态不可用,那么该商品将会被添加到老版本的购物车中,并由随后的步骤来解决冲突。