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
《大型網站高性能架構》

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