一文读懂一致性哈希算法

假如有一个业务快速增长,流量巨大,服务器压力也随之增加,直接读写数据库的方案已经不合适了,这时候我们就会想到引入分布式缓存机制,从而将许多热点数据放到缓存层,穿透到数据库层的请求就并不多了。此时,缓存的重要性就不言而喻了。但是,由于缓存数据量很大,缓存的快速查询又是基于内存高速存取实现,而服务器的内存资源又是十分稀缺的,所以如何让请求高效命中,分布式缓存集群如何优雅的伸缩就变成了亟待解决的问题。下文将会就这一问题的以下两种解决方案进行探讨:

  1. Naive Hash-based Distributed Dictionary
  2. Consistent Hashing

1. Naive Hash-based Distributed Dictionary

现在我们先看看 Naive Hash-based Distributed Dictionary 工作方式。假设我们有一个包含 n 个节点的集群,节点编号是 0 , 1 , 2 , 3 … … n − 1 0,1,2,3……n - 1 0,1,2,3n1,有一个哈希函数 h a s h ( x ) hash(x) hash(x),节点 k 存储的键值对 ( k , h a s h ( k ) % n ) (k, hash(k) \% n) (k,hash(k)%n)。如果选择的 h a s h ( x ) hash(x) hash(x) 比较理想,那么结果会比较均匀的分布在 0 , 1 , 2 , 3 … … n − 1 0,1,2,3……n - 1 0,1,2,3n1。如此一来,便可以将 Distributed Dictionary 均匀地分布在集群的各个节点上。

Naive Hash-based Distributed Dictionary 实现非常简单,但也有严重的缺陷。想象一下,此时向集群添加一个节点,那么 ( k , h a s h ( k ) % n ) (k, hash(k) \% n) (k,hash(k)%n) 将会变成 ( k , h a s h ( k ) % ( n + 1 ) ) (k, hash(k) \% (n + 1)) (k,hash(k)%(n+1)),所以几乎每个键值对结果都会在集群中重新分配。同时 n / ( n + 1 ) n/ (n + 1) n/(n+1) 的数据会被移到新节点,几乎是全部。这种操作是非常慢的,而且开销也很大。如果重分配是在任务运行中发生,这可能会带来不便。同理,当集群中剔除某个节点时,也会有类似的情况发生。

2. Consistent Hashing

Consistent Hashing 和 Naive Hashing 一样,会把数据均匀地分布在集群的节点上。但与 Naive Hashing 不同的是,Consistent Hashing 只需要移动相对较少的数据,例如向集群中添加一台机器,只需要移动该机器上的数据,其他所有数据都留在原来的位置。Consistent Hashing 有以下特性:

  • 平衡性(Balance):是指将哈希值尽可能均匀地分布到各个缓存区,平衡性是标准哈希函数的优势。
  • 单调性(Monotonicity):是指如果哈希值都分布到各个缓存区之后,又有新的缓存区加入到分布式缓存中时,哈希值会从一个旧的缓存区移到新的缓存区,但不会从一个旧的缓存区移到另一个旧的缓存区。换言之,当一组可用的缓存区发生更改时,只有在必要时才移动哈希值,以此保持均匀的分布。例如,上一节的取模的线性哈希函数就不满足单调性。
  • 传播性(Spread):表示在客户端上,哈希值被分布到不同缓存区的总数很小。在分布式系统中,由于有些客户端可能看不到所有的缓存区,当有些客户端将哈希值分布到缓存区时,可能造成不一致。传播性就行应对这种不一致现象的,显而易见,一个优秀的一致性哈希函数应该具有较低的传播性。
  • 负载性(Load):表示对于一个特定的缓存区,分布在它上的不同哈希值应该很少。因为客户端可能同一个哈希值分布到不同缓存区,同理对于同一个缓存区也可能被不同客户端分布不同的哈希值。负载性和传播性类似,只不过传播性是从哈希值的角度看,而负载性是从缓存区的角度讨论。因此,比较好的一致性哈希函数应该具有较低的负载性。
  • 平滑性(Smoothness):将缓存区添加到缓存集或从其中删除时,必须移动到新缓存中的对象的预期比例是维持缓存之间平衡负载所需的最低要求。简而言之,缓存集中的平滑变化与缓存对象位置的变化应该一致。

2.1 哈希环

由于 Hash 算法的结果一般是 unsigned int,因此 Consistent Hashing 的哈希值空间是 [ 0 , 2 32 − 1 ] [0, 2^{32} -1] [0,2321],并且会将整个哈希值空间组织成一个虚拟的环,即哈希环(Hash Ring),如图 1 所示。
一文读懂一致性哈希算法_第1张图片
得到哈希环之后,就可以对缓存服务器的 IP 或主机名等唯一标识进行哈希运算定位到在哈希环中的位置。在图 2 中的哈希环中有三台服务器。
一文读懂一致性哈希算法_第2张图片
为了使服务器和数据的哈希值在同一个哈希值空间,就需要对数据使用相同的哈希函数计算出哈希值,从而得到数据在哈希环上的位置。然后,从此位置沿顺时针方向出发,遇到的第一台服务器就是其对应的服务器。根据这种计算逻辑,可以找到在图 3 中数据 Object 和缓存服务器的对应关系:Object1 → Cache B,Object2 → Cache C,Object 3 → Cache A,Object 4 → Cache A。
一文读懂一致性哈希算法_第3张图片

2.2 增删服务器

在分布式环境中,不可避免地会由于某些原因出现增删服务器的情况。当增加服务器的时候,对于一致性哈希,只会影响从新增服务器所在哈希环位置开始,沿逆时针方向遇到第一台服务器的位置之间的数据,而这些数据会映射到新增的服务器。如图 4 哈希环中新增服务器 Cache D,原本映射到 Cache A 的数据 Object 3 会映射到 Cache D,而其他数据包括 Object 4 的映射关系不会改变。
一文读懂一致性哈希算法_第4张图片
同理,删除服务器(如服务器宕机等)时,根据一致性哈希算法,也只会影响从所删除服务器的位置开始,沿逆时针方向遇到的第一台服务的位置之间的数据,同时这些数据会映射到沿顺时针方向遇到的第一台服务器。在图 5 中,服务器 Cache B 由于某些原因下线了,此时数据 Object 1 会重新映射到服务器 Cache C,而其他数据的映射关系不变。
一文读懂一致性哈希算法_第5张图片

2.3 虚拟节点

一致性哈希算法在服务器比较少的时候,会出现分布不均匀的热点(Hot Spotting)现象,从而造成数据倾斜的问题。图 6 中有三台服务器,但是却有 4 份数据映射到了服务器 Cache A,而服务器 Cache B 和 Cache C 都只映射到了一份数据,造成 Cache A 出现了热点问题。
一文读懂一致性哈希算法_第6张图片
为了解决热点问题,一致性哈希算法引入了虚拟节点(Virtual Nodes)。虚拟节点是哈希环中服务器的副本,每个物理的服务器会对应于环中一个或多个虚拟节点。当添加缓存服务器时,会在哈希环中为它创建了一些虚拟节点;当缓存服务器被移除时,会移除哈希环中其对应所有的虚拟节点。如图 7 中,物理服务器和虚拟节点的对应关系如下: Cache A → Cache A#1 和 Cache A#2,Cache B → Cache B#1 和 Cache B#2、Cache C → Cache C#1 和 Cache C#2。
一文读懂一致性哈希算法_第7张图片
下面给出一致性哈希算法的 Java 简单实现:

public class ConsistentHashing {

    private static final String SEPARATOR = "#";
    private final int replicaFactor;
    private final HashFunction hashFunction;
    private TreeMap<Long, String> hashCircle = new TreeMap<>();

    public ConsistentHashing(int replicaFactor, HashFunction hashFunction) {
        this.replicaFactor = replicaFactor;
        this.hashFunction = hashFunction;
    }

    public String get(String node) {
        if (hashCircle.isEmpty()) {
            return null;
        }

        Long key = hashFunction.hash(node);
        System.out.println("key=>" + key);
        SortedMap<Long, String> virtualNodes = hashCircle.tailMap(key);
        return virtualNodes.isEmpty()
                ? hashCircle.get(hashCircle.firstKey())
                : hashCircle.get(virtualNodes.firstKey());
    }

    public void add(String node) {
        for (int i = 0; i < replicaFactor; i++) {
            hashCircle.put(hashFunction.hash(node + SEPARATOR + i), node + SEPARATOR + i);
        }
    }

    public void remove(String node) {
        for (int i = 0; i < replicaFactor; i++) {
            hashCircle.remove(hashFunction.hash(node + SEPARATOR + i));
        }
    }
    
    private interface HashFunction {
        Long hash(String key);
    }

    private static class FVNHashFunction implements HashFunction {
        @Override
        public Long hash(String key) {
            final int p = 16777619;
            Long hash = 2166136261L;
            for (int i = 0, length = key.length(); i < length; i++) {
                hash = (hash ^ key.charAt(i)) * p;
            }
            hash += hash << 13;
            hash ^= hash >> 7;
            hash += hash << 3;
            hash ^= hash >> 17;
            hash += hash << 5;

            return hash < 0 ? -hash : hash;
        }
    }
}

3. 总结

使用一致性哈希算法时,当发生增删服务器的时候,并不能彻底杜绝数据迁移,但是却可以有效地避免全量数据迁移。同时,一致性哈希算法使用虚拟节点可以解决热点问题。

扫码关注公众号:冰山烈焰的黑板报
一文读懂一致性哈希算法_第8张图片

你可能感兴趣的:(分布式)