【朝花夕拾】基础算法之一致性Hash
算法简介
普通Hash算法
普通的Hash函数作用是散列,本质上就是将一系列在形式上具有相似性质的数据,打散成随机的、均匀分布的数据。
比如,对字符进行md5计算,得到一串的hash值。
package main
import (
"crypto/md5"
"fmt"
)
func main() {
s1 := "abcd"
date := []byte(s1)
hs := md5.Sum(date)
md5str := fmt.Sprintf("%x", hs)
fmt.Println(md5str)
}
输出:e2fc714c4727ee9395f324cd2e7f331f
一致性Hash算法(Consistent Hashing)
一致性哈希算法在1997年由麻省理工学院的Karger等人在解决分布式缓存中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题,
核心思想:将key作hash运算, 并按一定规律取整得出0 -
$2^{32}-1$
之间的值, 环的大小为 $2^{32}$
,key计算出来的整数值则为key在hash环上的位置。
主要用途:解决分布式系统中对象与节点的映射关系。
go语言算法实现
导入代码实现库
go get github.com/g4zhuj/hashring
使用例子:
// virtualSpots means virtual spots created by each node
nodeWeight := make(map[string]int)
nodeWeight["node1"] = 1
nodeWeight["node2"] = 1
nodeWeight["node3"] = 2
vitualSpots := 100
hash := NewHashRing(virtualSpots)
//add nodes
hash.AddNodes(nodeWeight)
//remove node
hash.RemoveNode("node3")
//add node
hash.AddNode("node3", 3)
//get key's node
node := hash.GetNode("key")
原理及使用场景
使用场景 分布式系统中对象与节点的映射关系。
传统方案(硬哈希)是使用对象的哈希值,对节点个数取模,再映射到相应编号的节点,即 mod(key, n)。 这种方案在节点个数变动时,映射关系变为 mod(key, n+1) / mod(key, n-1),绝大多数对象的映射关系会失效而需要迁移。
详细原理
一、创建哈希环
- 一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为
$0-2^{32}-1$
(即哈希值是一个32位无符号整形),整个哈希空间环如下:
上图中,可见整个空间按顺时针方向组织,0和$2^{32}-1$
在零点中方向重合。
- 下一步,将各个服务器节点使用Hash进行一个哈希,具体可以选择服务器的ip或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将上文中四台服务器使用ip地址哈希后在环空间的位置如下:
- 接下来,使用如下算法定位数据访问到相应服务器:将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。
例如:我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:
图3根据一致性哈希算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上。
二、容错性
下面分析一致性哈希算法的容错性:
现假设Node C不幸宕机,可以看到此时对象A、B、D不会受到影响,只有C对象被重定位到Node D。
一般的,在一致性哈希算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。
三、扩张性
下面考虑另外一种情况,如果在系统中增加一台服务器Node X,如下图所示:
图4此时对象Object A、B、D不受影响,只有对象C需要重定位到新的Node X 。
一般的,在一致性哈希算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。
综上所述,一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
四、可能问题及解决方式
一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。
例如系统中只有两台服务器,其环分布如下
图5此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。
为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。
具体做法:在服务器ip或主机名的后面增加编号来实现。例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点:
图6同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。
算法特点
- 均衡性(Balance)
均衡性主要指,通过算法分配, 集群中各节点应该要尽可能均衡。
- 单调性(Monotonicity)
单调性主要指,当集群发生变化时, 已经分配到老节点的key,尽可能的任然分配到之前节点,以防止大量数据迁移。
这里一般的hash取模就很难满足这点,而一致性hash算法能够将发生迁移的key数量控制在较低的水平。
- 分散性(Spread)
分散性主要针对同一个key,当在不同客户端操作时,可能存在客户端获取到的缓存集群的数量不一致,从而导致将key映射到不同节点的问题,这会引发数据的不一致性。
好的hash算法应该要尽可能避免分散性。
- 负载(Load)
负载主要是针对一个缓存而言,同一缓存有可能会被用户映射到不同的key上,从而导致该缓存的状态不一致。
具体的工程应用场景
- Apache Cassandra:在数据分区(Data partitioning)中使用到一致性哈希
- Voldemort:LinkedIn 开发的键值(Key-Value)存储数据库,在数据分区(Data partitioning)中使用到一致性哈希
- Akka:Akka 的 Router 使用了一致性哈希
- Dynamo:数据划分使用了一致性哈希
- Couchbase:在数据分区(Data partitioning)中使用到一致性哈希
- Riak:数据划分使用了一致性哈希
- GlusterFS:文件分布使用了一致性哈希
参考文章