标签:目标 val hang 中间件 完全 项目 阿里 用户名 日期
MyCat是基于阿里开源的Cobar产品而研发,Cobar的稳定性、可靠性、优秀的架构和性能以及众多成熟的使用案例使得MYCAT一开始就拥有一个很好的起点,站在巨人的肩膀上,我们能看到更远。业界优秀的开源项目和创新思路被广泛融入到MYCAT的基因中,使得MYCAT在很多方面都领先于目前其他一些同类的开源项目,甚至超越某些商业产品。
MYCAT背后有一支强大的技术团队,其参与者都是5年以上资深软件工程师、架构师、DBA等,优秀的技术团队保证了MYCAT的产品质量。myCAT开源项目维护很频繁,目前最新版已经到了1.5 Release(23天前刚维护过)。因此它的维护性、稳定性是得到了保证的。同时myCat的文档极其丰富,参于开发的人员又很多,所以它可以应付很多在以往的Amoeba以及Corba上未能解决的问题,它是一个可以真正被应用在生产环境上的数据库中间件。在下面的章节我们将使用myCat来实现mySQL的读写分离和垂直水平折分的具体案例。
什么是MyCAT?简单的说,MyCAT就是:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mycat:server SYSTEM "server.dtd"> <mycat:server xmlns:mycat="http://org.opencloudb/"> <system> <property name="defaultSqlParser">druidparser</property> </system> <user name="mk"> <property name="password">aaaaaa</property> <property name="schemas">mycat</property> <property name="readOnly">false</property> </user> </mycat:server>
<?xml version="1.0"?> <!DOCTYPE mycat:schema SYSTEM "schema.dtd"> <mycat:schema xmlns:mycat="http://org.opencloudb/" > <schema name="mycat" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn1"/> <dataNode name="dn1" dataHost="virtualHost" database="mk" /> <dataHost name="virtualHost" maxCon="50" minCon="5" balance="3" writeType="0" dbType="mysql" dbDriver="native" switchType="1" > <heartbeat>select 1</heartbeat> <writeHost host="m1" url="192.168.0.101:3306" user="mk" password="aaaaaa"> <readHost host="s1" url="192.168.0.102:3306" user="mk" password="aaaaaa" /> <readHost host="s2" url="192.168.0.103:3306" user="mk" password="aaaaaa" /> </writeHost> </dataHost> </mycat:schema>
<writeHost host="m1" url="192.168.0.101:3306" user="mk" password="aaaaaa"> <readHost host="s1" url="192.168.0.102:3306" user="mk" password="aaaaaa" /> <readHost host="s2" url="192.168.0.103:3306" user="mk" password="aaaaaa" /> </writeHost>
./mycat start
insert into user_info(user_name)values(@@hostname); insert into user_info(user_name)values(@@hostname); insert into user_info(user_name)values(@@hostname); insert into user_info(user_name)values(@@hostname); commit;
<writeHost host="m1" url="192.168.0.101:3306" user="mk" password="aaaaaa"> <readHost host="s1" url="192.168.0.102:3306" user="mk" password="aaaaaa" /> <readHost host="s2" url="192.168.0.103:3306" user="mk" password="aaaaaa" /> </writeHost>
<writeHost host="m1" url="192.168.0.101:3306" user="mk" password="aaaaaa"> <readHost host="s1" url="192.168.0.102:3306" user="mk" password="aaaaaa" /> <readHost host="s2" url="192.168.0.103:3306" user="mk" password="aaaaaa" /> </writeHost>
<writeHost host="m1" url="192.168.0.101:3306" user="root" password="aaaaaa"> </writeHost> <writeHost host="m2" url="192.168.0.104:3306" user="root" password="aaaaaa"/>
<writeHost host="m1" url="192.168.0.101:3306" user="root" password="aaaaaa"> </writeHost> <writeHost host="m2" url="192.168.0.104:3306" user="root" password="aaaaaa"/>
<dataHost name="virtualHost" maxCon="50" minCon="5" balance="3" writeType="0" dbType="mysql" dbDriver="native" switchType="2" slaveThreshold="100"> <heartbeat>show slave status</heartbeat> <writeHost host="m1" url="192.168.0.101:3306" user="root" password="aaaaaa"> <readHost host="s1" url="192.168.0.102:3306" user="root" password="aaaaaa"/> <readHost host="s2" url="192.168.0.103:3306" user="root" password="aaaaaa"/> </writeHost> <writeHost host="m2" url="192.168.0.104:3306" user="root" password="aaaaaa"/> </dataHost>
一般来说,我们数据库中的所有表很难通过某一个(或少数几个)字段全部关联起来,所以很难简单的仅仅通过数据的水平切分来解决所有问题。而垂直切分也只能解决部分问题,对于那些负载非常高的系统,即使仅仅只是单个表都无法通过单台数据库主机来承担其负载。我们必须结合“垂直”和“水平”两种切分方式同时使用
每一个应用系统的负载都是一步一步增长上来的,在开始遇到性能瓶颈的时候,大多数架构师和DBA都会选择先进行数据的垂直拆分,因为这样的成本最先,最符合这个时期所追求的最大投入产出比。然而,随着业务的不断扩张,系统负载的持续增长,在系统稳定一段时期之后,经过了垂直拆分之后的数据库集群可能又再一次不堪重负,遇到了性能瓶颈。
如果我们再一次像最开始那样继续细分模块,进行数据的垂直切分,那我们可能在不久的将来,又会遇到现在所面对的同样的问题。而且随着模块的不断的细化,应用系统的架构也会越来越复杂,整个系统很可能会出现失控的局面。
这时候我们就必须要通过数据的水平切分的优势,来解决这里所遇到的问题。而且,我们完全不必要在使用数据水平切分的时候,推倒之前进行数据垂直切分的成果,而是在其基础上利用水平切分的优势来避开垂直切分的弊端,解决系统复杂性不断扩大的问题。而水平拆分的弊端(规则难以统一)也已经被之前的垂直切分解决掉了,让水平拆分可以进行的得心应手。
在数据切分处理中,特别是水平切分中,中间件最终要的两个处理过程就是数据的切分、数据的聚合。
选择合适的切分规则,至关重要,因为它决定了后续数据聚合的难易程度,甚至可以避免跨库的数据聚合
处理。
myCAT数据分片拆分原则:
myCAT对于后端多个mySQL的数据折分和折分后的聚合,对于myCAT前端调用来説是透明的。
在分片前,我们需要事先了解一个重要的知识点,也算是一个需要引起重视的问题。
A和B互为主从,我们按照userID来实施分片,userID为1,3,5的插入到一台mysql实例,userID为2,4,6的插入到另一个mysql实例中去。然后在mycat前端select时mycat会自动进行聚合,即它会从2个mysql实例中选择数据并合并成1,2,3,4,5,6…这样的数据显示给调用的客户端。
1. 先往A上插入 一条数据(id:1 name: tom, gender: m),于是B上也马上会REPLI(同步)一条数据,因此你在A和B上做SELECT查询,都可以看到两边记录一样
2. 往B上插入 一条数据(id:2 name: tom, gender: m),于是A上也马上会REPLI一条数据,因此你在A和B上做SELECT查询,都可以看到两边记录一样
3. 使用MYCAT读写分离,A为写,B为读,一切OK
4. 使用MYCAT做水平拆分,拆分规则为(id % 2 case 0 then Server A; case 1 then Server B;),于是,规则开始起效。
Server A | Server B |
1 | 1 |
2 | 2 |
3 | 3 |
我们拿192.168.0.101与192.168.0.102两台机器来做分片实验
在192.168.0.101上禁用binlog,关闭master slaver的关联,同时断开与原有192.168.0.104的master slaver关联。
别忘了把“log-slave-update”也注释掉。
重启后登录192.168.0.101,输入以下命令
stop slave; change master to master_host=‘ ‘;
别忘了把“log-slave-update”也注释掉。
重启后登录192.168.0.104,输入以下命令
stop slave; change master to master_host=‘ ‘;
CREATE TABLE `t_person` ( `person_id` int(11) NOT NULL, `person_name` varchar(20) DEFAULT NULL, PRIMARY KEY (`person_id`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1; show slave status;
配置mycat的rule.xml文件
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mycat:rule SYSTEM "rule.dtd"> <mycat:rule xmlns:mycat="http://org.opencloudb/"> <tableRule name="mod-long"> <rule> <columns>person_id</columns> <algorithm>mod-long</algorithm> </rule> </tableRule> <function name="mod-long" class="org.opencloudb.route.function.PartitionByMod"> <!-- how many data nodes --> <property name="count">2</property> </function> </mycat:rule>
通过配置可以看到我们按照person_id会把该表数据分在2个物理库内,分片数据包括通过mycat的mysql client端insert时自动分片以及select时自动聚合。
rule.xml文件定义后还需要应用它里面的规则,才能够最终让数据分片在mycat中起效,为此,我们更改schema.xml文件,増加如下一段配置:
<schema name="split" checkSQLschema="false" sqlMaxLimit="100"> <table name="t_person" dataNode="splitNode1,splitNode2" rule="mod-long"/> </schema> <dataNode name="splitNode1" dataHost="host1" database="mk" /> <dataNode name="splitNode2" dataHost="host2" database="mk" /> <dataHost name="host1" maxCon="20" minCon="5" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1" > <heartbeat>select 1</heartbeat> <writeHost host="m1" url="192.168.0.101:3306" user="mk" password="aaaaaa"/> </dataHost> <dataHost name="host2" maxCon="20" minCon="5" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1" > <heartbeat>select 1</heartbeat> <writeHost host="s1" url="192.168.0.104:3306" user="mk" password="aaaaaa"/> </dataHost>
<user name="split"> <property name="password">aaaaaa</property> <property name="schemas">split</property> <property name="readOnly">false</property> </user>
我们使用split/aaaaaa用户连上mycat实例
我们插入5条数据
insert into t_person(person_id,person_name)values(‘1‘,‘michael‘); insert into t_person(person_id,person_name)values(‘2‘,‘tom‘); insert into t_person(person_id,person_name)values(‘3‘,‘tonny‘); insert into t_person(person_id,person_name)values(‘4‘,‘marry‘); insert into t_person(person_id,person_name)values(‘5‘,‘jack‘); commit;
连入192.168.0.101上进行查看,我们可以得到2条数据,person_id为偶数倍。
连入192.168.0.104上进行查看,我们可以得到3条数据,person_id为奇数倍。
分片成功!!!
myCAT的分片功能相当的强大,完全可以应付亿万级的数据,如果再结合适当的读写分离机制是完全可以让你的网站飞起来的。
myCAT的分片规则还有很多,文后的附件中会给出一个myCAT分片规则大全。
常用的根据主键或非主键的分片规则配置:
通过在配置文件中配置可能的枚举id,自己配置分片,使用规则:‘
<tableRule name="sharding-by-intfile"> <rule> <columns>user_id</columns> <algorithm>hash-int</algorithm> </rule> </tableRule> <function name="hash-int" class="org.opencloudb.route.function.PartitionByFileMap"> <property name="mapFile">partition-hash-int.txt</property> <property name="type">0</property> <property name="defaultNode">0</property> </function> partition-hash-int.txt 配置: 10000=0 10010=1 DEFAULT_NODE=1
/** * defaultNode 默认节点:小于0表示不设置默认节点,大于等于0表示设置默认节点 * 默认节点的作用:枚举分片时,如果碰到不识别的枚举值,就让它路由到默认节点 * 如果不配置默认节点(defaultNode值小于0表示不配置默认节点),碰到 * 不识别的枚举值就会报错, * like this:can‘t find datanode for sharding column:column_name val:ffffffff */
<tableRule name="rule1"> <rule> <columns>user_id</columns> <algorithm>func1</algorithm> </rule> </tableRule> <function name="func1" class="org.opencloudb.route.function.PartitionByLong"> <property name="partitionCount">2,1</property> <property name="partitionLength">256,512</property> </function>
配置说明:
上面columns 标识将要分片的表字段,algorithm 分片函数,
partitionCount 分片个数列表,partitionLength 分片范围列表
分区长度:默认为最大2^n=1024 ,即最大支持1024分区
约束 :
count,length两个数组的长度必须是一致的。
1024 = sum((count[i]*length[i])). count和length两个向量的点积恒等于1024
用法例子:
本例的分区策略:希望将数据水平分成3份,前两份各占25%,第三份占50%。(故本例非均匀分区)
// |<---------------------1024------------------------>| // |<----256--->|<----256--->|<----------512---------->| // | partition0 | partition1 | partition2 | // | 共2份,故count[0]=2 | 共1份,故count[1]=1 | int[] count = new int[] { 2, 1 }; int[] length = new int[] { 256, 512 }; PartitionUtil pu = new PartitionUtil(count, length); // 下面代码演示分别以offerId字段或memberId字段根据上述分区策略拆分的分配结果 int DEFAULT_STR_HEAD_LEN = 8; // cobar默认会配置为此值 long offerId = 12345; String memberId = "qiushuo"; // 若根据offerId分配,partNo1将等于0,即按照上述分区策略,offerId为12345时将会被分配到partition0中 int partNo1 = pu.partition(offerId); // 若根据memberId分配,partNo2将等于2,即按照上述分区策略,memberId为qiushuo时将会被分到partition2中 int partNo2 = pu.partition(memberId, 0, DEFAULT_STR_HEAD_LEN);
<function name="func1" class="org.opencloudb.route.function.PartitionByLong"> <property name="partitionCount">4</property> <property name="partitionLength">256</property> </function>
<tableRule name="auto-sharding-long"> <rule> <columns>user_id</columns> <algorithm>rang-long</algorithm> </rule> </tableRule> <function name="rang-long" class="org.opencloudb.route.function.AutoPartitionByLong"> <property name="mapFile">autopartition-long.txt</property> </function> # range start-end ,data node index # K=1000,M=10000. 0-500M=0 500M-1000M=1 1000M-1500M=2 或 0-10000000=0 10000001-20000000=1
<tableRule name="mod-long"> <rule> <columns>user_id</columns> <algorithm>mod-long</algorithm> </rule> </tableRule> <function name="mod-long" class="org.opencloudb.route.function.PartitionByMod"> <!-- how many data nodes --> <property name="count">3</property> </function>
<tableRule name="sharding-by-date"> <rule> <columns>create_time</columns> <algorithm>sharding-by-date</algorithm> </rule> </tableRule> <function name="sharding-by-date" class="org.opencloudb.route.function.PartitionByDate"> <property name="dateFormat">yyyy-MM-dd</property> <property name="sBeginDate">2014-01-01</property> <property name="sPartionDay">10</property> </function>配置说明:
<tableRule name="sharding-by-pattern"> <rule> <columns>user_id</columns> <algorithm>sharding-by-pattern</algorithm> </rule> </tableRule> <function name="sharding-by-pattern" class="org.opencloudb.route.function.PartitionByPattern"> <property name="patternValue">256</property> <property name="defaultNode">2</property> <property name="mapFile">partition-pattern.txt</property> </function>
String idVal = "0";
Assert.assertEquals(true, 7 == autoPartition.calculate(idVal));
idVal = "45a";
Assert.assertEquals(true, 2 == autoPartition.calculate(idVal));
<tableRule name="sharding-by-prefixpattern"> <rule> <columns>user_id</columns> <algorithm>sharding-by-prefixpattern</algorithm> </rule> </tableRule> <function name="sharding-by-pattern" class="org.opencloudb.route.function.PartitionByPattern"> <property name="patternValue">256</property> <property name="prefixLength">5</property> <property name="mapFile">partition-pattern.txt</property> </function> partition-pattern.txt # range start-end ,data node index # ASCII # 48-57=0-9 # 64、65-90=@、A-Z # 97-122=a-z ###### first host configuration 1-4=0 5-8=1 9-12=2 13-16=3 ###### second host configuration 17-20=4 21-24=5 25-28=6 29-32=7 0-0=7 配置说明: 上面columns 标识将要分片的表字段,algorithm 分片函数,patternValue 即求模基数,prefixLength ASCII 截取的位数 mapFile 配置文件路径 配置文件中,1-32 即代表id%256后分布的范围,如果在1-32则在分区1,其他类推 此种方式类似方式6只不过采取的是将列种获取前prefixLength位列所有ASCII码的和进行求模sum%patternValue ,获取的值,在通配范围内的 即 分片数, /** * ASCII编码: * 48-57=0-9阿拉伯数字 * 64、65-90=@、A-Z * 97-122=a-z * */ 如 String idVal="gf89f9a"; Assert.assertEquals(true, 0==autoPartition.calculate(idVal)); idVal="8df99a"; Assert.assertEquals(true, 4==autoPartition.calculate(idVal)); idVal="8dhdf99a"; Assert.assertEquals(true, 3==autoPartition.calculate(idVal));
<tableRule name="sharding-by-substring"> <rule> <columns>user_id</columns> <algorithm>sharding-by-substring</algorithm> </rule> </tableRule> <function name="sharding-by-substring" class="org.opencloudb.route.function.PartitionDirectBySubString"> <property name="startIndex">0</property> <!-- zero-based --> <property name="size">2</property> <property name="partitionCount">8</property> <property name="defaultPartition">0</property> </function> 配置说明: 上面columns 标识将要分片的表字段,algorithm 分片函数 此方法为直接根据字符子串(必须是数字)计算分区号(由应用传递参数,显式指定分区号)。 例如id=05-100000002 在此配置中代表根据id中从startIndex=0,开始,截取siz=2位数字即05,05就是获取的分区,如果没传默认分配到defaultPartition
<tableRule name="sharding-by-stringhash"> <rule> <columns>user_id</columns> <algorithm>sharding-by-stringhash</algorithm> </rule> </tableRule> <function name="sharding-by-substring" class="org.opencloudb.route.function.PartitionDirectBySubString"> <property name=length>512</property> <!-- zero-based --> <property name="count">2</property> <property name="hashSlice">0:2</property> </function> 配置说明: 上面columns 标识将要分片的表字段,algorithm 分片函数 函数中length代表字符串hash求模基数,count分区数,hashSlice hash预算位 即根据子字符串 hash运算 hashSlice : 0 means str.length(), -1 means str.length()-1 /** * "2" -> (0,2)<br/> * "1:2" -> (1,2)<br/> * "1:" -> (1,0)<br/> * "-1:" -> (-1,0)<br/> * ":-1" -> (0,-1)<br/> * ":" -> (0,0)<br/> */
String idVal=null; rule.setPartitionLength("512"); rule.setPartitionCount("2"); rule.init(); rule.setHashSlice("0:2"); // idVal = "0"; // Assert.assertEquals(true, 0 == rule.calculate(idVal)); // idVal = "45a"; // Assert.assertEquals(true, 1 == rule.calculate(idVal)); //last 4 rule = new PartitionByString(); rule.setPartitionLength("512"); rule.setPartitionCount("2"); rule.init(); //last 4 characters rule.setHashSlice("-4:0"); idVal = "aaaabbb0000"; Assert.assertEquals(true, 0 == rule.calculate(idVal)); idVal = "aaaabbb2359"; Assert.assertEquals(true, 0 == rule.calculate(idVal));
<tableRule name="sharding-by-murmur"> <rule> <columns>user_id</columns> <algorithm>murmur</algorithm> </rule> </tableRule> <function name="murmur" class="org.opencloudb.route.function.PartitionByMurmurHash"> <property name="seed">0</property><!-- 默认是0--> <property name="count">2</property><!-- 要分片的数据库节点数量,必须指定,否则没法分片--> <property name="virtualBucketTimes">160</property><!-- 一个实际的数据库节点被映射为这么多虚拟节点,默认是160倍,也就是虚拟节点数是物理节点数的160倍--> <!-- <property name="weightMapFile">weightMapFile</property> 节点的权重,没有指定权重的节点默认是1。以properties文件的格式填写,以从0开始到count-1的整数值也就是节点索引为key,以节点权重值为值。所有权重值必须是正整数,否则以1代替 --> <!-- <property name="bucketMapPath">/etc/mycat/bucketMapPath</property> 用于测试时观察各物理节点与虚拟节点的分布情况,如果指定了这个属性,会把虚拟节点的murmur hash值与物理节点的映射按行输出到这个文件,没有默认值,如果不指定,就不会输出任何东西 --> </function>
一致性hash预算有效解决了分布式数据的扩容问题,前1-9中id规则都多少存在数据扩容难题,而10规则解决了数据扩容难点
关于一致性hash详细:
一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。
一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:
1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。
4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。
在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了,这样严重的违反了单调性原则。接下来主要讲解一下一致性哈希算法是如何设计的:
环形Hash空间
按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图
把数据通过一定的hash算法处理后映射到环上
现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对
应的key值,然后散列到Hash环上。如下图:
Hash(object1) = key1;
Hash(object2) = key2;
Hash(object3) = key3;
Hash(object4) = key4;
将机器通过hash算法映射到环上
在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。
假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;
通过上图可以看出对象与机器处于同一哈希空间中,这样按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。
机器的删除与添加
普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会照成大量的对象存储位置失效,这样就大大的不满足单调性了。下面来分析一下一致性哈希算法是如何处理的。
1. 节点(机器)的删除
以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。如下图:
2. 节点(机器)的添加
如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,如下图:
通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持这原有的存储位置。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。
平衡性
根据上面的图解分析,一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,因为还缺少了平衡性。下面将分析一致性哈希算法是如何满足平衡性的。hash算法是不保证平衡的,如上面只部署了NODE1和NODE3的情况(NODE2被删除的图),object1存储到了NODE1中,而object2、object3、object4都存储到了NODE3中,这样就照成了非常不平衡的状态。在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。
——“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。
以上面只部署了NODE1和NODE3的情况(NODE2被删除的图)为例,之前的对象在机器上的分布很不均衡,现在我们以2个副本(复制个数)为例,这样整个hash环中就存在了4个虚拟节点,最后对象映射的关系图如下:
根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。那么在实际操作中,正真的对象查询是如何工作的呢?对象从hash到虚拟节点到实际节点的转换如下图:
“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2
以上所有规则每种都有特定使用场景,可以选择性使用!
标签:目标 val hang 中间件 完全 项目 阿里 用户名 日期
原文地址:http://blog.csdn.net/lifetragedy/article/details/52919984