最终一致数据类型Eventually Consistent Data Types
在上一节我们假定两个节点总是合并他们的数据版本。但要解决更新冲突并不容易,让所有副本都最终达到一个语义上正确的值出乎意料的难。一个众所周知的例子是Amazon Dynamo数据库中已经删除的条目可以重现。
我们假设一个例子来说明这个问题:数据库维护一个逻辑上的全局计数器,每个节点可以增加或者减少计数。虽然每个节点可以在本地维护一个自己的值,但这些本地计数却不能通过简单的加减来合并。假设这样一个例子:有三个节点A、B和C,每个节点执行了一次加操作。如果A从B获得一个值,并且加到本地副本上,然后C从B获得值,然后C再从A获得值,那么C最后的值是4,而这是错误的。解决这个问题的方法是用一个类似于向量时钟的数据结构为每个节点维护一对计数器:
1 class Counter { 2 int[] plus 3 int[] minus 4 int NODE_ID 5 6 increment() { 7 plus[NODE_ID]++ 8 } 9 10 decrement() { 11 minus[NODE_ID]++ 12 } 13 14 get() { 15 return sum(plus) – sum(minus) 16 } 17 18 merge(Counter other) { 19 for i in 1..MAX_ID { 20 plus[i] = max(plus[i], other.plus[i]) 21 minus[i] = max(minus[i], other.minus[i]) 22 } 23 } 24 }Cassandra用类似的方法计数。利用基于状态的或是基于操作的复制理论也可以设计出更复杂的最终一致的数据结构。例如,中就提及了一系列这样的数据结构,包括:
计数器(加减操作)
集合(添加和移除操作)
图(增加边或顶点,移除边或顶点)
列表(插入某位置或者移除某位置)
最终一致数据类型的功能通常是有限的,还会带来额外的性能开销。
数据放置这部分主要关注控制在分布式数据库中放置数据的算法。这些算法负责把数据项映射到合适的物理节点上,在节点间迁移数据以及像内存这样的资源的全局调配。
均衡数据我们还是从一个简单的协议开始,它可以提供集群节点间无缝的数据迁移。这常发生于像集群扩容(加入新节点),故障转移(一些节点宕机)或是均衡数据(数据在节点间的分布不均衡)这样的场景。如下图A中所描绘的场景 – 有三个节点,数据随便分布在三个节点上(假设数据都是key-value型)。
如果数据库不支持数据内部均衡,就要在每个节点上发布数据库实例,如上面图B所示。这需要手动进行集群扩展,停掉要迁移的数据库实例,把它转移到新节点上,再在新节点上启动,如图C所示。尽管数据库能够监控到每一条记录,包括MongoDB, Oracle Coherence, 和还在开发中的 Redis Cluster 在内的许多系统仍然使用的是自动均衡技术。也即,将数据分片并把每个数据分片作为迁移的最小单位,这是基于效率的考虑。很明显分片数会比节点数多,数据分片可以在各节点间平均分布。按照一种简单的协议即可实现无缝数据迁移,这个协议可以在迁移数据分片的时候重定向客户的数据迁出节点和迁入节点。下图描绘了一个Redis Cluster中实现的get(key)逻辑的状态机。
假定每个节点都知道集群拓扑,能够把任意key映射到相应的数据分片,把数据分片映射到节点。如果节点判断被请求的key属于本地分片,就会在本地查找(上图中上面的方框)。假如节点判断请求的key属于另一个节点X,他会发送一个永久重定向命令给客户端(上图中下方的方框)。永久重定向意味着客户端可以缓存分片和节点间的映射关系。如果分片迁移正在进行,迁出节点和迁入节点会标记相应的分片并且将分片的数据加锁逐条加锁然后开始移动。迁出节点首先会在本地查找key,如果没有找到,重定向客户端到迁入节点,假如key已经迁移完毕的话。这种重定向是一次性的,并且不能被缓存。迁入节点在本地处理重定向,但定期查询在迁移还没完成前被永久重定向。
动态环境中的数据分片和复制我们关注的另一个问题是怎么把记录映射到物理节点。比较直接的方法是用一张表来记录每个范围的key与节点的映射关系,一个范围的key对应到一个节点,或者用key的hash值与节点数取模得到的值作为节点ID。但是hash取模的方法在集群发生更改的情况下就不是很好用,因为增加或者减少节点都会引起集群内的数据彻底重排。导致很难进行复制和故障恢复。
有许多方法在复制和故障恢复的角度进行了增强。最著名的就是一致性hash。网上已经有很多关于一致性hash的介绍了,所以在这里我只提供一个基本介绍,仅仅为了文章内容的完整性。下图描绘了一致性hash的基本原理:
一致性hash从根本上来讲是一个键值映射结构 – 它把键(通常是hash过的)映射到物理节点。键经过hash之后的取值空间是一个有序的定长二进制字符串,很显然每个在此范围内的键都会被映射到图A中A、B、C三个节点中的某一个。为了副本复制,将取值空间闭合成一个环,沿环顺时针前行直到所有副本都被映射到合适的节点上,如图B所示。换句话说,Y将被定位在节点B上,因为它在B的范围内,第一个副本应该放置在C,第二个副本放置在A,以此类推。
这种结构的好处体现在增加或减少一个节点的时候,因为它只会引起临接区域的数据重新均衡。如图C所示,节点D的加入只会对数据项X产生影响而对Y无影响。同样,移除节点B(或者B失效)只会影响Y和X的副本,而不会对X自身造成影响。但是,这种做法在带来好处的同时也有弱点,那就是重新均衡的负担都由邻节点承受了,它们将移动大量的数据。通过将每个节点映射到多个范围而不是一个范围可以一定程度上减轻这个问题带来的不利影响,如图D所示。这是一个折中,它避免了重新均衡数据时负载过于集中,但是与基于模块的映射相比,保持了总均衡数量适当降低。
给大规模的集群维护一个完整连贯的hash环很不容易。对于相对小一点的数据库集群就不会有问题,研究如何在对等网络中将数据放置与网络路由结合起来很有意思。一个比较好的例子是Chord算法,它使环的完整性让步于单个节点的查找效率。Chord算法也使用了环映射键到节点的理念,在这方面和一致性hash很相似。不同的是,一个特定节点维护一个短列表,列表中的节点在环上的逻辑位置是指数增长的(如下图)。这使得可以使用二分搜索只需要几次网络跳跃就可以定位一个键。
这张图画的是一个由16个节点组成的集群,描绘了节点A是如何查找放在节点D上的key的。 (A) 描绘了路由,(B) 描绘了环针对节点A、B、C的局部图像。在参考资料中有更多关于分散式系统中的数据复制的内容。