笔记-分布式键值系统

分布式键值模型可以看成是分布式表格模型的一种特例。然而,由于它只支持针对单个key-value的增、删、查、改操作,因此,适用3.3.1节提到的哈希分布算法。

Amazon Dynamo是分布式键值系统,最初用于支持购物车应用。Dynamo将很多分布式技术融合到一个系统内,学习Dynamo的设计对理解分布式系统的理论很有帮助。当然,这个系统的主要价值在于学术层面,从工程的角度看,Dynamo牺牲了一致性,却没有换来什么好处,不适合直接模仿。

Amazon Dynamo

Dynamo以很简单的键值方式存储数据,不支持复杂的查询。Dynamo中存储的是数据值的原始形式,不解析数据的具体内容。Dynamo主要用于Amazon的购物车及S3云存储服务。

Dynamo通过组合P2P的各种技术打造了线上可运行的分布式键值系统,表5-1中列出了Dynamo设计时面临的问题及最终采取的解决方案。


数据分布

Dynamo系统采用3.3.1节(见图3-2)中介绍的一致性哈希算法将数据分布到多个存储节点中。一致性哈希算法思想如下:给系统中每个节点分配一个随机token,这些token构成一个哈希环。执行数据存放操作时,先计算主键的哈希值,然后存放到顺时针方向第一个大于或者等于该哈希值的token所在的节点。一致性哈希的优点在于节点加入/删除时只会影响到在哈希环中相邻的节点,而对其他节点没影响。

考虑到节点的异构性,不同节点的处理能力差别可能很大,Dynamo使用了改进的一致性哈希算法:每个物理节点根据其性能的差异分配多个token,每个token对应一个“虚拟节点”。每个虚拟节点的处理能力基本相当,并随机分布在哈希空间中。存储时,数据按照哈希值落到某个虚拟节点负责的区域,然后被存储在该虚拟节点所对应的物理节点中。

如图5-1所示,某Dynamo集群中原来有3个节点,每个节点分配了3个token:节点1(1,4,7),节点2(2,3,8),节点3(0,5,6)。存放数据时,首先计算主键的哈希值,并根据哈希值将数据存放到对应token所在的节点。假设增加节点4,Dynamo集群可能会分别将节点1和节点3的token 1和token 5迁移到节点4,节点token分配情况变为:节点1(4,7),节点2(2,3,8),节点3(0,6)以及节点4(1,5)。这样就实现了自动负载均衡。


为了找到数据所属的节点,要求每个节点维护一定的集群信息用于定位。Dynamo系统中每个节点维护整个集群的信息,客户端也缓存整个集群的信息,因此,绝大部分请求能够一次定位到目标节点。

由于机器或者人为的因素,系统中的节点成员加入或者删除经常发生,为了保证每个节点缓存的都是Dynamo集群中最新的成员信息,所有节点每隔固定时间(比如1s)通过Gossip协议的方式从其他节点中任意选择一个与之通信的节点。如果连接成功,双方交换各自保存的集群信息。

Gossip协议用于P2P系统中自治的节点协调对整个集群的认识,比如集群的节点状态、负载情况。我们先看看两个节点A和B是如何交换对世界的认识的:

1)A告诉B其管理的所有节点的版本(包括Down状态和Up状态的节点);

2)B告诉A哪些版本它比较旧了,哪些版本它有最新的,然后把最新的那些节点发给A(处于Down状态的节点由于版本没有发生更新所以不会被关注);

3)A将B中比较旧的节点发送给B,同时将B发送来的最新节点信息做本地更新;

4)B收到A发来的最新节点信息后,对本地缓存的比较旧的节点做更新。

由于种子节点的存在,新节点加入可以做得比较简单。新节点加入时首先与种子节点交换集群信息,从而对集群有了认识。DHT(Distributed Hash Table,也称为一致性哈希表)环中原有的其他节点也会定期和种子节点交换集群信息,从而发现新节点的加入。

集群不断变化,可能随时有机器下线,因此,每个节点还需要定期通过Gossip协议同其他节点交换集群信息。如果发现某个节点很长时间状态都没有更新,比如距离上次更新的时间间隔超过一定的阈值,则认为该节点已经下线了。

一致性与复制

为了处理节点失效的情况(DHT环中删除节点),需要对节点的数据进行复制。思路如下:假设数据存储N份,DHT定位到的数据所属节点为K,则数据存储在节点K,K+1,……,K+N-1上。如果第K+i(0≤i≤N-1)台机器宕机,则往后找一台机器K+N临时替代。如果第K+i台机器重启,临时替代的机器K+N能够通过Gossip协议发现,它会将这些临时数据归还K+i,这个过程在Dynamo中叫做数据回传(Hinted Handoff)。机器K+i宕机的这段时间内,所有的读写均落入到机器[K,K+i-1]和[K+i+1,K+N]中。如果机器K+i永久失效,机器K+N需要进行数据同步操作。一般来说,从机器K+i宕机开始到被认定为永久失效的时间不会太长,积累的写操作也不会太多,可以利用Merkle树对机器的数据文件进行快速同步(参见下一小节)。

NWR是Dynamo中的一个亮点,其中N表示复制的备份数,R指成功读操作的最少节点数,W指成功写操作的最少节点数。只要满足W+R>N,就可以保证当存在不超过一台机器故障的时候,至少能够读到一份有效的数据。如果应用重视读效率,可以设置W=N,R=1;如果应用需要在读/写之间权衡,一般可设置N=3,W=2,R=2;当然,如果丢失最后的一些更新也不会有影响的话,也可以选择W=1,R=1,N=3。

NWR看似很完美,其实不然。在Dynamo这样的P2P集群中,由于每个节点存储的集群信息有所不同,可能出现同一条记录被多个节点同时更新的情况,无法保证多个节点之间的更新顺序。为此Dynamo引入向量时钟(Vector Clock)的技术手段来尝试解决冲突,如图5-2所示。


Dynamo中的向量时钟用一个[nodes,counter]对表示。其中,nodes表示节点,counter是一个计数器,初始为0,节点每次更新操作加1。首先,Sx对某个对象进行一次写操作,产生一个对象版本D1([Sx,1]),接着Sx再次操作,counter值更新为2,产生第二个版本D2([Sx,2]);之后,Sy和Sz同时对该对象进行写操作,Sy将自身的信息加入向量时钟产生了新的版本D3([Sx,2],[Sy,1]),Sz同样产生了新的版本信息D4([Sx,2],[Sz,1]),这时系统中就有了两个冲突的版本。最常见的冲突解决方法有两种:一种是通过客户端逻辑来解决,比如购物车应用;另外一种常见的策略是"last write wins",即选择时间戳最新的副本,然而,这个策略依赖集群内节点之间的时钟同步算法,不能完全保证准确性。

向量时钟不能完美解决冲突,即使N+W>R,Dynamo也只能保证每个读取操作能读到所有的更新版本,这些版本可能冲突,需要进行版本合并。Dynamo只保证最终一致性,如果多个节点之间的更新顺序不一致,客户端可能读取不到期望的结果。这个不一致问题需要注意,因为影响到了应用程序的设计和对整个系统的测试工作。

容错

Dynamo把异常分为两种类型:临时性的异常和永久性异常。有一些异常是临时性的,比如机器假死;其他异常,如硬盘报修或机器报废等,由于其持续时间太长,称为永久性的。下面解释Dynamo的容错机制:

数据回传 在Dynamo设计中,一份数据被写到K,K+1,……,K+N-1这N台机器上,如果机器K+i(0≤i≤N-1)宕机,原本写入该机器的数据转移到机器K+N,如果在指定的时间T内K+i重新提供服务,机器K+N将通过Gossip协议发现,并将启动传输任务将暂存的数据回传给机器K+i。

Merkle树同步 如果超过了时间T机器K+i还是处于宕机状态,这种异常被认为是永久性的。这时需要借助Merkle树机制从其他副本进行数据同步。Merkle树同步的原理很简单,每个非叶子节点对应多个文件,为其所有子节点值组合以后的哈希值;叶子节点对应单个数据文件,为文件内容的哈希值。这样,任何一个数据文件不匹配都将导致从该文件对应的叶子节点到根节点的所有节点值不同。每台机器对每一段范围的数据维护一颗Merkle树,机器同步时首先传输Merkle树信息,并且只需要同步从根到叶子的所有节点值均不相同的文件。

读取修复 假设N=3,W=2,R=2,机器K宕机,可能有部分写操作已经返回客户端成功了但是没有完全同步到所有的副本,如果机器K出现永久性异常,比如磁盘故障,三个副本之间的数据一直都不一致。客户端的读取操作如果发现了某些副本版本太老,则启动异步的读取修复任务。该任务会合并多个副本的数据,并使用合并后的结果更新过期的副本,从而使得副本之间保持一致。

负载均衡

Dynamo的负载均衡取决于如何给每台机器分配虚拟节点号,即token。由于集群环境的异构性,每台物理机器包含多个虚拟节点。一般有如下两种分配节点号的方法。

随机分配。每台物理节点加入时根据其配置情况随机分配S个Token。这种方法的负载平衡效果还是不错的,因为自然界的数据大致是比较随机的,虽然可能出现某段范围的数据特别多的情况(如baidu、sina等域名下的网页特别多),但是只要切分足够细,即S足够大,负载还是比较均衡的。这个方法的问题是可控性较差,新节点加入/离开系统时,集群中的原有节点都需要扫描所有的数据从而找出属于新节点的数据,Merkle树也需要全部更新;另外,增量归档/备份变得几乎不可能。

数据范围等分+随机分配。为了解决上种方法的问题,首先将数据的哈希空间等分为Q=N×S份(N=机器个数,S=每台机器的虚拟节点数),然后每台机器随机选择S个分割点作为Token。和上种方法一样,这种方法的负载也比较均衡,并且每台机器都可以对属于每个范围的数据维护一颗逻辑上的Merkle树,新节点加入/离开时只需扫描部分数据进行同步,并更新这部分数据对应的逻辑Merkle树,增量归档也变得简单。

另外,Dynamo对单机的前后台任务资源分配也做了一些工作。Dynamo中同步操作、写操作重试等后台任务较多。为了不影响正常的读写服务,需要对后台任务能够使用的资源做出限制。Dynamo中维护一个资源授权系统。该系统将整个机器的资源切分成多个片,监控60秒内的磁盘读写响应时间,事务超时时间及锁冲突情况,根据监控信息算出机器负载从而动态调整分配给后台任务的资源片个数。

讨论

Dynamo采用无中心节点的P2P设计,增加了系统可扩展性,但同时带来了一致性问题,影响上层应用。另外,一致性问题也使得异常情况下的测试变得更加困难,由于Dynamo只保证最基本的最终一致性,多客户端并发操作的时候很难预测操作结果,也很难预测不一致的时间窗口,影响测试用例设计。

总体上看,Dynamo在Amazon的使用场景有限,后续的很多系统,如Simpledb,采用其他设计思路以提供更好的一致性。主流的分布式系统一般都带有中心节点,这样能够简化设计,而且中心节点只维护少量元数据,一般不会成为性能瓶颈。

从Amazon、Facebook等公司的实践经验可以得出,Dynamo及其开源实现Cassandra在实践中受到的关注逐渐减少,无中心节点的设计短期之内难以成为主流。另一方面,Dynamo综合使用了各种分布式技术,在实践过程中可以选择性借鉴。

淘宝Tair

Tair是淘宝开发的一个分布式键/值存储引擎。Tair分为持久化和非持久化两种使用方式:非持久化的Tair可以看成是一个分布式缓存,持久化的Tair将数据存放于磁盘中。为了解决磁盘损坏导致数据丢失,Tair可以配置数据的备份数目,Tair自动将一份数据的不同备份放到不同的节点上,当有节点发生异常,无法正常提供服务的时候,其余的节点会继续提供服务。

系统架构

Tair作为一个分布式系统,是由一个中心控制节点和若干个服务节点组成。其中,中心控制节点称为Config Server,服务节点称为Data Server。Config Server负责管理所有的Data Server,维护其状态信息;Data Server对外提供各种数据服务,并以心跳的形式将自身状况汇报给Config Server。Config Server是控制点,而且是单点,目前采用一主一备的形式来保证可靠性,所有的Data Server地位都是等价的。

图5-5是Tair的系统架构图。客户端首先请求Config Server获取数据所在的Data Server,接着往Data Server发送读写请求。Tair允许将数据存放到多台Data Server,以实现异常容错。


关键问题

(1)数据分布

根据数据的主键计算哈希值后,分布到Q个桶中,桶是负载均衡和数据迁移的基本单位。Config Server按照一定的策略把每个桶指派到不同的Data Server上。因为数据按照主键计算哈希值,所以可以认为每个桶中的数据基本是平衡的,只要保证桶分布的均衡性,就能够保证数据分布的均衡性。根据Dynamo论文中的实验结论,Q取值需要远大于集群的物理机器数,例如Q取值10240。

2)容错

当某台Data Server故障不可用时,Config Server能够检测到。每个哈希桶在Tair中存储多个副本,如果是备副本,那么Config Server会重新为其指定一台Data Server,如果是持久化存储,还将复制数据到新的Data Server上。如果是主副本,那么ConfigServer首先将某个正常的备副本提升为主副本,对外提供服务。接着,再选择另外一台Data Server增加一个备副本,确保数据的备份数。

(3)数据迁移

机器加入或者负载不均衡可能导致桶迁移,迁移的过程中需要保证对外服务。当迁移发生时,假设Data Server A要把桶3、4、5迁移到Data Server B。迁移完成前,客户端的路由表没有变化,客户端对3、4、5的访问请求都会路由到A。现在假设3还没开始迁移,4正在迁移中,5已经迁移完成。那么如果对3访问,A直接服务;如果对5访问,A会把请求转发给B,并且将B的返回结果返回给用户;如果对4访问,由A处理,同时如果是对4的修改操作,会记录修改日志,等到桶4迁移完成时,还要把修改日志发送到B,在B上应用这些修改操作,直到A和B之间数据完全一致迁移才真正完成。

(4)Config Server

客户端缓存路由表,大多数情况下,客户端不需要访问Config Server,Config Server宕机也不影响客户端正常访问。每次路由的变更,Config Server都会将新的配置信息推给Data Server。在客户端访问Data Server的时候,会发送客户端缓存的路由表的版本号。如果Data Server发现客户端的版本号过旧,则会通知客户端去Config Server获取一份新的路由表。如果客户端访问某台Data Server发生了不可达的情况(该Data Server可能宕机了),客户端会主动去Config Server获取新的路由表。

5)Data Server

Data Server负责数据的存储,并根据Config Server的要求完成数据的复制和迁移工作。Data Server具备抽象的存储引擎层,可以很方便地添加新存储引擎。Data Server还有一个插件容器,可以动态加载/卸载插件,如图5-6所示。


Tair存储引擎有一个抽象层,只要满足存储引擎需要的接口,就可以很方便地替换Tair底层的存储引擎。Tair默认包含两个存储引擎:Mdb和Fdb,此外,还支持Berkerly DB、Tokyo Cabinet、InnoDB、Leveldb等各种存储引擎。

讨论

Amazon Dynamo采用P2P架构,而在Tair中引入了中心节点Config Server。这种方式很容易处理数据的一致性,不再需要向量时钟、数据回传、Merkle树、冲突处理等复杂的P2P技术。另外,中心节点的负载很低。笔者认为,分布式键值系统的整体架构应该参考Tair,而不是Dynamo。

当然,Tair最主要的用途在于分布式缓存,持久化存储起步比较晚,在实现细节上也有一些不尽如人意的地方。例如,Tair持久化存储通过复制技术来提高可靠性,然而,这种复制是异步的。因此,当有Data Server发生故障时,客户有可能在一定时间内读不到最新的数据,甚至发生最新修改的数据丢失的情况。

推荐阅读更多精彩内容