剖析Amazon(亚马逊)的网站数据存储架构
一、系统概述
1、amazon平台概述
amazon平台是一个由数百服务组成的面向服务的架构,其秉承高度去中心化、松散耦合、完全分布式的原则,具体架构参考下图。
在这种环境中,尤其需要一个始终可用的存储系统,由此,dynamo诞生了。
2、dynamo概述
dynamo是amazon提供的一款高可用的分布式key-value存储系统,其满足可伸缩性、可用性、可靠性。
cap原理满足:通过一致性哈希满足p,用复制满足a,用对象版本与向量时钟满足c。用牺牲c来满足高可用的a,但是最终会一致。但是,是牺牲c满足a,还是牺牲a满足c,可以根据nwr模型来调配,以达到收益成本平衡。
dynamo内部有3个层面的概念:
key-value:key唯一标识一个数据对象,value标识数据对象实体,通过对key来完成对数据对象的读写操作。
节点node:节点是指一个物理主机。在每个节点上,会有3个必备组件:请求协调器(request coordination)、成员与失败检测、本地持久引擎(local persistence engine),这些组件都由java实现。本地持久引擎支持不同的存储引擎,最主要的引擎是berkeley database transactional data store(存储数百k的对象更合适),其它还有bdb java edtion、mysql以及一致性内存cache。本地持久化引擎组件是一个可插拔的持久化组件,应用程序可以根据需要选择最合适的存储引擎,比如:如果存储对象的通常为数千字节则可以选择bdb,如果是更多尺寸则可以选择mysql。生产中,dynamo通常使用bdb事物数据存储。
实例instance:从应用的角度来看就是一个服务,提供io功能。每个实例由一组节点组成,这些节点可能位于不同的idc,这样idc出现问题也不会导致数据丢失,这样会有更好的容灾和可靠性。
二、背景条件
1、系统假设与要求
(1)查询模型
基于key-value模型,而不是sql即关系模型。存储对象比较小,通常小于1mb。
(2)acid属性
传统的关系数据库中,用acid(a原子性、c一致性、i隔离性、d持久性)来保证事务,在保证acid的前提下往往有很差的可用性。dynamo用弱一致性c来达到高可用,不提供数据隔离i,只允许单key更新。
(3)效率
在廉价的机器上满足sla,通过配置来满足延时和吞吐量的要求,因此,必须在性能、成本、可用性和持久性保证之间做权衡。
(4)其它假设
dynamo仅在amazon内部使用,因此,认为其使用环境是可信赖的。
2、服务水平协议(sla)
所谓服务水平协议是指客户端和服务端在某几个指标上达成一致的一个协议,通常包括客户端请求api的速率、服务端的预期延时,比如:在客户端每秒500个请求负载的高峰时,99.9%的响应时间为300毫秒。
一般业界,对这种面向性能的sla采用平均数(average)、中值(median)和预期变化(expected variance)。但是这些指标只能对大多数客户端有良好体验,而不是所有。为了解决这个问题,dynamo采用99.9%百分位来代替这些指标。
3、设计考虑(复制数据)
传统的数据复制算法,在出现故障时,为了保证数据一致性*牺牲掉可用性,即:与其不能确定数据是否正确,不如让数据一直不可用直到数据绝对正确时。
但是,一个高度灵活的系统应该能够让用户知道在何种情况下能到达哪些属性,dynamo就是如此。
对于故障是常态的系统来说,采用乐观复制技术可以提供系统的可用性,但带来的问题是需要检测并协调解决冲突,协调解决冲突的过程又包含两个问题,即:何时协调和由谁协调。dynamo的设计是数据存储最终一致,即所有更新操作最终到达所有副本。
(1)何时协调
无外乎两种情况:写或者读时协调冲突。
传统数据存储在写时协调冲突,即如果给定时间内数据不能达到所要求的所有或大多数副本数,则写入可能会被拒绝。
amazon认为拒绝客户的更新操作会导致糟糕的用户体验,典型应用是购物车服务,即使出现故障,客户仍然可以向购物车添加或者删除物品,基于此,dynamo的目标定位为“永远可写”(always writable)即数据存储的“写”是高可用的。也就是说dynamo为了确保“写”永远不会被拒绝,那么数据存储在读时协调冲突。
(2)由谁协调
无外乎两种情况:由数据存储本身或客户端应用程序来协调。
如果是数据存储本身协调,则只能使用简单策略来协调冲突的更新操作,比如:“最后一次写入获胜”(last write wins)。
如果是客户端应用程序协调,则应用程序可以根据业务需求来选择最适合协调冲突的方法。
dynamo选择了后者,典型应用还是购物车服务,返回所有数据对象版本,最后选择合并完冲突的版本。
三、关键技术
dynamo作为一类分布式系统的典型代表,其众多关键技术给其带来一系列的优势,具体参看下表:
1、数据分区
hash算法:使用md5对key进行hash以产生一个128位的标示符,以此来确定key的存储节点。
为了达到增量可伸缩性的目地,dynamo采用一致性哈希来完成数据分区。在一致性哈希中,哈希函数的输出范围为一个圆环,如图2所示,系统中每个节点映射到环中某个位置,而key也被hash到环中某个位置,key从其被映射的位置开始沿顺时针方向找到第一个位置比其大的节点作为其存储节点,换个角度说,就是每个系统节点负责从其映射的位置起到逆时针方向的第一个系统节点间的区域。
一致性哈希最大的优点在于节点的扩容与缩容,只影响其直接的邻居节点,而对其它节点没有影响。这样看似很完美了,但是亚马逊没有因此而停止脚本,这是其伟大之处,其实还存在两个问题:节点数据分布不均匀和无视节点性能的异质性。为了解决这两个问题,dynamo对一致性哈希进行了改进而引入了虚拟节点,即每个节点从逻辑上切分为多个虚拟节点,每个虚拟节点从逻辑上看像一个真实节点,这样每个节点就被分配到环上多个点而不是一个单点。
2、数据复制
为了实现高可用,dynamo将每个数据复制到n台主机上,其中n是每个实例(per-instance)的配置参数,建议值为3。每个key被分配到一个协调器(coordinator)节点,协调器节点管理其负责范围内的复制数据项,其除了在本地存储其责任范围内的每个key外,还复制这些key到环上顺时针方向的n-1个后继节点。这样,系统中每个节点负责环上从其自己位置开始到第n个前驱节点间的一段区域。具体逻辑见图2,图中节点b除了在本地存储键k外,还在节点c和d处复制键k,这样节点d将存储落在范围(a, b]、(b, c]和(c, d]上的所有键:
对于一个特定的键都有一个首选节点列表,由于虚拟节点的存在,为了解决节点故障的问题,构建首先节点列表时会跳过环上某些位置,让这些节点分别位于不同的物理节点上,以保证高可用。
为了保证复制时数据副本的一致性,dynamo采用类似于quorum系统的一致性协议实现。这里涉及到三个关键参数(n, r, w),其中,n是指数据对象复制到n台主机,协调器负责将数据复制到n-1个节点上,亚马逊建议n配置为3,r代表一次成功的读取操作中最小参与节点数量,w代表一次成功的写操作中最小参与节点数量。r+w>n,则会产生类似于quorum的效果。该模型中,读(写)延迟由最慢的r(w)复制副本决定,为了得到比较小的延迟,r和w通常配置为小于n。亚马逊建议(n, r, w)设置为(3, 2, 2)以兼顾性能与可用性。r和w直接影响性能、扩展性和一致性,如果w设置为1,则一个实例中只要有一个节点可用,也不影响写操作,如果r设置为1,只要有一个节点可用,也不会影响读请求,r和w值过小则影响一致性,过大则可用性,因此,需要在r和w两个值之间平衡,这也是dynamo的一个亮点之一。
3、版本合并
由前文可知,dynamo为了保证高可用,对每份数据都复制了多份(建议3份),在数据没有被异步复制到所有副本前,如果有get操作会取到不一致的数据,但是dynamo提供最终一致性。在亚马逊平台中,购物车就是这种情况的典型应用,为了保证购物车永远可用,对任何一个副本的任何一次更改操作的结果都会当做一个数据版本存储起来,这样当用户get时就会取到多个版本,这样也就需要做数据版本合并了。dynamo将合并工作推给应用程序,在这里就是购物车get时处理。
dynamo用向量时钟来标识同一数据在不同节点上多个副本之间的因果关系。向量时钟实际上就是一个列表,列表的每个节点是一个(node, counter)对,即(节点,计数器)列表。数据版本之间的关系要么是因果关系,要么是平行关系,关系判断依赖于计数器值大小,如果第一个时钟对象的计数器小于或等于所有其它时钟对象的计数器时则是因果关系,那么因是果的祖先可以认为是旧版数据而直接忽略,否则是平行关系,那么就认为数据版本产生了冲突,需要协调并合并。
在dynamo中,当客户端更新一个对象时,必须指定更新哪个版本数据,更新版本依赖于早期get操作时获得的向量时钟。
向量时钟的使用过程图上图3所示,具体流程解析如下:
客户端写入一个新对象。节点sx处理了这个请求,处理对key的写:序列号递增,并创建数据的向量时钟,这样在该节点上生成对象d1和向量时钟[(sx, 1)]。
客户端更新该对象。假设由同样的节点即sx处理了这个请求,由于该节点有了d1和向量时钟[(sx, 1)],则更新该对象后在该节点上生成对象d2和向量时钟[(sx, 2)],d2继承自d1,即d2覆写d1,计数器增1,但其它节点此时可能是d1,也可能是d2,这取决于网络和节点状态。
假设同一客户端更新该对象但被不同的服务器处理了。节点sy处理了这个请求,则更新该对象后在该节点上生成对象d3和向量时钟[(sx, 2), (sy, 1)]。
假设另一客户端读取到了d2并尝试更新它但被另一个不同的服务器处理了。节点sz处理了这个请求,则更新该对象后在该节点上生成对象d4和向量时钟[(sx, 2), (sz, 1)]。
节点数据版本回收。现在有4个版本的数据存在并在各个节点之间传递了,当节点收到d3或d4时,会根据向量时钟将d1和d2回收掉,因为其是d3和d4的祖先。但是收到d3和d4的节点,根据向量时钟发现它们之间是并行关系,则保留二者,并在客户端get时将二者都提交给客户端由其来协调并合并版本。
假设客户端读取数据,则会获取到d3和d4,根据两者的向量时钟,会合并为d5和向量时钟[(sx, 2), (sy, 1), (sz, 1)],节点sx协调写操作,并更新对象和向量时钟。
从上面的过程中可以看出,在节点比较多且情况极端时,向量时钟列表会增长,dynamo对此采用时钟截断方案来解决此问题,即(node, counter)对带有时间戳,在数目达到阈值(比如:10)时,将最早的一对从向量时钟中删除。
4、故障检测
(1)ring membership
每个节点启动时存储自己在环上的映射信息并持久化到磁盘上,然后每个节点每隔一秒随机选择一个对等节点,通过gossip协议传播节点的映射i信息,最终每个节点都知道对等节点所处理范围,即每个节点都可以直接转发一个key的读/写操作到正确的数据集节点,而不需要经过中间路由或者跳。
(2)external discovery
如果人工分别往dynamo环中加入节点a和b,则ring membership不会立即检测到这一变化,而出现暂时逻辑分裂的dynamo环(a和b都认为自己在环中,但是互相不知道对方存在)。dynamo用external discovery来解决这个问题,即有些dynamo节点充当种子节点的角色,在非种子节点中配置种子节点的ip,所有非种子节点都与种子节点协调成员关系 。
(3)failure detection
dynamo采用类gossip协议来实现去中心化的故障检测,使系统中的每个节点都可以了解其它节点的加入和likai
5、故障处理
传统的quorum,在节点故障或者网络故障情况下,系统不可用。为了提高可用性,dynamo采用sloppy quorum和hinted handoff,即所有读写操作由首选列表中的前n个健康节点执行,而发往故障节点的数据做好标记后被发往健康节点,故障节点重新可用时恢复副本。
如上面所示dynamo配置n为3。如果在写过程中节点a暂时不可用(down或无法连接),则发往a的副本将被发送到节点d,发到d的副本在其原始数据中有一个hint以表明节点a才是副本的预期接收者,d将副本数据保存在一个单独的本地存储中,在检测到a可用时,d尝试将副本发到a,如果发送成功,d会将数据从本地存储中删除而不会降低系统中的副本总数。
一个高可用的存储系统具备处理整个idc故障(断电、自然灾害、网络故障灯)的能力是非常重要的,dynamo就具备此能力。dynamo可以配置成跨多个idc复制对象,即key的首选列表由跨多个idc的节点组成,这些idc之间由高速专线连接,跨多个idc的复制方案使得dynamo能够处理整个idc故障。
此外,为了处理在hinted副本移交会预期节点之前该副本不可用的情况,dynamo实现了anti-entropy协议来保持副本同步,为了更快递检测副本之间的不一致性并减少传输量,dynamo采用merkletree。
6、扩容/缩容
(1)扩容
当一个新节点x加入到系统中时,其得到一些随机分配到环上的token,节点x会负责处理一个key range,而这些key在节点x加入前由现有的一些节点负责,当节点x加入后,这些节点将这些key传递给节点x。以图2为例,假设节点x添加到环中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加入的确认信号后出发这一过程。
(2)缩容
当从系统中删除一个节点时,key的重新分配情况与步骤(1)正好相反。
7、读/写操作
读取和写入由请求协调组件执行,每个客户端请求都将导致在处理该请求的节点上创建一个状态机,每个状态机都包含以下逻辑:
标识负责一个key的节点;
发送请求;
等待回应;
可能的重试处理;
加工和包装返回客户端响应。
每个状态机实例只处理一个客户端请求,如果是一个读请求,则状态机如下:
发送读请求到相应结点;
等待所需的最低数量的响应;
如果在给定的时间内收到的响应太少,则请求失败;
否则收集所有数据的版本,并确定要返回的版本;
如果启用版本合并,则执行语法协调并生成一个对客户端不透明的写上下文,其中包含一个囊括所有版本的向量时钟。
返回读取响应给客户端后,状态机等待一段时间以接受任何悬而未决的响应,如果任何响应返回了过时的版本,则协调员用最新版本更新这些节点,以完成读修复。
写请求通常跟随在读请求之后,则协调员由读操作答复最快的节点充当,这种优化能提高读写一致性。
五、解决问题
1、可用性
完全去中心化,无单点,永远可写。
2、伸缩性
带虚拟机节点的一致性hash:一致性hash解决扩容/缩容问题,虚拟节点解决机器异质性问题。
3、可靠性
数据复制多份副本,用向量时钟解决版本合并问题。
4、可配置
平衡性可调,即根据(n,w,r)模型平衡可用性和一致性,建议模型参数为(3,2,2)。
上一篇: 摄像头常见问题三例
下一篇: 蓝牙商务伴侣充电要求