分布式策略
准备知识
- Merkle tree[1]
上图(来自Wikipedia[1])给出了一个二进制的哈希树(二叉哈希树, 较常用的tiger hash tree也是这个形式). 据称哈希树经常应用在一些分布式系统或者分布式存储中的反熵机制(Anti-entropy),也有称做去熵的.这些应用包括 Amazon的Dynamo 还有Apache的Cassandra数据库, 通过去熵可以去做到各个不同节点的同步, 即保持各个节点的信息都是同步最新.
哈希树的特点很鲜明: 叶子节点存储的是数据文件,而非叶子节点存储的是其子节点的哈希值(称为MessageDigest) 这些非叶子节点的Hash被称作路径哈希值, 叶子节点的Hash值是真实数据的Hash值. 因为使用 了树形结构, MT的时间复杂度为 O(logn)
比如下图中, 我们如果使用SHA1算法来做校验值, 比如数据块8对应的哈希值是H23, 则按照这个路径来看应该有
H11=SHA1(H23∥H24)
H5=SHA1(H11∥H12)
H2=SHA1(H5∥H6)
H0=SHA1(H1∥H2)
其中∥是表联接的意思.
应用举例
BitTorrent中应用
在BT中, 通常种子文件中包含的信息是Root值, 此外还有文件长度、数据块长等重要信息. 当客户端下载数据块8时,在下载前,它将要求peer提供校验块8所需的全部路径哈希值:H24、H12、H6和H1. 下载完成后, 客户端就会开始校验, 它先计算它已经下载的数据块8的Hash值23, 记做H23′, 表示尚未验证. 随后会按照我在上一小节中给出的几个公式, 来依次求解直到得到H0′并与H0做比较, 校验通过则下载无误. 校验通过的这些路径哈希值会被缓存下来, 当一定数量的路径哈希值被缓存之后,后继数据块的校验过程将被极大简化。此时我们可以直接利用校验路径上层次最低的已知路径哈希值来对数据块进行部分校验,而无需每次都校验至根哈希值H0.
- Quorum NRW
• N: 复制的节点数量
• R: 成功读操作的最小节点数
• W: 成功写操作的最小节点数
只需W + R > N,就可以保证强一致性。
第一个关键参数是 N,这个 N 指的是数据对象将被复制到 N 台主机上,N 在实例级别配置,协调器将
负责把数据复制到 N-1 个节点上。N 的典型值设置为 3.
在分布式系统中,一般都要有容错性,因此一般N都是大于3的,此时根据CAP理论,一致性,可用性
和分区容错 性最多只能满足两个,那么我们就需要在一致性和分区容错性之间做一平衡,如果要高的
一致性,那么就配置N=W,R=1,这个时候可用性就会大大降低。如果 想要高的可用性,那么此时就
需要放松一致性的要求,此时可以配置W=1,这样使得写操作延迟最低,同时通过异步的机制更新剩
余的N-W个节点。
当存储系统保证最终一致性时,存储系统的配置一般是W+R<=N,此时读取和写入操作是不重叠的,
不一致性的窗口就依赖于存储系统的异步实现方式,不一致性的窗口大小也就等于从更新开始到所有的
节点都异步更新完成之间的时间。
几种特殊情况:
W = 1, R = N,对写操作要求高性能高可用。
R = 1, W = N , 对读操作要求高性能高可用,比如类似cache之类业务。
W = Q, R = Q where Q = N / 2 + 1 一般应用适用,读写性能之间取得平衡。如N=3,W=2,R=2
- Vector clock
vector clock算法。可以把这个vector clock想象成每个节点都记录自己的版本信息,而一个数据,包
含所有这些版本信息。来看一个例子:假设一个写请求,第一次被节点A处理了。节点A会增加一个版
本信息(A,1)。我们把这个时候的数据记做D1(A,1)。 然后另外一个对同样key(这一段讨论都是针
对同样的key的)的请求还是被A处理了于是有D2(A,2)。
这个时候,D2是可以覆盖D1的,不会有冲突产生。现在我们假设D2传播到了所有节点(B和C),B和C
收到的数据不是从客户产生的,而是别人复制给他们的,所以他们不产生新的版本信息,所以现在B和
C都持有数据D2(A,2)。好,继续,又一个请求,被B处理了,生成数据D3(A,2;B,1),因为这是
一个新版本的数据,被B处理,所以要增加B的版本信息。
假设D3没有传播到C的时候又一个请求被C处理记做D4(A,2;C,1)。假设在这些版本没有传播开来
以前,有一个读取操作,我们要记得,我们的W=1 那么R=N=3,所以R会从所有三个节点上读,在
这个例子中将读到三个版本。A上的D2(A,2);B上的D3(A,2;B,1);C上的D4(A,2;C,1)这个时
候可以判断出,D2已经是旧版本,可以舍弃,但是D3和D4都是新版本,需要应用自己去合并。
如果需要高可写性,就要处理这种合并问题。好假设应用完成了冲入解决,这里就是合并D3和D4版
本,然后重新做了写入,假设是B处理这个请求,于是有D5(A,2;B,2;C,1);这个版本将可以覆盖
掉D1-D4那四个版本。这个例子只举了一个客户的请求在被不同节点处理时候的情况, 而且每次写更
新都是可接受的,大家可以自己更深入的演算一下几个并发客户的情况,以及用一个旧版本做更新的情
况。
上面问题看似好像可以通过在三个节点里选择一个主节点来解决,所有的读取和写入都从主节点来进
行。但是这样就违背了W=1这个约定,实际上还是退化到W=N的情况了。所以如果系统不需要很大的
弹性,W=N为所有应用都接受,那么系统的设计上可以得到很大的简化。Dynamo 为了给出充分的弹
性而被设计成完全的对等集群(peer to peer),网络中的任何一个节点都不是特殊的。
进入正题——分布式策略
- 一致性哈希
- 从单节点的服务扩充到分布式(可能为了提高性能、或者解决单机存储瓶颈),首先能考虑到的是进行hash() mod n。此方法优点是简单易实现;缺点是当单点发生故障,系统无法自动恢复,另一个缺点是一旦对系统进行扩容操作,需要调整全部n台机器。
- 将 hash() mod n 调整为hash() mod (n/2),这样相对第一种方案,优点就是当单点出现故障时,系统可以暂时满足服务请求,只是另外一台的负载会比较重。系统扩容导致的n台机器调整依旧无法改变。
- 一致性哈希
- 我们把每台server分成v个虚拟节点,再把所有虚拟节点(n*v)随机分配到一致性哈希的圆环上,这样
- 所有的用户从自己圆环上的位置顺时针往下取到第一个vnode就是自己所属节点。当此节点存在故障
- 时,再顺时针取下一个作为替代节点(这对于缓存数据库Memcached是很有效的,但是对于持久化的数据库在单节点故障时,依旧需要有双备份,或者更高的备份才行)。而且它一定程度上解决了系统扩容问题,当有新节点加入时,只需要调整一台机器即可。
- gossip 协议[2]
Gossip算法又被称为反熵(Anti-Entropy),在一个有界网络中,每个节点都随机地与其他节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致。
即使有的节点因宕机而重启,有新节点加入,但是经过一段时间后,这些节点的状态也会与其他节点达成一致。也就是说,gossip天然具有分布式容错的优点。
但gossip的缺点也很明显,冗余通信会对网络带宽、cpu资源造成很大的负载。
gossip分为两种.
- anti-entropy 只要数据不同步,就开始同步数据
- rumor mongering 每隔固定的时间同步数据
下面讨论anti-entropy
见公式(1). 此公式表示在节点p上,q节点的属性k的值是v,其版本号是n。
为了保证一致性,规定数据的value及version只有宿主节点才能修改,其他节点只能间接通过Gossip协议来请求数据对应的宿主节点修改,即m (p)只能由有节点p来修改。
anti-entropy协议通过版本号大小来对数据进行更新。
两个节点(A、B)之间存在三种通信方式:
- push-gossip: A节点将数据推送给B节点,B节点更新A中比自己新的数据
- pull-gossip:A仅将摘要数据 (node,key,value,version)推送给B,B根据摘要数据来选择那些版本号比A高的数据推送给A,A更新本地。
- push-pull gossip:与pull类似,只是多了一步,A再将本地比B新的数据推送给B,B更新本地。
如果把两个节点数据同步一次定义为一个周期,则在一个周期内,push需通信1次,pull需2次,push/pull则需3次。从效果上来讲,push/pull最好,理论上一个周期内可以使两个节点完全一致。直观上也感觉,push/pull的收敛速度是最快的
(cassandra 之 gossip实现,可参考)
参考文献
- 《Merkle Hash Tree》 http://yishanhe.net/merkle-hash-tree/
- 《gossip 协议》http://blog.csdn.net/liaoxiangui/article/details/6854549
- 颜开 《NoSQL 数据库笔谈》