一致性hash算法--负载均衡

有没有好奇过redis、memcache等是怎么实现集群负载均衡的呢?

其实他们都是通过一致性hash算法实现节点调度的。

一、Redis集群的使用

我们在使用Redis的时候,为了保证Redis的高可用,提高Redis的读写性能,最简单的方式我们会做主从复制,组成Master-Master或者Master-Slave的形式,或者搭建RedisCluster集群,进行数据的读写分离,类似于数据库的主从复制和读写分离。如下所示:




redis集群 强一致性 redis集群一致性算法_hash算法


同样类似于数据库,当单表数据大于500W的时候需要对其进行分库分表,当数据量很大的时候(标准可能不一样,要看Redis服务器容量)我们同样可以对Redis进行类似的操作,就是分库分表。

在讲一致性hash算法前,我们先看看求余hash算法:

hash(object)%N

1.一个缓存服务器宕机了,这样所有映射到这台服务器的对象都会失效,我们需要把属于该服务器中的缓存移除,这时候缓存服务器是 N-1 台,映射公式变成了 hash(object)%(N-1) ;

2.由于QPS升高,我们需要添加多一台服务器,这时候服务器是 N+1 台,映射公式变成了 hash(object)%(N+1) 。

1 和 2 的改变都会出现所有服务器需要进行数据迁移。

一致性HASH算法

一致性hash算法在分布式系统中广泛应用,研究过memcached缓存数据库的人都知道,memcached服务器端本身不提供分布式cache的一致性,而是由客户端来提供,具体在计算一致性hash时采用如下步骤:


redis集群 强一致性 redis集群一致性算法_hash crc32_02


1.首先求出memcached服务器(节点)的哈希值,并将其配置到0-2^32的圆(continuum)上。

2.然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。

3.然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过2^32仍然找不到服务器,就会保存到第一台memcached服务器上。

从上图的状态中添加一台memcached服务器。余数分布式算法由于保存键的服务器会发生巨大变化而影响缓存的命中率,但Consistent Hashing中,只有在圆(continuum)上增加服务器的地点逆时针方向的第一台服务器上的键会受到影响,如下图所示:


redis集群 强一致性 redis集群一致性算法_服务器_03


一致性HASH算法的出现有效的解决了上面普通求余算法在节点变动后面临全部缓存失效的问题:

type Consistent struct {
 numOfVirtualNode int 
 hashSortedNodes []uint32
 circle map[uint32]string
 nodes map[string]bool
}

简单地说,一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某空间哈希函数H的值空间是0-2^32-1(即哈希值是一个32位无符号整形),整个哈希空间如下:


redis集群 强一致性 redis集群一致性算法_服务器_04


下一步将各个服务器使用哈希算法计算出每台机器的位置,具体可以使用服务器的IP地址或者主机名作为关键字,并且是按照顺时针排列:

//这里我选择crc32,具体情况具体安排
 func hashKey(host string) uint32 {
   scratch := []byte(host) 
 return crc32.ChecksumIEEE(scratch)
}

这里我们假设三台节点memcache经计算后位置如下:


redis集群 强一致性 redis集群一致性算法_服务器_05


//add the node 
 c.Add("Memcache_server01") 
 c.Add("Memcache_server02") 
 c.Add("Memcache_server03")
 func (c *Consistent) Add(node string) error { 
 if _, ok := c.nodes[node]; ok { 
 return errors.New("host already existed") 
}
 c.nodes[node] = true 
 // add virtual node 
 for i := 0; i < c.numOfVirtualNode; i++ {
 virtualKey := getVirtualKey(i, node)
 c.circle[virtualKey] = node
 c.hashSortedNodes = append(c.hashSortedNodes, virtualKey)
 } 
 sort.Slice(c.hashSortedNodes, func(i, j int) bool {
 return c.hashSortedNodes[i] < c.hashSortedNodes[j]
 })
 return nil
}

接下来使用相同算法计算出数据的哈希值,并由此确定数据在此哈希环上的位置。

假如我们有数据A、B、C和D,经过哈希计算后位置如下:


redis集群 强一致性 redis集群一致性算法_数据_06


根据一致性哈希算法,数据A就被绑定到了server01上,D被绑定到了server02上,B、C在server03上,是按照顺时针找最近服务节点方法

这样得到的哈希环调度方法,有很高的容错性和可扩展性:

假设server03宕机


redis集群 强一致性 redis集群一致性算法_数据_07


可以看到此时A、C、B不会受到影响,只是将B、C节点被重定位到Server 1。一般的,在一致性哈希算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即顺着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。

考虑另外一种情况,如果我们在系统中增加一台服务器Memcached Server 04:


redis集群 强一致性 redis集群一致性算法_hash crc32_08


此时A、D、C不受影响,只有B需要重定位到新的Server 4。一般的,在一致性哈希算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即顺着逆