一致性hash
需求
任何东西的存在都是因为有需求。
在一个稍微大型的分布式系统中,尤其是分布式缓存中,单台机器的容量有限,需要通过N台机器共同提供服务。那么针对N台机器最简单的负载均衡策略就是hash(object) % N
。通过这种方式,只要选择一个比较分散的hash算法,就可以比较好的平均的把请求分散到N台机器上去了。但是这种策略缺点在哪呢?那就是这个N的值一旦发生变化,比如增加一台机器或者有一台机器挂掉了,这时% N
的结果就会发生剧烈的变化,导致大量的缓存失效,大量的负载会穿透到缓存的后台,给整个系统产生剧烈的震荡。
一致性hash
从上面传统的hash策略可以看出,一旦发生节点数量变化,几乎所有的关键字都要重新hash。而一致性hash可以做到只有K/n
个关键词需要迁移。其中K
是关键词的数量,n
是节点数量。
一致性hash的核心思想是,每一个节点对应了hash算法的一段区间域,当一个节点失效的时候,其所对应的区间域会被合并到临近的其他节点的区间域中。其他区间域不受影响。
举例
假设一个hash算法产生的区间域是[0, 100)
,这时,我们有3个节点,他们被分散到0, 30, 80。分别表示[0, 30)
,[30, 80)
,[80, 100)
区间的key都指向对应的节点。假设这时候,第二个节点,也就是30挂掉了,那么[0, 80)
区间的所有key就自动全部指向0。这时,第三个节点80是不受影响的。同样,当我们增加一个节点,他被分配到20,那么[20, 30)
区间就会指向20,不再指向0。
上面在死掉一个节点的时候,会产生0负责的区域过大的问题。这里,我们可以通过建立虚拟节点的方式来优化。也就是一个实际的物理节点拥有若干个虚拟节点,然后让这些虚拟节点放到hash的区间域中。这样,一个实际的物理节点,负责的会是hash域的多个区间段。挂掉之后,这若干的区间段会被合并到不同的虚拟节点上去,也就相当于合并到不同的物理机器上去了。
源码分析
这里我们使用一个Go语言写的一致性hash库consistent来举例。
1 | type Consistent struct { |
这是一致性hash的核心的一个类型。
circle
指的是整个hash域中,值与虚拟节点的映射关系。这个虚拟节点一般是物理机器的ip:port
或者hostname:port
加上虚拟节点编号的一个字符串。
members
虽然是一个map类型,但在这里只是用来作为一个set使用的。用来记录实际的物理节点的地址。
sortedHashes
是一个[]uint32
类型。也就是用来维护所有虚拟节点在hash域中对应的值的顺序。
NumberOfReplicas
就是默认一个物理节点产生多少个虚拟节点。
count
是物理节点的数量
sync.RWMutex
是一把读写锁。
然后我们来看增加一个物理节点:
1 | // need c.Lock() before calling |
首先是产生虚拟节点,并映射到实际的物理节点地址。然后将物理节点加入set,然后更新SortedHashes。最后计数加一。
这个updateSortedHashes
其实很简单,就是把所有序号排序,便于后面Get的时候进行二分查找。
再来看Get
,也就是知道,某个key应该存储哪个物理节点上去。
1 | // Get returns an element close to where name hashes to in the circle. |
过程很简单,除了开头加锁,特殊情况处理,接下来先算一下给定key的hash值,然后search找到在整个hash域对应的是哪个虚拟节点号。这个search就是在上面提到的sortedHashes
上进行二分查找。最后就是返回物理节点的信息。
总结
总得看来,一致性hash还是很好理解的,尤其是结合源码来看。