一致性哈希的通用实现

Posted 林中小舍

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一致性哈希的通用实现相关的知识,希望对你有一定的参考价值。

  

一致性哈希算法在1997年由麻省理工学院的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题

国际惯例,先上源码 https://github.com/manerfan/consistent-hash

  1. 可自定义节点数据类型

  2. 可自定义hash函数

原理

一致性哈希可应用于负载均衡、分布式缓存(缓存分片)等场景中,以下以分布式缓存为例

传统方式

如,现有N个缓存实例,将一个对象object映射到某一个缓存上可以采取取模方式 hash(object) % N
我们称之为简单hash算法。一般,简单hash算法确实能够比较均匀地实现分布式映射,但如果考虑缓存实例变动(增删)的情况:

  1. 某一缓存实例宕机,需要将该实例从集群中摘除,则映射公式变为 hash(object) % (N - 1)

  2. 增加一台缓存实例,将该实例加入集群,则映射公式变为 hash(object) % (N + 1)

对于以上情况,无论新增还是移除,大部分object所映射的缓存实例均会改变,缓存命中率大幅度降低从而回源到服务器,短时间内造成缓存雪崩现象

一致性哈希

一致性 Hash 算法简单的说,在移除/添加一个缓存实例时,尽可能小的改变已存在key映射关系,尽可能的满足单调性的要求。

1. 环形空间

通常的hash算法都是将一个value映射到一个32位的key值,我们可以将这个[0, 2^32-1]空间想象成一个首尾相接的环形队列

2. 将对象映射到hash空间

通过hash函数计算对象hash值,将对象映射到hash环形空间

一致性哈希的通用实现

3. 将缓存实例映射到hash空间

使用缓存实例的ip、port等信息,通过hash函数计算其hash值,将缓存实例映射到hash空间

一致性哈希的通用实现

4. 将对象映射到缓存实例

沿着顺时针方向,查找距离对象object最近的缓存实例,并将对象映射到该实例

一致性哈希的通用实现

5. 添加缓存实例

按照同样的算法,在添加实例后发现,只有少部分对象的映射关系改变

一致性哈希的通用实现

6. 移除缓存实例

按照同样的算法,在移除实例后,同样只有少部分对象的映射关系改变

一致性哈希的通用实现

7. 虚拟节点

为了使对象尽可能均匀地映射到所有的缓存实例中(解决缓存实例分布不均匀的问题),引入虚拟节点的概念
虚拟节点其实为真实节点在hash空间中的复制品,一个真实节点可以对应多个虚拟节点

虚拟节点的hash求值可以在真实节点的求值基础上加入编号等信息 hash(realCacheKey#1) 、 hash(realCacheKey#2)

通用实现

实现目标:

  1. 由于一致性哈希应用较多,并不局限于某一特定场景,故需要能够 自定义节点数据类型

  2. 常规hash算法一般采用md5等,但不限制hash函数实现,故需要能够 自定义hash函数

自定义节点数据类型

这里,我们定义一个公共接口

/** * 真实节点 */interface PhysicalNode { fun hashKey(): String}

自定义节点数据,只需要实现PhysicalNode接口及hashKey方法,程序会通过hashKey的值计算节点的hash值

如,我们定义常规服务节点

/** * 常规的服务节点 * * @param name 服务名称 * @param host 服务host/ip * @param port 服务port */data class HostPortPhysicalNode(val name: String, val host: String, val port: Int) : PhysicalNode { override fun hashKey() = "$name:$host:$port"}

这里,我们还需要定义一个虚拟节点

/** * 虚拟节点 * * @param parent 真实节点 * @param replica 虚拟节点id */data class VirtualNode<out T : PhysicalNode>(val parent: T, private val replica: Int) : PhysicalNode { override fun hashKey() = "${parent.hashKey()}#$replica" fun matches(key: String) = parent.hashKey() == key}

虚拟节点的hashKey为真实节点hashKey加上节点编号

自定义hash函数

这里,我们同样定义一个公共接口

/** * 哈希函数 */interface HashFunc { fun hash(str: String): Long}

自定义hash函数,只需要实现HashFunc接口及hash方法

如,我们定义md5函数

class Md5 : HashFunc { override fun hash(str: String): Long { val md5 = MessageDigest.getInstance("MD5") md5.reset() md5.update(str.toByteArray()) val bytes = md5.digest()
var h: Long = 0 bytes.forEach { h = h shl 8 h = h or (it.toLong() and 0xFF) } return h } }

ConsistentHash的使用

ConsistentHashConsistentHashHelper的实现,见 ConsistentHashHelper.kt

ConsistentHashHelper

我们使用ConsistentHashHelper来构建ConsistentHash

val consistentHash = ConsistentHashHelper.create<HostPortPhysicalNode>().build()

如果需要自定义hash函数,可以通过withHash指定,默认使用md5

val consistentHash = ConsistentHashHelper.create<HostPortPhysicalNode>() .withHash(MyMd5HashFunc()) .build()

同样,可以通过withNodes指定在初始化时生成节点信息

val master = HostPortPhysicalNode("master", "192.169.1.1", 8080)val slave = HostPortPhysicalNode("slave", "192.169.1.2", 8080)
val consistentHash = ConsistentHashHelper.create<HostPortPhysicalNode>() .withHash(MyMd5HashFunc()) .withNodes(listOf(master, slave), 2) // 节点,并指定每个节点的副本数(可以省略,缺省1) .build()

ConsistentHash

运行过程中,可以动态增删节点

val backup = HostPortPhysicalNode("backup", "192.168.1.13", 8888)
consistentHash.add(backup, 4) // 增加节点,并指定每个节点的副本数(可以省略,缺省1)consistentHash.remove(slave.hashKey())

通过getNode函数获取对应object所映射的缓存实例

consistentHash.getNode(hashFunc.hash(object1.key))consistentHash.getNode(hashFunc.hash(object2.key))






以上是关于一致性哈希的通用实现的主要内容,如果未能解决你的问题,请参考以下文章

一致性哈希的golang实现

5行代码实现一致性哈希

一致性哈希的实现

一致性哈希的分析与实现

一致性哈希的分析与实现

深入一致性哈希(Consistent Hashing)算法原理,并附100行代码实现