ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

一致性hash和普通hash和hash槽

2022-05-27 09:35:23  阅读:154  来源: 互联网

标签:Hash 算法 普通 key 服务器 一致性 hash 节点


普通hash

Hash函数:一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。
碰撞(冲突):如果两个关键字通过hash函数得到的值是一样的,就是碰撞或冲突。
Hash表(散列表):根据散列函数和冲突处理将一组关键字分配在连续的地址空间内,并以散列值记录在表中的存储位置,这种表成为散列表。

常用算法

直接寻址法:即取关键字或关键字的线性函数为散列地址:H(key)=key或H(key)=a*key+b;
数字分析法:即分析一组数据后采用的方法:如人的出生年月为92-09-03则前三位重复的几率比较大,容易产生碰撞,所以应该采用后三位作为hash值好点
平方取中法:取关键字平方的后几位。
折叠法:把关键字分割成位数相同的几部分,最后一部分可以位数不同,然后取这几部分的叠加值
随机数法:以关键值作为生成随机数的种子生成散列地址,通常适用于关键字长度不同的场合。
除留余数法:取关键字被某个不大于散列表长度m的数p除后所得的余数为散列地址:H(key)=key%p(p<=m);不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生碰撞。

列表:

• 加法Hash;把输入元素一个一个的加起来构成最后的结果
• 位运算Hash;这类型Hash函数通过利用各种位运算(常见的是移位和异或)来充分的混合输入元素
• 乘法Hash;这种类型的Hash函数利用了乘法的不相关性(乘法的这种性质,最有名的莫过于平方取头尾的随机数生成算
法,虽然这种算法效果并不好);jdk5.0里面的String类的hashCode()方法也使用乘法Hash;32位FNV算法
• 除法Hash;除法和乘法一样,同样具有表面上看起来的不相关性。不过,因为除法太慢,这种方式几乎找不到真正的应用
• 查表Hash;查表Hash最有名的例子莫过于CRC系列算法。虽然CRC系列算法本身并不是查表,但是,查表是它的一种最快
的实现方式。查表Hash中有名的例子有:Universal Hashing和Zobrist Hashing。他们的表格都是随机生成的。
• 混合Hash;混合Hash算法利用了以上各种方式。各种常见的Hash算法,比如MD5、Tiger都属于这个范围。它们一般很少

什么是一致性Hash算法
1. 为什么要使用Hash算法
举个例子,我们在使用Redis的时候,为了保证Redis的高可用,提高Redis的读写性能,最简单的方式我们会做主从复制,,或者搭建Redis集群,进行数据的读写分离,当数据量很大的时候(标准可能不一样,要看Redis服务器容量)我们同样可以对Redis进行类似数据库的操作,就是分库分表。如图所示

假若采用随机分配的方式,那么我们保存到一条数据都有可能存储在任何一组Redis中,如果我们需要查询某一条数据,由于我们不清楚数据保存在哪一个redis服务器中,因此需要遍历了所有的Redis服务器,这显然不是我们想要的结果。
如果我们使用Hash的方式,每一张图片在进行分库的时候都可以定位到特定的服务器,示意图如下:

上图中,假设我们查找的是”a.png”,由于有4台服务器(排除从库),因此公式为hash(a.png) % 4 = 2 ,可知定位到了第2号服务器,这样的话就不会遍历所有的服务器,大大提升了性能!

2. 使用Hash带来的问题
上述的方式虽然提升了性能,我们不再需要对整个Redis服务器进行遍历!但是,使用上述Hash算法进行缓存时,会出现一些缺陷,主要体现在服务器数量变动的时候,所有缓存的位置都要发生改变!

试想一下,如果4台缓存服务器已经不能满足我们的缓存需求,那么我们应该怎么做呢?很简单,多增加几台缓存服务器不就行了!假设:我们增加了一台缓存服务器,那么缓存服务器的数量就由4台变成了5台。那么原本hash(a.png) % 4 = 2 的公式就变成了hash(a.png) % 5 = ? , 可想而知这个结果肯定不是2的,这种情况带来的结果就是当服务器数量变动时,所有缓存的位置都要发生改变!换句话说,当服务器数量发生改变时,所有缓存在一定时间内是失效的,当应用无法从缓存中获取数据时,则会向后端数据库请求数据!

同样的,假设4台缓存中突然有一台缓存服务器出现了故障,无法进行缓存,那么我们则需要将故障机器移除,但是如果移除了一台缓存服务器,那么缓存服务器数量从4台变为3台,也是会出现上述的问题!

所以,我们应该想办法不让这种情况发生,但是由于上述Hash算法本身的缘故,使用取模法进行缓存时,这种情况是无法避免的,为了解决这些问题,Hash一致性算法(一致性Hash算法)诞生了!

3. 一致性hash算法原理
一致性Hash算法也是使用取模的方法,只是,刚才描述的取模法是对服务器的数量进行取模,而一致性Hash算法是对2^32取模,一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,将各个服务器使用Hash进行一个哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将上文中四台服务器使用IP地址哈希后在环空间的位置如下:

每次根据要缓存的key计算得到hash值,在hash环上顺时针查找距离最近的缓存服务器节点,

根据一致性Hash算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上。

一致性Hash算法的容错性和可扩展性
现假设Node C不幸宕机,可以看到此时对象A、B、D不会受到影响,只有C对象被重定位到Node D。一般的,在一致性Hash算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响,如下所示:

下面考虑另外一种情况,如果在系统中增加一台服务器Node X,如下图所示:

此时对象Object A、B、D不受影响,只有对象C需要重定位到新的Node X !一般的,在一致性Hash算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。

综上所述,一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。

4. Hash环的数据倾斜问题
一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题,例如系统中只有两台服务器,其环分布如下:

此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。为了解决这种数据倾斜问题,一致性Hash算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器IP或主机名的后面增加编号来实现。
例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点:

同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。

总结
在分布式系统中一致性hash起着不可忽略的地位,无论是分布式缓存,还是分布式Rpc框架的负载均衡策略都有所使用。分布式系统每个节点都有可能失效,并且新的节点很可能动态的增加进来的情况,如何保证当系统的节点数目发生变化的时候,我们的系统仍然能够对外提供良好的服务,这是值得考虑的!

一致性哈希算法能尽可能减少了服务器数量变化所导致的缓存迁移。

consistent(一致性) hash算法能够在一定程度上改善缓存的雪崩问题,它能够在移除/添加一台缓 存服务器时,尽可能小的改变已存在的key映射关系,避免大量key的重新映射。

先构造一个长度为2 32次方的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 23 2次方-1])将服务器节点放置在这个Hash环上,然后根据数据的Key值计算得到其Hash值(其分布也为[0, 2 32次方-1]),接着在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。

这种算法解决了普通余数Hash算法伸缩性差的问题,可以保证在上线、下线服务器的情况下尽量有多的请求命中原来路由到的服务器。
————————————————————————————————————————————————

一致性hash主要用于分布式系统中,用于解决数据选择节点存储、选择节点访问、增删节点后数据的迁移和重分布问题。redis集群并没有使用一致性hash,而是使用了hash槽来解决数据分配的问题。

一致性hash:

它是一个0-2^32次方的圆,主要操作步骤:将每一个服务节点进行hash(如ip),让其落在这个闭合的圆环上;当我们进行数据存储或访问时,计算key的hash值,让其也落在这个闭合圆环之中;那么它顺时针找到的第一个服务节点就是处理key的节点

当添加节点时,例如再节点2和4之间添加了一个节点5,那么就有可能伴随数据的重分配,图中黄色区域的数据被分配到了节点5之上

 

 

 

当删除节点时,那么它的数据就会被分配到它顺时针寻找到的下一个节点之上。以下图为例节点2被删除,2的数据被分配到了4,那么就有可能出现一个问题,4的访问量或内存使用率就会瞬间增高,如果2之上有热点数据,就有可能将4直接打挂,这样就会顺延再将数据转移到3,从而导致所有节点循环崩溃。

同时当集群中节点过少时还有可能出现数据倾斜,大量的数据都被分配到了同一个节点上,导致单一节点内存使用率和qps都很高,分布式服务的负载不平衡。如下图所示,从A顺时针到B明显比B顺时针到A所占的区域小,那么数据被分配到A的概率就会比B大。

为了解决循环崩溃现象和数据倾斜现象,提出了虚拟节点的概念。就是将真实节点计算多个哈希形成多个虚拟节点并放置到哈希环上,定位算法不变,只是多了一步虚拟节点到真实节点映射的过程。如下图所示每个节点都映射出了两个虚拟节点,当节点1挂掉以后,它的数据并没有全部转移到同一个节点,而是被分配到了v301和v200,即节点2和3之上;此外由于加入了虚拟节点数据倾斜的问题也得到了解决。

redis 使用数据分片的hash槽来应对数据存储和读取。redis集群共有2^14次方16384个hash槽,当操作数据时,使用CRC16算法计算key的hash值,然后与16384取模从而确定该数据是属于哪一个slot槽的。redis集群中,每个服务节点都被分配一段hash槽,只要确定了数据属于哪一个槽,就可以确定该数据是在哪一个节点之上。槽是可以迁移的,master节点的slave节点不分配槽,只拥有读权限即使用权。但是注意在代码中redis cluster执行读写操作的都是master节点,并不是你想 的读是从节点,写是主节点(不过应该可以进行设置,从节点也提供读的能力,因为MT的Squirrel从节点可以提供读能力)。第一次新建redis cluster时,16384个槽是被master节点均匀分布的。当新增或删除master节点时,需要对槽进行重分配,而不是直接对数据进行分配。对于hash槽的转移和分配,redis不会自动进行,需要人工辅助。

 

一致性hash和redis hash槽的区别

1. redis hash槽并不是闭合的,它一共有16384个槽,使用CRC16算法计算key的hash值,与16384取模,确定数据在哪个槽中,从而找到所属的redis节点;一致性hash表示一个0到2^32的圆环,对数据计算hash后落到该圆环中,顺时针第一个节点为其所属服务。

2.一致性hash是通过虚拟节点去避免服务节点宕机后数据转移造成的服务访问量激增、内存占用率过高、数据倾斜等问题,保证数据完整性和集群可用性的;而hash集群是使用主从节点的形式,主节点提供读写服务,从节点进行数据同步备份,当主节点出现故障后,从节点继续提供服务。

二、redis集群中数据迁移

当向集群中添加节点后,新添加的节点中是没有分配槽的,假设我们新在集群中添加了节点D,那么可以将现有节点(A、B、C)的slot分配给新节点。例如从A分配slot8至D,那么在槽迁移的中间状态下slot8在A表现为MIGRATING、在D表现为IMPORTING。

当槽迁移的过程中,发生了key属于该槽的请求即当客户端请求的key处于在正在迁移的某个slot时,因为此时节点的映射关系(即槽与节点的映射)还没有改变,客户端依然认为slot8是属于A的,但是其中的一些key所对应的数据可能已经被转移到了D,对于A来说会有如下几种情况:

(1)key还在A,那么进行处理后返回

(2)key不在A了,返回ASK,让其发送ASKING命令到目标节点即D,然后进行请求

(3)如果命令包括了多个key,如果这些key都存在则进行操作后返回;如果都不存在返回客户端ASK;如果部分存在,则返回客户端TRYAGAIN,通知客户端稍后重试,这样当所有的key都迁移完成后再请求就会返回ASK

如果请求了目标节点的IMPORTING状态的slot,会出现以下几种情况

(1)如果是正常的请求则会发送MOVED命令,此时说明节点映射出错了,迁移还没有完成,被slot还属于源节点,会刷新映射

(2)如果是ASKING命令则会被执行,key存在,执行操作后返回;key不存在,则会被新建

键空间迁移

在槽迁移的前提下,还会进行键迁移,即将节点A中槽8对应键空间迁移至节点D。执行migrate命令,分为三步完成该过程,即dump(A)、restore(D)、del(A)

完成之后会将处于migrating和importing状态的槽置为正常状态,同时会刷新槽与节点的映射关系

在redis代码表示节点node的结构体ClusterNode中,存在一个char类型的slots数组和一个int型numSlots,数组长度是2048,表示一个二进制位数组,包含16384个二进制位,每一位默认为0,如果一个slot属于该节点,那么它在数组中对应下标所在的二进制位就会变成1;numslots表示该节点拥有多少个槽。

而槽的分配信息存储在clusterState结构体的clusterNode类型的数组中,它的长度为16384,即我们通过CRC16算法得出key的hash值,并对16384取模后使用该值对应的slot是哪一个,获取数组对应下标的元素,它就是该槽所属的redis节点。

redis集器并不会使用代理,当一个操作请求来到了节点a,但是经过计算发现key对应的槽不在a上,那么此时a会执行move命令将它转发到正确的节点上
————————————————————————————————————————————————

通过本文将收获如下:

  • 为什么Redis Cluster的Hash Slot 是16384?
  • 什么是hash(概念)
  • 什么是一致性hash
  • 什么是hash slot

为什么Redis Cluster的Hash Slot 是16384?
我们知道一致性hash算法是2的16次方,为什么hash slot是2的14次方呢?

在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 * 8 (8 bit) * 1024(1k) = 16K),也就是说使用2k的空间创建了16k的槽数。 虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 * 8 (8 bit) *1024(1k)=65K),也就是说需要需要8k的心跳包,作者认为这样做不太值得;并且一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择。
————————————————

什么是hash
Hash一般翻译做“散列”,就是把固定或任意长度的输入,通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,通常散列值的空间远小于输入的空间,不同的输入可能会散列成相同的输出,而且不可能从散列值来唯一确定输入值。简单而言Hash就是一种将固定或任意长度的消息压缩到某一固定长度的消息摘要的处理

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

 

先看这段代码,这是hashmap的hash函数
代码解析:

当key不为null时,返回key hash值异或key hash值无符号右移16位后的hash值
异或:相同为0(1^ 1= 0 、 0^0=0 ),不同为1 (1^0 = 1 )
int类型32位,右移16,就是将前16置为0
eg:00011000000100011111000101100000–>00000000000000000001100000010001
这样可以保留高16位数据特性和低16位数据特性,同时高16位异或低16位降低hash冲突、增加散列程度。但是当数小于216时,右移16位后全是0,(h = key.hashCode()) ^ (h >>> 16)等价与key.hashCode(),不会重新调整其值

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length; //获取table的长度,默认16
        //  table数组在(n - 1) & hash位置上有没有值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);// 创建链表节点
        else ……
    }

(n - 1) & hash 等价于<hash的十进制数%(n - 1=16-1=15)>
15二进制1111,如果上述key hash值不进行异或key hash值无符号右移16位后的hash值,这样最后4位为0的hash值都会插入table数组的0位

————————————————

hash算法的缺点,一致性hash算法诞生
在分布式缓存服务中,经常需要对服务进行节点添加和删除操作,我们希望的是节点添加和删除操作尽量减少数据-节点之间的映射关系更新(节点数的增加与减少对数据命中节点影响不大)
eg:我们使用的是哈希取模( hash(key)%nodes ) 算法作为路由策略

hash(key)=10,nodes=5,hash(key)%nodes=0,命中0 redis节点 set hello world
hash(key)=10,nodes=3,hash(key)%nodes=1,命中1 redis节点 get hello
结果发现每次同样key,在节点数变化后命中的节点不一样,导致造成大量的请求无法命中(访问正确的节点)从而导致缓存数据被重新加载,这样的结果就是重新hash做一次sharding。这种操作会导致服务在一定的时间不可用,而且每次扩缩容都会存在这个问题

基于上面的缺点提出了一种新的算法:一致性哈希。一致性哈希可以实现节点删除和添加只会影响一小部分数据的映射关系,由于这个特性哈希算法也常常用于各种均衡器中实现系统流量的平滑迁移

什么是一致性hash
一致性hash算法主要应用于分布式存储系统中,可以有效地解决分布式存储结构下普通余数Hash算法带来的伸缩性差的问题,可以保证在动态增加和删除节点的情况下尽量有多的请求命中原来的机器节点。

一致性hash是一个0-232的闭合圆,(拥有223个桶空间,每个桶里面可以存储很多数据,可以理解为s3的存储桶)所有节点存储的数据都是不一样的

看到这幅图不要怕,让我们看一下这幅图这些大小不一的⚪都是什么

这个最大的⚪便是0-232的闭合圆,其中0位置与232重叠
第二打的⚪且有节点字样的代表节点,通常使用其节点的ip或者是具有唯一标示的数据进行hash(ip),将其值分布在这个闭合圆上
最小的⚪代表节点里的数据,通常将存储的key进行hash(key),然后将其值要分布在这个闭合圆上
怎么区分最小的⚪是那个节点的?
– 从hash(key)在圆上映射的位置开始顺时针方向找到的一个节点即为存储key的节点(节点逆时针的最小⚪都是该节点数据)
一致性hash就是尽量在分布式节点增加、减少情况下,让请求命中原节点

一致性hash——增加节点

看看减少节点那幅图,你有没有发现,当节点一数据(热点数据)特别多,节点一宕机,数据传给节点二,但这样节点二数据突然暴增,请求突然暴增,然后也宕机了,就这样一种传下去,所有节点是不是都宕机了,这就是雪崩
当节点特别少,比如两个节点时,容易造成数据倾斜

雪崩:当节点一宕机,节点的虚拟节点v1001与v1002消失,数据落在了节点三虚拟节点v3001和节点四虚拟节点v4001上,这样对节点压力减小
数据倾斜:虚拟节点增加,数据一直落在一个节点上的概率下降
到这里,一致性hash结束,让我们快乐的进入下一个知识点,什么是hash槽
————————————————

什么是hash slot
Redis Cluster在设计中没有使用一致性哈希(Consistency Hashing),而是使用数据分片引入哈希槽(hash slot)来实现
一个 Redis Cluster包含16384(0~16383)个哈希槽(可以假设成盒子),存储在Redis Cluster中的所有键都会被映射到这些slot中,集群中的每个键都属于这16384个哈希槽中的一个。按照槽来进行分片,通过为每个节点指派不同数量的槽,可以控制不同节点负责的数据量和请求数
eg:当前集群有3个节点,槽默认是平均分的:

节点 A (6381)包含 0 到 5499号哈希槽.
节点 B (6382)包含5500 到 10999 号哈希槽.
节点 C (6383)包含11000 到 16383号哈希槽
增加一个 master节点,就将其他 master 的 hash slot 移动部分过去
减少一个 master节点,就将它的 hash slot 移动到其他 master 上去
这样移动 hash slot 的成本是非常低的,并且将一个哈希槽从一个节点移动到另一个节点不会造成节点阻塞, 所以无论是添加新节点还是移除已存在节点, 又或者改变某个节点包含的哈希槽数量, 都不会造成集群下线
那hash slot是怎么分槽的呢?
集群使用公式slot=CRC16(key)/16384来计算key属于哪个槽,其中CRC16(key)语句用于计算key的CRC16 校验和
————————————————

问题一:增加、减少节点,都会有槽移动到新节点,ip不一样了,那不会报错吗?

当客户端的key 经过hash运算,发现slot 槽位不在原节点的时候:

如果是非集群方式连接,则直接报告错误给client,并告诉client slot 槽位在集群中那个IP的新master主机
如果是集群方式连接,则将客户端重定向到正确的节点上
到这里,让我们一起来看文章开头的那个面试题

Redis Cluster的之所以使用Hash Slot 的16384(214),而不使用一致性hash的216
bitmap与CRC16请自行查阅资料

Redis的作者认为CRC16(key) mod 16384的效果已经不错了,虽然没有一致性hash灵活,但实现很简单,节点增删时处理起来也很方便
redis的一个节点的心跳信息中需要携带该节点的所有配置信息,而16K(214)大小的槽数量所需要耗费的内存为2K(2 * 8bit *1K=16K),但如果使用65K(216)个槽,这部分空间将达到8K(8 * 8bit *1K=65K),心跳信息就会很庞大
你可能认为2k和8k差不多,但当节点非常多时,4倍差距将会放的很大,当然Redis集群中主节点的数量基本不可能超过1000个
Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行char压缩,但是如果bitmap的填充率slots / N很高的话,bitmap的压缩率就很低,所以N表示节点数,如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。而16K个槽且主节点为1000的时候,是刚好比较合理的,既保证了每个节点有足够的哈希槽,又可以很好的利用bitmap
选取了16384是因为CRC16会输出16bit的结果,可以看作是一个分布在0-216-1之间的数,redis的作者测试发现这个数对214求模的会将key在0-214-1之间分布得很均匀,因此选了这个值
————————————————

 

标签:Hash,算法,普通,key,服务器,一致性,hash,节点
来源: https://www.cnblogs.com/hanease/p/16316267.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有