5 分钟精通一致性 Hash

起源

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

性质

  1. 平衡性
  2. 单调性
  3. 分散性
  4. 负载
  5. 平滑性

考虑因素

  1. 数据的均衡性:计算 Hash
  2. 扩容:减少数据迁移,避免数据不平衡
  3. 宕机:减少数据迁移,避免数据不平衡

算法实现

参考《大型网站高性能架构》6.3

1、hash 取余的问题:

1.1. key 计算 hash 取余,找到节点

优点:数据均衡

缺点:扩容导致 N/(N+1) 节点缓存失效,需要数据迁移

2、一致性hash

2.1、长度 N 的 Hash 环(使用二叉树实现)

2.2、节点计算 Hash

2.3、key计算 Hash 顺时针找到对应的节点

优点:解决了扩容导致的缓存失效。增加节点失效比例由 N/(N+1) 变为 1/(N+1)

缺点:数据不均衡

3、基于虚拟节点的一致性 Hash

3.1、长度 N 的 Hash 环(使用二叉树实现)

3.2、每个服务器分配 M 个虚拟机节点

3.2、虚拟节点分布到 Hash 环上

3.3、key计算 Hash 顺时针找到对应的虚拟节点

3.4、通过虚拟节点找到物理节点

优点:解决了扩容导致的数据不均衡问题。各个阶段都分担几乎相同的失效数据

应用场景

减少扩容时数据的迁移

  1. 分布式存储
  2. 分布式缓存

一致性 Hash 代码示例

要点

  1. hash 函数:MD5,crc32,crc64 等
  2. 存储hash环的数据结构:TreeMap,平衡二叉树
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.Iterator;
import java.util.SortedMap;
import java.util.TreeMap;

/**
* @author liuwenxue
* @date 2020-06-18
*/
public class ConsistentHash<T extends Node> {
    // 节点 key:虚拟节点
    private final SortedMap<Long, VirtualNode<T>> ring = new TreeMap<>();
    private final HashFunction hashFunction;

    public ConsistentHash(Collection<T> pNodes, int vNodeCount) {
        this(pNodes,vNodeCount, new MD5Hash());
    }

    /**
     * 虚拟节点
     *
     * @param pNodes 物理节点
     * @param vNodeCount 虚拟节点数量
     * @param hashFunction 函数函数
     */
    public ConsistentHash(Collection<T> pNodes, int vNodeCount, HashFunction hashFunction) {
        if (hashFunction == null) {
            throw new NullPointerException("Hash Function is null");
        }
        this.hashFunction = hashFunction;
        if (pNodes != null) {
            for (T pNode : pNodes) {
                addNode(pNode, vNodeCount);
            }
        }
    }

    /**
     * 将 pNode 加入 ring
     *
     * @param pNode 物理节点
     * @param vNodeCount 虚拟节点数量
     */
    public void addNode(T pNode, int vNodeCount) {
        if (vNodeCount < 0) {
            throw new IllegalArgumentException("illegal virtual node counts :" + vNodeCount);
        }
        int existingReplicas = getExistingReplicas(pNode);
        for (int i = 0; i < vNodeCount; i++) {
            VirtualNode<T> vNode = new VirtualNode<>(pNode, i + existingReplicas);
            ring.put(hashFunction.hash(vNode.getKey()), vNode);
        }
    }

    /**
     *  从 ring 中删除物理机节点
     *
     * @param pNode 物理节点
     */
    public void removeNode(T pNode) {
        Iterator<Long> it = ring.keySet().iterator();
        while (it.hasNext()) {
            Long key = it.next();
            VirtualNode<T> virtualNode = ring.get(key);
            if (virtualNode.isVirtualNodeOf(pNode)) {
                it.remove();
            }
        }
    }

    /**
     * 找到对象 Key 对应的物理机节点
     *
     * @param objectKey 对象的 key
     * @return 对应的物理节点
     */
    public T routeNode(String objectKey) {
        if (ring.isEmpty()) {
            return null;
        }
        Long hashVal = hashFunction.hash(objectKey);
        if (!ring.containsKey(objectKey)) {
            SortedMap<Long,VirtualNode<T>> tailMap = ring.tailMap(hashVal);
            hashVal = tailMap.isEmpty() ? ring.firstKey() : tailMap.firstKey() ;
        }
        return ring.get(hashVal).getPhysicalNode();
    }

    /**
     * 找到某个物理节点的虚拟节点数量
     *
     * @param pNode 物理机节点
     * @return 虚拟节点数量
     */
    public int getExistingReplicas(T pNode) {
        int replicas = 0;
        for (VirtualNode<T> vNode : ring.values()) {
            if (vNode.isVirtualNodeOf(pNode)) {
                replicas++;
            }
        }
        return replicas;
    }

    private static class MD5Hash implements HashFunction {
        MessageDigest instance;

        public MD5Hash() {
            try {
                instance = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
            }
        }

        @Override
        public long hash(String key) {
            instance.reset();
            instance.update(key.getBytes());
            byte[] digest = instance.digest();

            long h = 0;
            for (int i = 0; i < 4; i++) {
                h <<= 8;
                h |= ((int) digest[i]) & 0xFF;
            }
            return h;
        }
    }
}

public interface HashFunction {
    long hash(String key);
}

public interface Node {
    String getKey();
}

public class VirtualNode<T extends Node> implements Node {
    final T physicalNode;
    final int replicaIndex;

    public VirtualNode(T physicalNode, int replicaIndex) {
        this.replicaIndex = replicaIndex;
        this.physicalNode = physicalNode;
    }

    @Override
    public String getKey() {
        return physicalNode.getKey() + "-" + replicaIndex;
    }

    public boolean isVirtualNodeOf(T pNode) {
        return physicalNode.getKey().equals(pNode.getKey());
    }

    public T getPhysicalNode() {
        return physicalNode;
    }
}

开源实现分析

Apache Cassandra:在数据分区(Data partitioning)中使用到一致性哈希

GlusterFS:文件分布使用了一致性哈希,https://www.gluster.org/glusterfs-algorithms-distribution/

Akka:Akka 的 Router 使用了一致性哈希,https://doc.akka.io/docs/akka/snapshot/routing.html

参考

http://en.wikipedia.org/wiki/Consistent_hashing
http://arxiv.org/pdf/1406.2294v1.pdf
《大型网站高性能架构》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章