一、概述
1、我们的memcacheclient(这里我看的spymemcache的源代码)。使用了一致性hash算法ketama进行数据存储节点的选择。与常规的hash算法思路不同。仅仅是对我们要存储数据的key进行hash计算,分配到不同节点存储。一致性hash算法是对我们要存储数据的server进行hash计算,进而确认每一个key的存储位置。
2、常规hash算法的应用以及其弊端
最常规的方式莫过于hash取模的方式。比方集群中可用机器适量为N,那么key值为K的的数据请求非常easy的应该路由到hash(K) mod N相应的机器。
的确,这种结构是简单的,也是有用的。可是在一些快速发展的web系统中,这种解决方式仍有些缺陷。随着系统訪问压力的增长,缓存系统不得不通过添加机器节点的方式提高集群的相应速度和数据承载量。添加机器意味着依照hash取模的方式,在添加机器节点的这一时刻,大量的缓存命不中。缓存数据须要又一次建立,甚至是进行总体的缓存数据迁移,瞬间会给DB带来极高的系统负载。设置导致DBserver宕机。
3、设计分布式cache系统时,一致性hash算法能够帮我们解决哪些问题?
分布式缓存设计核心点:在设计分布式cache系统的时候,我们须要让key的分布均衡。而且在添加cache server后,cache的迁移做到最少。
这里提到的一致性hash算法ketama的做法是:选择详细的机器节点不在仅仅依赖须要缓存数据的key的hash本身了,而是机器节点本身也进行了hash运算。
二、一致性哈希算法情景描写叙述(转载)
1、 hash机器节点
首先求出机器节点的hash值(怎么算机器节点的hash?ip能够作为hash的參数吧。
。当然还有其它的方法了),然后将其分布到0~2^32的一个圆环上(顺时针分布)。例如以下图所看到的:
图一
集群中有机器:A , B, C, D, E五台机器,通过一定的hash算法我们将其分布到如上图所看到的的环上。
2、訪问方式
假设有一个写入缓存的请求。当中Key值为K。计算器hash值Hash(K), Hash(K) 相应于图 – 1环中的某一个点,假设该点相应没有映射到详细的某一个机器节点。那么顺时针查找。直到第一次找到有映射机器的节点。该节点就是确定的目标节点,假设超过了2^32仍然找不到节点,则命中第一个机器节点。比方 Hash(K) 的值介于A~B之间,那么命中的机器节点应该是B节点(如上图 )。
3、添加节点的处理
如上图 – 1,在原有集群的基础上欲添加一台机器F。添加步骤例如以下:
计算机器节点的Hash值,将机器映射到环中的一个节点。例如以下图:
图二
添加机器节点F之后,訪问策略不改变,依旧依照(2)中的方式訪问。此时缓存命不中的情况依旧不可避免,不能命中的数据是hash(K)在添加节点曾经落在C~F之间的数据。虽然依旧存在节点添加带来的命中问题,可是比較传统的 hash取模的方式。一致性hash已经将不命中的数据降到了最低。
Consistent Hashing最大限度地抑制了hash键的又一次分布。另外要取得比較好的负载均衡的效果,往往在server数量比較少的时候须要添加虚拟节点来保证server能均匀的分布在圆环上。
由于使用一般的hash方法,server的映射地点的分布很不均匀。使用虚拟节点的思想。为每一个物理节点(server)在圆上分配100~200个点。
这样就能抑制分布不均匀,最大限度地减小server增减时的缓存又一次分布。
用户数据映射在虚拟节点上,就表示用户数据真正存储位置是在该虚拟节点代表的实际物理server上。
以下有一个图描写叙述了须要为每台物理server添加的虚拟节点。
图三
x轴表示的是须要为每台物理server扩展的虚拟节点倍数(scale)。y轴是实际物理server数。能够看出,当物理server的数量非常小时,须要更大的虚拟节点,反之则须要更少的节点,从图上能够看出,在物理server有10台时。差点儿相同须要为每台server添加100~200个虚拟节点才干达到真正的负载均衡。
三、以spymemcache源代码来演示虚拟节点应用
1、上边描写叙述的一致性Hash算法有个潜在的问题是:
(1)、将节点hash后会不均匀地分布在环上,这样大量key在寻找节点时,会存在key命中各个节点的概率区别较大,无法实现有效的负载均衡。
(2)、如有三个节点Node1,Node2,Node3,分布在环上时三个节点挨的非常近,落在环上的key寻找节点时,大量key顺时针总是分配给Node2,而其他两个节点被找到的概率都会非常小。
2、这样的问题的解决方式能够有:
改善Hash算法,均匀分配各节点到环上;[引文]使用虚拟节点的思想,为每一个物理节点(server)在圆上分配100~200个点。
这样就能抑制分布不均匀,最大限度地减小server增减时的缓存又一次分布。用户数据映射在虚拟节点上。就表示用户数据真正存储位置是在该虚拟节点代表的实际物理server上。
在查看Spy Memcached client时。发现它採用一种称为Ketama的Hash算法。以虚拟节点的思想。解决Memcached的分布式问题。
3、源代码说明
该client採用TreeMap存储全部节点,模拟一个环形的逻辑关系。
在这个环中,节点之前是存在顺序关系的。所以TreeMap的key必须实现Comparator接口。
那节点是如何放入这个环中的呢?
[html] view plaincopyprint?
- protected void setKetamaNodes(List<MemcachedNode> nodes) {
- TreeMap<Long, MemcachedNode> newNodeMap = new TreeMap<Long, MemcachedNode>();
- int numReps= config.getNodeRepetitions();
- for(MemcachedNode node : nodes) {
- // Ketama does some special work with md5 where it reuses chunks.
- if(hashAlg == HashAlgorithm.KETAMA_HASH) {
- for(int i=0; i<numReps / 4; i++) {
- byte[] digest=HashAlgorithm.computeMd5(config.getKeyForNode(node, i));
- for(int h=0;h<4;h++) {
- Long k = ((long)(digest[3+h*4]&0xFF) << 24)
- | ((long)(digest[2+h*4]&0xFF) << 16)
- | ((long)(digest[1+h*4]&0xFF) << 8)
- | (digest[h*4]&0xFF);
- newNodeMap.put(k, node);
- getLogger().debug("Adding node %s in position %d", node, k);
- }
- }
- } else {
- for(int i=0; i<numReps; i++) {
- newNodeMap.put(hashAlg.hash(config.getKeyForNode(node, i)), node);
- }
- }
- }
- assert newNodeMap.size() == numReps * nodes.size();
- ketamaNodes = newNodeMap;
protected void setKetamaNodes(List<MemcachedNode> nodes) { TreeMap<Long, MemcachedNode> newNodeMap = new TreeMap<Long, MemcachedNode>(); int numReps= config.getNodeRepetitions(); for(MemcachedNode node : nodes) { // Ketama does some special work with md5 where it reuses chunks. if(hashAlg == HashAlgorithm.KETAMA_HASH) { for(int i=0; i<numReps / 4; i++) { byte[] digest=HashAlgorithm.computeMd5(config.getKeyForNode(node, i)); for(int h=0;h<4;h++) { Long k = ((long)(digest[3+h*4]&0xFF) << 24) | ((long)(digest[2+h*4]&0xFF) << 16) | ((long)(digest[1+h*4]&0xFF) << 8) | (digest[h*4]&0xFF); newNodeMap.put(k, node); getLogger().debug("Adding node %s in position %d", node, k); } } } else { for(int i=0; i<numReps; i++) { newNodeMap.put(hashAlg.hash(config.getKeyForNode(node, i)), node); } } } assert newNodeMap.size() == numReps * nodes.size(); ketamaNodes = newNodeMap;
上面的流程大概能够这样归纳:四个虚拟结点为一组。以getKeyForNode方法得到这组虚拟节点的name,Md5编码后。每一个虚拟结点相应Md5码16个字节中的4个,组成一个long型数值。做为这个虚拟结点在环中的惟一key。第10行k为什么是Long型的呢?就是由于Long型实现了Comparator接口。
处理完正式结点在环上的分布后,能够開始key在环上寻找节点的游戏了。
对于每一个key还是得完毕上面的步骤:计算出Md5,依据Md5的字节数组,通过Kemata Hash算法得到key在这个环中的位置。
[html] view plaincopyprint?
- MemcachedNode getNodeForKey(long hash) {
- final MemcachedNode rv;
- if(!ketamaNodes.containsKey(hash)) {
- // Java 1.6 adds a ceilingKey method, but I‘m still stuck in 1.5
- // in a lot of places, so I‘m doing this myself.
- SortedMap<Long, MemcachedNode> tailMap=getKetamaNodes().tailMap(hash);
- if(tailMap.isEmpty()) {
- hash=getKetamaNodes().firstKey();
- } else {
- hash=tailMap.firstKey();
- }
- }
- rv=getKetamaNodes().get(hash);
- return rv;
- }
MemcachedNode getNodeForKey(long hash) { final MemcachedNode rv; if(!ketamaNodes.containsKey(hash)) { // Java 1.6 adds a ceilingKey method, but I‘m still stuck in 1.5 // in a lot of places, so I‘m doing this myself. SortedMap<Long, MemcachedNode> tailMap=getKetamaNodes().tailMap(hash); if(tailMap.isEmpty()) { hash=getKetamaNodes().firstKey(); } else { hash=tailMap.firstKey(); } } rv=getKetamaNodes().get(hash); return rv; }
上边代码的实现就是在环上顺时针查找,没找到就去的第一个,然后就知道相应的物理节点了。
四、应用场景分析
1、memcache的add方法:通过一致性hash算法确认当前client相应的cacheserver的hash值以及要存储数据key的hash进行相应,确认cacheserver。获取connection进行数据存储
2、memcache的get方法:通过一致性hash算法确认当前client相应的cacheserver的hash值以及要提取数据的hash值,进而确认存储的cacheserver,获取connection进行数据提取
五、总结
1、一致性hash算法仅仅是帮我们降低cache集群中的机器数量增减的时候,cache的数据能进行最少重建。仅仅要cache集群的server数量有变化。必定产生数据命中的问题
2、对于数据的分布均衡问题。通过虚拟节点的思想来达到均衡分配。当然,我们cache server节点越少就越须要虚拟节点这个方式来均衡负载。
3、我们的cacheclient根本不会维护一个map来记录每一个key存储在哪里。都是通过key的hash和cacheserver(或许ip能够作为參数)的hash计算当前的key应该存储在哪个节点上。
4、当我们的cache节点崩溃了。我们必然丢失部分cache数据。而且要依据活着的cache server和key进行新的一致性匹配计算。
有可能对部分没有丢失的数据也要做重建...
5、至于正常到达数据存储节点,怎样找到key相应的数据,那就是cache server本身的内部算法实现了。此处不做描写叙述。
这里仅仅是针对数据的存储方式以及提取方式进行了流程展示。
转载:http://blog.csdn.net/kongqz/article/details/6695417