哈希取余
好吧,我们决定打破这种基于数据项商业逻辑的划分思维,来考虑一种基于 key 的划分方式,这有些类似于后面介绍的数据库水平分区(Sharding)。我们需要设计一种不依赖数据项内容的散列算法,将所有数据项的 key 均衡分配在这三台缓存服务器上。
一个简单而有效的方法是“取余”运算,这就像打扑克时的发牌,让所有数据项按照一个顺序在不同的缓存服务器上轮询,这可以达到较好的相对平衡,想想发牌的动机也正是为了让大家彼此公平。这种方法是一种比较常用的基本散列算法,事实上在很多时候都用得到,而且你可以根据实际情况对它进行改造,达到更好的散列效果。下面我们举个例子。
在“取余”之前,我们先要做一些准备工作,目的是让 key 变成整数,而且尽量唯一。比如对于以下这个 key:
article_090222.htm
我们先对它进行 md5 运算,这里直接使用 PHP 命令行方式:
s-colin:~ # php -r "echo md5('article_090222.htm');"
e6e87fc9f9c2914339a9b7cc4db6055c
得到的是一个 32 字节的字符串,同时它也是一个十六进制的长整数,为了减少计算开销,我们取这个字符串的前 5 个字节,然后将它转换为十进制数:
s-mat:~ # php -r "echo hexdec('e6e87');"
945799
然后将结果进行“模 3”运算:
s-mat:~ # php -r "echo 945799 % 3;"
1
得到的余数便是缓存服务器的编号,我们的三台缓存服务器应该从 0 开始编号,那么 1 代表了第二台服务器。 看起来真复杂,我们不得不需要一个“缓存连接器”了,我们希望将上面这些运算都放在连接器里,而只需要告诉它 key,接下来选择缓存服务器的事情就拜托给它了。看看连接器的一个例子:
<?php
function memcache_connector($key)
{
$hosts = array(
'10.0.1.12',
'10.0.1.13',
'10.0.1.14'
);
$host_index = hexdec(substr(md5($key), 0, 5)) % 3;
$host = $hosts[$host_index];
return memcache_connect($host, 11711);
}
?>
现在,我们访问缓存的时候只需要这样连接缓存服务器即可:
<?php
$memcache = memcache_connector('article_090222.htm');
?>
那么,如果还需要继续扩展,一定难不倒你了,将“模 3”的运算变成“模 4”、“模 5”……然后其余的工作就放心地交给连接器去做吧。
这里有一个问题也许你一直在思考,那就是当我们扩展缓存系统后,由于分区算法的改变,会涉及缓存数据需要从一台缓存服务器迁移到另一台缓存服务器的问题,如何迁移呢?事实上,根本不需要考虑分区之间的迁移,因为这是缓存,它应该具备在必要时刻牺牲自己的勇气,当然这是你赋予它的,你必须明白缓存不是持久存储,并且从引入分布式缓存开始就不断地提醒自己。
没错,当调整缓存分区算法后,我们需要时间来等待缓存重建和预热,但这往往并不影响站点的正常运转,前提是你按照前面读缓存和写缓存的理念来进行设计。顺便一提的是,与此相比,数据库规模扩展引发分区(Shard)之间的数据迁移就要复杂得多,后面我们会有专门的章节探讨它。
——郭昕《高并发web框架》
一致性哈希(Consistent Hashing):
在大型web应用中,缓存可算是当今的一个标准开发配置了。在大规模的缓存应用中,应运而生了分布式缓存系统。
分布式缓存系统的基本原理,大家也有所耳闻。
key-value如何均匀的分散到集群中?
说到此,最常规的方式莫过于hash取模的方式。比如集群中可用机器适量为N,那么key值为K的的数据请求很简单的应该路由到hash(K) mod N对应的机器。的确,这种结构是简单的,也是实用的。
但是在一些高速发展的web系统中,这样的解决方案仍有些缺陷。随着系统访问压力的增长,缓存系统不得不通过增加机器节点的方式提高集群的相应速度和数据承载量。增加机器意味着按照hash取模的方式,在增加机器节点的这一时刻,大量的缓存命不中,缓存数据需要重新建立,甚至是进行整体的缓存数据迁移,瞬间会给DB带来极高的系统负载,设置导致DB服务器宕机。
那么就没有办法解决hash取模的方式带来的诟病吗?
一致性哈希算法(Consistent Hashing Algorithm)是一种分布式算法,常用于负载均衡。Memcached client也选择这种算法,解决将key-value均匀分配到众多Memcached server上的问题。它可以取代传统的取模操作,解决了取模操作无法应对增删Memcached Server的问题(增删server会导致同一个key,在get操作时分配不到数据真正存储的server,命中率会急剧下降)
选择具体的机器节点不在只依赖需要缓存数据的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键的重新分布。
要取得比较好的负载均衡的效果,往往在服务器数量比较少的时候需要增加虚拟节点来保证服务器能均匀的分布在圆环上。因为使用一般的hash方法,服务器的映射地点的分布非常不均匀。使用虚拟节点的思想,为每个物理节点(服务器)在圆上分配100~200个点。这样就能抑制分布不均匀,最大限度地减小服务器增减时的缓存重新分布。用户数据映射在虚拟节点上,就表示用户数据真正存储位置是在该虚拟节点代表的实际物理服务器上。
下面有一个图描述了需要为每台物理服务器增加的虚拟节点。
x轴表示的是需要为每台物理服务器扩展的虚拟节点倍数(scale),y轴是实际物理服务器数,可以看出,当物理服务器的数量很小时,需要更大的虚拟节点,反之则需要更少的节点,从图上可以看出,在物理服务器有10台时,差不多需要为每台服务器增加100~200个虚拟节点才能达到真正的负载均衡。
讨论:
1.一致性hash仅仅是为了不去做数据迁移,但是随之机器的增加会越来越不可用。而且本身的消耗也会增大
这个的依据是什么?
你好 ,一致性hash,假设本来应该落在B点的数据,在A,B之间加一台机器,平均有一半的数据会无效。并且A到加的机器点上的数据在B上已经没有用,怎么去清理。随着机器的越来越多,不命中的概率也会越来越多。
虽然说最常用的hash取模不可避免的需要做数据迁移,但是可以选择时间点,比如半夜两点。这个时候访问肯定会很少。
不对的请指教
如果是C、A之间加入节点B,那原来落在CB之间的数据不再找A,而是找B了,这部分数据在A确实是失效。但你说的这个是纯理论。实际中加入B节点之后,CB间的数据(原来命中A上)会逐渐保存到B上(而不是不命中的时候什么都不做),同时A上的数据随着新到数据增加,原来那部分失效数据通过LRU算法将逐渐淘汰掉。所以我觉随着机器增加,不命中的概率不会大幅波动。 事实上,一致性hash就是用来解决存储节点增加导致的命中降低问题的。
实际例子:日本mixi也是逐渐增加到200台以上的memcached服务器集群,用的就是这种方法,并没有你说的问题。